20-bo‘lim

Amaliy loyiha - kitob do'koni tahlili

Sxema va cheklovlardan boshlab upsert, oyna funksiyalari, CTE, JSONB, indeks va trigger bilan tugaydigan to'liq amaliy loyiha.

🕑 14 daqiqa o‘qish 📄 731 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Loyihaning tuzilmasi
  2. 1-vazifa: umumiy ko'rsatkichlar
  3. 2-vazifa: eng ko'p sotilgan kitoblar
  4. 3-vazifa: oylik dinamika
  5. 4-vazifa: shahar bo'yicha reyting
  6. 5-vazifa: teglar va JSONB
  7. 6-vazifa: yangi kitobni xavfsiz qo'shish
  8. 7-vazifa: narx tarixini yozish
  9. 8-vazifa: indeks va reja
  10. 9-vazifa: ko'rinish
  11. 10-vazifa: buyurtmani xavfsiz qabul qilish
  12. Keyingi qadamlar
  13. Xulosa

Yigirma bobda o'rgangan hamma narsani bitta ishchi loyihada birlashtiramiz. Loyiha - Namangandagi kitob do'koni uchun sotuv tahlili.

SQL
CREATE TABLE muallif (
    id  int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    ism text NOT NULL UNIQUE
);

CREATE TABLE kitob (
    id         int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    isbn       text NOT NULL UNIQUE,
    nom        text NOT NULL,
    muallif_id int  NOT NULL REFERENCES muallif(id),
    narx       numeric(10,2) NOT NULL CHECK (narx > 0),
    teglar     text[] NOT NULL DEFAULT '{}',
    xossa      jsonb  NOT NULL DEFAULT '{}'
);

CREATE TABLE mijoz (
    id     int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    ism    text NOT NULL,
    shahar text NOT NULL
);

CREATE TABLE buyurtma (
    id       int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    mijoz_id int  NOT NULL REFERENCES mijoz(id),
    sana     date NOT NULL
);

CREATE TABLE buyurtma_qator (
    buyurtma_id int NOT NULL REFERENCES buyurtma(id) ON DELETE CASCADE,
    kitob_id    int NOT NULL REFERENCES kitob(id),
    soni        int NOT NULL CHECK (soni > 0),
    narx        numeric(10,2) NOT NULL CHECK (narx > 0),
    PRIMARY KEY (buyurtma_id, kitob_id)
);

INSERT INTO muallif (ism) VALUES
    ('Abdulla Qodiriy'), ('Cho''lpon'), ('Oybek'), ('Said Ahmad');

INSERT INTO kitob (isbn, nom, muallif_id, narx, teglar, xossa) VALUES
    ('978-1', 'Otkan kunlar',   1,  85000, '{roman,tarixiy}',
     '{"yil":1926,"sahifa":420}'),
    ('978-2', 'Mehrobdan chayon',1, 72000, '{roman,tarixiy}',
     '{"yil":1929,"sahifa":380}'),
    ('978-3', 'Kecha va kunduz', 2, 68000, '{roman}',
     '{"yil":1936,"sahifa":310}'),
    ('978-4', 'Navoiy',          3, 95000, '{roman,tarixiy}',
     '{"yil":1944,"sahifa":520}'),
    ('978-5', 'Ufq',             4, 64000, '{roman,zamonaviy}',
     '{"yil":1964,"sahifa":290}');

INSERT INTO mijoz (ism, shahar) VALUES
    ('Husanboy', 'Namangan'),
    ('Malika',   'Namangan'),
    ('Nodira',   'Toshkent'),
    ('Aziza',    'Samarqand'),
    ('Kamola',   'Namangan');

INSERT INTO buyurtma (mijoz_id, sana) VALUES
    (1, '2026-01-12'), (2, '2026-01-18'), (1, '2026-02-03'),
    (3, '2026-02-14'), (4, '2026-02-27'), (5, '2026-03-05'),
    (2, '2026-03-11'), (1, '2026-03-22');

INSERT INTO buyurtma_qator (buyurtma_id, kitob_id, soni, narx) VALUES
    (1, 1, 1, 85000), (1, 3, 2, 68000),
    (2, 4, 1, 95000),
    (3, 2, 1, 72000), (3, 5, 1, 64000),
    (4, 1, 3, 85000),
    (5, 4, 1, 95000), (5, 2, 2, 72000),
    (6, 5, 1, 64000),
    (7, 1, 1, 85000), (7, 4, 1, 95000),
    (8, 3, 1, 68000);

Loyihaning tuzilmasi #

Beshta jadval va ular orasidagi bog'lanishlar muallif id, ism kitob isbn, narx, teglar, xossa mijoz id, ism, shahar 1 : N buyurtma_qator soni, narx buyurtma mijoz_id, sana buyurtma_qator - bog'lovchi jadval Uning kaliti ikkita ustundan: (buyurtma_id, kitob_id) Shu tufayli bir buyurtmada bir kitob ikki marta chiqmaydi
Bir buyurtmada ko'p kitob, bir kitob ko'p buyurtmada - bu ko'pga-ko'p bog'lanish
Nega narx ikki joyda saqlanadi

kitob.narx - bugungi narx. buyurtma_qator.narx - sotilgan paytdagi narx.

Bu takrorlash emas, balki ataylab: kitob narxi ertaga oshsa, o'tgan oydagi hisobot o'zgarib ketmasligi kerak.

Umumiy qoida: o'tmishdagi hodisa yozuvi o'sha paytdagi qiymatni o'zida saqlashi kerak - boshqa jadvalga havola qilmasligi kerak.

1-vazifa: umumiy ko'rsatkichlar #

Avval do'kon haqida umumiy tasavvur hosil qilamiz:

SQL
SELECT count(DISTINCT b.id)                 AS buyurtmalar,
       count(DISTINCT b.mijoz_id)           AS mijozlar,
       sum(q.soni)                          AS sotilgan_nusxa,
       sum(q.soni * q.narx)                 AS tushum,
       round(avg(q.soni * q.narx), 0)       AS ortacha_qator
FROM buyurtma b
JOIN buyurtma_qator q ON q.buyurtma_id = b.id;
Natija
 buyurtmalar | mijozlar | sotilgan_nusxa |   tushum   | ortacha_qator
-------------+----------+----------------+------------+---------------
           8 |        5 |             16 | 1258000.00 |        104833
(1 row)

2-vazifa: eng ko'p sotilgan kitoblar #

JOIN va agregat bilan:

SQL
SELECT k.nom,
       m.ism                AS muallif,
       sum(q.soni)          AS nusxa,
       sum(q.soni * q.narx) AS tushum
FROM buyurtma_qator q
JOIN kitob k   ON k.id = q.kitob_id
JOIN muallif m ON m.id = k.muallif_id
GROUP BY k.nom, m.ism
ORDER BY nusxa DESC, k.nom;
Natija
       nom        |     muallif     | nusxa |  tushum
------------------+-----------------+-------+-----------
 Otkan kunlar     | Abdulla Qodiriy |     5 | 425000.00
 Kecha va kunduz  | Cho'lpon        |     3 | 204000.00
 Mehrobdan chayon | Abdulla Qodiriy |     3 | 216000.00
 Navoiy           | Oybek           |     3 | 285000.00
 Ufq              | Said Ahmad      |     2 | 128000.00
(5 rows)

3-vazifa: oylik dinamika #

date_trunc va oyna funksiyasi bilan o'sishni ko'ramiz:

SQL
WITH oylik AS (
    SELECT date_trunc('month', b.sana)::date AS oy,
           sum(q.soni * q.narx)              AS tushum
    FROM buyurtma b
    JOIN buyurtma_qator q ON q.buyurtma_id = b.id
    GROUP BY 1
)
SELECT oy,
       tushum,
       lag(tushum) OVER (ORDER BY oy)                AS otgan_oy,
       tushum - lag(tushum) OVER (ORDER BY oy)       AS farq,
       sum(tushum) OVER (ORDER BY oy)                AS jamlangan
FROM oylik
ORDER BY oy;
Natija
     oy     |  tushum   | otgan_oy  |    farq    | jamlangan
------------+-----------+-----------+------------+------------
 2026-01-01 | 316000.00 |           |            |  316000.00
 2026-02-01 | 630000.00 | 316000.00 |  314000.00 |  946000.00
 2026-03-01 | 312000.00 | 630000.00 | -318000.00 | 1258000.00
(3 rows)

lag o'tgan oyni yoniga qo'yadi, sum(...) OVER (ORDER BY) esa yig'ilib boruvchi summani beradi.

4-vazifa: shahar bo'yicha reyting #

Har shaharda eng ko'p xarid qilgan mijozni topamiz:

SQL
WITH mijoz_summa AS (
    SELECT m.shahar,
           m.ism,
           sum(q.soni * q.narx) AS summa
    FROM mijoz m
    JOIN buyurtma b       ON b.mijoz_id = m.id
    JOIN buyurtma_qator q ON q.buyurtma_id = b.id
    GROUP BY m.shahar, m.ism
),
reyting AS (
    SELECT shahar, ism, summa,
           row_number() OVER (PARTITION BY shahar
                              ORDER BY summa DESC) AS orin
    FROM mijoz_summa
)
SELECT shahar, ism, summa
FROM reyting
WHERE orin = 1
ORDER BY summa DESC;
Natija
  shahar   |   ism    |   summa
-----------+----------+-----------
 Namangan  | Husanboy | 425000.00
 Toshkent  | Nodira   | 255000.00
 Samarqand | Aziza    | 239000.00
(3 rows)
Bu naqshni yodda tuting

"Har guruhdagi eng yaxshi N ta" - amalda eng ko'p uchraydigan talablardan biri.

Yechim har doim ikki qadamdan iborat:

  1. row_number() OVER (PARTITION BY guruh ORDER BY ...)
  2. Tashqi so'rovda WHERE orin <= N

WHERE ni to'g'ridan-to'g'ri oyna funksiyasi ustiga yozib bo'lmaydi - shuning uchun CTE yoki ichki so'rov kerak.

Uchta funksiyani ajratib oling:

FunksiyaTeng qiymatlarda
row_number()1, 2, 3, 4 - takrorlanmaydi
rank()1, 1, 3, 4 - o'rin tashlaydi
dense_rank()1, 1, 2, 3 - tashlamaydi

5-vazifa: teglar va JSONB #

Massiv va JSONB birgalikda:

SQL
SELECT k.nom,
       k.teglar,
       (k.xossa->>'yil')::int     AS yil,
       (k.xossa->>'sahifa')::int  AS sahifa
FROM kitob k
WHERE k.teglar @> ARRAY['tarixiy']
  AND (k.xossa->>'sahifa')::int > 350
ORDER BY yil;
Natija
       nom        |     teglar      | yil  | sahifa
------------------+-----------------+------+--------
 Otkan kunlar     | {roman,tarixiy} | 1926 |    420
 Mehrobdan chayon | {roman,tarixiy} | 1929 |    380
 Navoiy           | {roman,tarixiy} | 1944 |    520
(3 rows)

Teg bo'yicha nechta kitob borligini sanash:

SQL
SELECT t.teg, count(*) AS kitoblar
FROM kitob k, unnest(k.teglar) AS t(teg)
GROUP BY t.teg
ORDER BY kitoblar DESC, t.teg;
Natija
    teg    | kitoblar
-----------+----------
 roman     |        5
 tarixiy   |        3
 zamonaviy |        1
(3 rows)

6-vazifa: yangi kitobni xavfsiz qo'shish #

INSERT ... ON CONFLICT bilan - isbn takrorlansa yangilaymiz:

SQL
INSERT INTO kitob (isbn, nom, muallif_id, narx, teglar, xossa)
VALUES ('978-1', 'Otkan kunlar', 1, 92000,
        '{roman,tarixiy,klassika}', '{"yil":1926,"sahifa":420}')
ON CONFLICT (isbn) DO UPDATE
    SET narx   = EXCLUDED.narx,
        teglar = EXCLUDED.teglar
RETURNING isbn, nom, narx, teglar;
Natija
 isbn  |     nom      |   narx   |          teglar
-------+--------------+----------+--------------------------
 978-1 | Otkan kunlar | 92000.00 | {roman,tarixiy,klassika}
(1 row)

INSERT 0 1

Kitob ikki marta qo'shilmadi - narxi va teglari yangilandi.

7-vazifa: narx tarixini yozish #

Trigger bilan avtomatik audit:

SQL
CREATE TABLE narx_tarixi (
    id       int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    kitob_id int NOT NULL,
    eski     numeric(10,2) NOT NULL,
    yangi    numeric(10,2) NOT NULL
);

CREATE FUNCTION narxni_yoz() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
    IF NEW.narx IS DISTINCT FROM OLD.narx THEN
        INSERT INTO narx_tarixi (kitob_id, eski, yangi)
        VALUES (OLD.id, OLD.narx, NEW.narx);
    END IF;
    RETURN NULL;
END;
$$;

CREATE TRIGGER kitob_narx_audit
    AFTER UPDATE ON kitob
    FOR EACH ROW EXECUTE FUNCTION narxni_yoz();

UPDATE kitob SET narx = narx * 1.10 WHERE teglar @> ARRAY['tarixiy'];

SELECT kitob_id, eski, yangi FROM narx_tarixi ORDER BY kitob_id;
Natija
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
UPDATE 3
 kitob_id |   eski   |   yangi
----------+----------+-----------
        1 | 85000.00 |  93500.00
        2 | 72000.00 |  79200.00
        4 | 95000.00 | 104500.00
(3 rows)

8-vazifa: indeks va reja #

Tez-tez ishlatiladigan so'rovlar uchun indeks qo'yamiz:

SQL
CREATE INDEX buyurtma_sana_idx ON buyurtma (sana);
CREATE INDEX qator_kitob_idx   ON buyurtma_qator (kitob_id);
CREATE INDEX kitob_teglar_idx  ON kitob USING gin (teglar);
CREATE INDEX kitob_xossa_idx   ON kitob USING gin (xossa);

SELECT indexname
FROM pg_indexes
WHERE tablename IN ('kitob', 'buyurtma', 'buyurtma_qator')
ORDER BY indexname;
Natija
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
      indexname
---------------------
 buyurtma_pkey
 buyurtma_qator_pkey
 buyurtma_sana_idx
 kitob_isbn_key
 kitob_pkey
 kitob_teglar_idx
 kitob_xossa_idx
 qator_kitob_idx
(8 rows)
IndeksNima uchun
buyurtma (sana)Oylik hisobotlar sana bo'yicha filtrlaydi
buyurtma_qator (kitob_id)JOIN va "bu kitob qancha sotilgan"
gin (teglar)teglar @> ARRAY[...]
gin (xossa)xossa @> '{...}'
Kichik jadvalda indeks ishlamaydi - va bu normal

Yuqoridagi indekslarni qo'yib, EXPLAIN ni ishga tushirsangiz baribir Seq Scan ko'rasiz.

Sabab: bu darslikdagi jadvallarda atigi bir necha o'nlab qator bor. Baza ularni bitta sahifada o'qiy oladi - indeksga borib kelish qimmatroq bo'lardi.

Bu rejalashtiruvchining to'g'ri qarori, xato emas.

Shuning uchun indeks foydasini haqiqiy hajmda o'lchang: kamida o'n minglab qator kerak. 15-bobdagi misolda 50 000 qator aynan shuning uchun ishlatilgan.

9-vazifa: ko'rinish #

Tez-tez kerak bo'ladigan so'rovni VIEW ga aylantiramiz:

SQL
CREATE VIEW kitob_sotuvi AS
SELECT k.id,
       k.nom,
       m.ism                              AS muallif,
       coalesce(sum(q.soni), 0)           AS nusxa,
       coalesce(sum(q.soni * q.narx), 0)  AS tushum
FROM kitob k
JOIN muallif m           ON m.id = k.muallif_id
LEFT JOIN buyurtma_qator q ON q.kitob_id = k.id
GROUP BY k.id, k.nom, m.ism;

SELECT nom, muallif, nusxa, tushum
FROM kitob_sotuvi
WHERE nusxa > 2
ORDER BY tushum DESC;
Natija
CREATE VIEW
       nom        |     muallif     | nusxa |  tushum
------------------+-----------------+-------+-----------
 Otkan kunlar     | Abdulla Qodiriy |     5 | 425000.00
 Navoiy           | Oybek           |     3 | 285000.00
 Mehrobdan chayon | Abdulla Qodiriy |     3 | 216000.00
 Kecha va kunduz  | Cho'lpon        |     3 | 204000.00
(4 rows)

LEFT JOIN va coalesce juftligi muhim: hech sotilmagan kitob ham ro'yxatda 0 bilan turadi, NULL bilan emas.

10-vazifa: buyurtmani xavfsiz qabul qilish #

Hammasi bitta tranzaksiyada:

SQL
BEGIN;

INSERT INTO buyurtma (mijoz_id, sana)
VALUES (5, '2026-03-28')
RETURNING id AS yangi_buyurtma;

INSERT INTO buyurtma_qator (buyurtma_id, kitob_id, soni, narx)
SELECT currval(pg_get_serial_sequence('buyurtma', 'id')),
       k.id, 2, k.narx
FROM kitob k WHERE k.isbn = '978-3';

COMMIT;

SELECT b.id, m.ism, b.sana, q.soni, q.narx
FROM buyurtma b
JOIN mijoz m          ON m.id = b.mijoz_id
JOIN buyurtma_qator q ON q.buyurtma_id = b.id
WHERE b.sana = '2026-03-28';
Natija
BEGIN
 yangi_buyurtma
----------------
              9
(1 row)

INSERT 0 1
INSERT 0 1
COMMIT
 id |  ism   |    sana    | soni |   narx
----+--------+------------+------+----------
  9 | Kamola | 2026-03-28 |    2 | 68000.00
(1 row)

Buyurtma sarlavhasi va qatorlari birga saqlandi. Ikkinchi INSERT xato bergan bo'lsa, birinchisi ham bekor bo'lar edi

  • ya'ni qatorsiz "osilib qolgan" buyurtma paydo bo'lmaydi.

Keyingi qadamlar #

Loyihani o'zingiz kengaytirib ko'ring:

Yo'nalishNima qilish
Qidiruvpg_trgm bilan kitob nomidan qidirish
Bo'laklashbuyurtma ni yil bo'yicha bo'lash
Xavfsizlikhisobot_rol yaratib, faqat VIEW ga ruxsat berish
Tezlik100 000 qator yaratib, EXPLAIN ANALYZE bilan o'lchash
Ishonchlilikpg_dump bilan nusxa olib, boshqa bazaga tiklash
Amaliy topshiriq
  1. Sxemani o'zingiz noldan yozing va ma'lumot kiriting.
  2. Umumiy ko'rsatkichlar so'rovini tuzing.
  3. Eng ko'p sotilgan uchta kitobni chiqaring.
  4. Oylik tushumni lag bilan solishtiring.
  5. Har shahardagi eng faol mijozni toping.
  6. Teg va JSONB bo'yicha filtr yozing.
  7. ON CONFLICT bilan kitobni qayta qo'shib ko'ring.
  8. Narx o'zgarishini yozadigan trigger tuzing.
  9. Kerakli indekslarni qo'ying va nima uchun kerakligini yozing.
  10. Buyurtmani bitta tranzaksiyada qabul qiling va sinab ko'ring.

Xulosa #

  • Sxema cheklovlardan boshlanadi: NOT NULL, CHECK, UNIQUE, tashqi kalit.
  • Hodisa yozuvi o'sha paytdagi qiymatni o'zida saqlaydi - buyurtma_qator.narx kabi.
  • JOIN va agregatlar hisobotning asosini tashkil qiladi.
  • date_trunc va lag birga oylik dinamikani beradi.
  • "Har guruhdagi eng yaxshi N ta" - row_number() va tashqi WHERE juftligi.
  • Massiv va JSONB bir jadvalda birga ishlay oladi.
  • ON CONFLICT takroriy yozuvni xatosiz yangilaydi.
  • Trigger narx tarixini avtomatik yozib boradi.
  • Indeks foydasi faqat katta jadvalda ko'rinadi - kichigida Seq Scan normal.
  • Bog'liq yozuvlarni har doim bitta tranzaksiyada saqlang.

Darslik shu yerda tugadi. Endi qo'lingizda PostgreSQL bilan haqiqiy loyiha qurish uchun yetarli bilim bor - eng yaxshi davomi o'z bazangizni yozib ko'rishdir.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.