IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL34/64-dars18 daqiqa
Mundarija (26)

Bog'lanishlar 1:1, 1:N va N:M: diagrammadan jadvallarga

Qisqacha: 1:N da tashqi kalit "ko'p" tomondagi jadvalga yoziladi (izohlar.vazifa_id). N:M ni to'g'ridan-to'g'ri ifodalab bo'lmaydi — ikki tashqi kalitli ko'prik (junction) jadval kerak, uning birlamchi kaliti ikkala ustundan iborat. 1:1 — tashqi kalit ustuni ayni paytda birlamchi kalit ham bo'lgan jadval. Self-relation — jadval o'ziga havola qiladi (ota_id).

Bu darsda

  • Har bog'lanish turida tashqi kalit qaysi jadvalga yozilishini aniq bilasiz.
  • izohlar jadvalini (1:N) quramiz va ON DELETE CASCADE bilan SET NULL farqini amalda ko'rasiz.
  • Ko'prik jadvalni tarkibiy kalit va qo'shimcha maydonlar bilan loyihalaysiz.
  • 1:1 bog'lanishni ikki usulda yozasiz va qachon kerakligini bilasiz.
  • Bazadagi hamma bog'lanishlarni katalogdan bitta so'rov bilan chiqarasiz.

Oldin bilishingiz kerak: ER modellash va diagrammalar, Foreign key va referential actions, UPSERT va MERGE, Rekursiv CTE.

1. Nega bu kerak?

O'tgan darsda diagrammada chiziq chizdik: vazifalar ||--o{ izohlar. Endi uni SQL'ga aylantirish kerak. Savol oddiy ko'rinadi: tashqi kalitni qayerga yozamiz — vazifalar ga izoh_id mi yoki izohlar ga vazifa_id mi?

Noto'g'ri tanlov qimmatga tushadi. vazifalar.izoh_id desak, vazifada faqat bitta izoh bo'ladi. Ikkinchisini qo'shish uchun izoh2_id, izoh3_id ustunlari kerak bo'ladi — bu yo'lning oxiri yo'q.

O'xshatish: maktab sinfi. Sinf jurnaliga 30 o'quvchining ismini ustun qilib yozish mumkin emas — o'quvchi soni har yili boshqa. Buning o'rniga har o'quvchining kartochkasiga "sinfi: 9-B" deb yoziladi. "Ko'p" tomonda — havola, "bitta" tomonda — hech narsa. Bu qoida bog'lanishlarning asosi.

2. 1:N — eng ko'p uchraydigan bog'lanish

2.1 Qoida: tashqi kalit "ko'p" tomonda

Birga-ko'p (one-to-many, 1:N) — bitta yozuvga ko'p yozuv bog'lanadi: bitta vazifa — ko'p izoh. Tashqi kalit har doim "ko'p" tomondagi jadvalga yoziladi: izohlar.vazifa_id. Chunki har izohning vazifasi bitta — ustun ham bitta.

Kanon holat 34 da izohlar jadvali qo'shildi. Mana uning to'liq SQL fayli (kanon sql/007-izohlar.sql dan, aynan):

sql
CREATE TABLE izohlar (
  id              bigint      GENERATED ALWAYS AS IDENTITY,
  vazifa_id       bigint      NOT NULL,
  -- muallif o'chirilsa izoh qoladi ("o'chirilgan foydalanuvchi") — SET NULL; hozir muallif = vazifa egasi,
  -- jamoaviy vazifalar — 38-qism capstone (`jamoa`)
  muallif_id      bigint,
  matn            text        NOT NULL,
  yaratilgan_vaqt timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT izohlar_pkey PRIMARY KEY (id),
  CONSTRAINT izohlar_vazifa_id_fkey FOREIGN KEY (vazifa_id) REFERENCES vazifalar (id) ON DELETE CASCADE,
  CONSTRAINT izohlar_muallif_id_fkey FOREIGN KEY (muallif_id) REFERENCES foydalanuvchilar (id) ON DELETE SET NULL,
  CONSTRAINT izohlar_matn_check CHECK (btrim(matn) <> '' AND char_length(matn) <= 2000)
);
-- "vazifaning izohlari, vaqt tartibida" — bitta indeks ham FK'ni, ham saralashni qoplaydi (#57)
CREATE INDEX izohlar_vazifa_id_yaratilgan_vaqt_idx ON izohlar (vazifa_id, yaratilgan_vaqt);
CREATE INDEX izohlar_muallif_id_idx ON izohlar (muallif_id);

Bu bitta jadvalda ikkita 1:N bog'lanish bor:

Tashqi kalit Bog'lanish NOT NULL? O'chirishda
vazifa_id vazifa → izohlar ha — izoh vazifasiz bo'lmaydi CASCADE
muallif_id foydalanuvchi → izohlar yo'q — muallif yo'qolishi mumkin SET NULL

Diagrammadagi belgilar shu yerdan keladi: NOT NULL — || (aniq bitta), NULL mumkin — |o (nol yoki bitta). Ikki indeks ham bejiz emas: Postgres tashqi kalit ustuniga indeksni o'zi yaratmaydi (Foreign key darsidagi qoida). Birinchi indeks (vazifa_id, yaratilgan_vaqt) — ham tashqi kalitni, ham "izohlar vaqt tartibida" so'rovini qoplaydi. Bu haqida indekslar tartibi darsida batafsil gaplashamiz.

Ikki kod qatori 70 belgidan uzun — kanon faylidan aynan olingan, qisqartirmadik. Izohdagi "38-qism capstone" — kursning keyingi qismidagi jamoaviy loyiha.

2.2 Psql'da tekshiramiz

Jadval tuzilishini \d ko'rsatadi (psql chuqur). Kanon chiqishi (PostgreSQL 18.6):

text
                                      Table "public.izohlar"
     Column      |           Type           | Collation | Nullable |           Default
-----------------+--------------------------+-----------+----------+------------------------------
 id              | bigint                   |           | not null | generated always as identity
 vazifa_id       | bigint                   |           | not null |
 muallif_id      | bigint                   |           |          |
 matn            | text                     |           | not null |
 yaratilgan_vaqt | timestamp with time zone |           | not null | now()
Indexes:
    "izohlar_pkey" PRIMARY KEY, btree (id)
    "izohlar_muallif_id_idx" btree (muallif_id)
    "izohlar_vazifa_id_yaratilgan_vaqt_idx" btree (vazifa_id, yaratilgan_vaqt)
Check constraints:
    "izohlar_matn_check" CHECK (btrim(matn) <> ''::text AND char_length(matn) <= 2000)
Foreign-key constraints:
    "izohlar_muallif_id_fkey" FOREIGN KEY (muallif_id) REFERENCES foydalanuvchilar(id) ON DELETE SET NULL
    "izohlar_vazifa_id_fkey" FOREIGN KEY (vazifa_id) REFERENCES vazifalar(id) ON DELETE CASCADE

Nullable ustunida muallif_id bo'sh — demak NULL bo'lishi mumkin. Pastda ikkala tashqi kalit o'z amali bilan.

Namuna ma'lumotda (kanon 008-namuna-izohlar.sql) 15 ta izoh bor. Har vazifada nechta izoh borligini sanaymiz — 1:N bog'lanishning "ko'p" tomonini LEFT JOIN va GROUP BY bilan yig'amiz:

sql
SELECT v.matn, count(i.id) AS izohlar
FROM vazifalar v
LEFT JOIN izohlar i ON i.vazifa_id = v.id
GROUP BY v.id
HAVING count(i.id) > 0
ORDER BY 2 DESC, v.id;
text
                       matn                       | izohlar
--------------------------------------------------+---------
 JavaScript kursining 26-qismini boshlash #o'qish |       3
 Server zaxirasini tekshirish #ish #server        |       2
 Mashinani texnik ko'rikka olib borish #mashina   |       2
 Oshxonadagi jo'mrakni almashtirish #ta'mir #uy   |       2
 Postgres'ni Docker'da ko'tarish #o'qish #docker  |       1
 Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish    |       1
 Buyurtmalarni Telegram'da qabul qilish #ish      |       1
 IELTS writing task 2 mashqi #ingliz              |       1
 Universitet kontrakti #to'lov                    |       1
 Soliq hisobotini topshirish #ish #to'lov         |       1
(10 rows)

O'nta vazifada izoh bor, 15 izoh jami. Qolgan vazifalarda 0 — HAVING ularni olib tashladi.

2.3 CASCADE va SET NULL amalda

Ikki tashqi kalitning amali turli. Kanon ularni tranzaksiyada sinab, oxirida ROLLBACK bilan qaytargan. Biz ikkala tajribani alohida tranzaksiyaga ajratdik — har biri o'zi to'liq. Avval izohni vaqtincha Shahnoza (id 8) nomiga o'tkazamiz va Shahnozani o'chiramiz:

sql
BEGIN;
UPDATE izohlar SET muallif_id = 8 WHERE id = 1;
DELETE FROM foydalanuvchilar WHERE id = 8;
SELECT id, vazifa_id, muallif_id, matn FROM izohlar WHERE id = 1;
ROLLBACK;
text
BEGIN
UPDATE 1
DELETE 1
 id | vazifa_id | muallif_id |                matn
----+-----------+------------+-------------------------------------
  1 |         2 |            | Port 5432 band ekan, 5499 ga o'tdim
(1 row)

ROLLBACK

Izoh qoldi, muallif_id bo'sh — SET NULL ishladi. Ilova uni "o'chirilgan foydalanuvchi" deb ko'rsatadi. Endi vazifani o'chiramiz:

sql
BEGIN;
DELETE FROM vazifalar WHERE id = 1;
SELECT count(*) AS vazifa1_izohlari FROM izohlar WHERE vazifa_id = 1;
ROLLBACK;
text
BEGIN
DELETE 1
 vazifa1_izohlari
------------------
                0
(1 row)

ROLLBACK

1-vazifaning uchta izohi ham ketdi — CASCADE. Vazifasiz izohning ma'nosi yo'q, shuning uchun bu to'g'ri tanlov.

Qaysi amalni tanlash — biznes savoli, texnik emas:

Savol Javob "ha" bo'lsa
Ota-onasiz bola ma'nosizmi? (vazifasiz izoh) CASCADE
Bola qolishi kerak, lekin "egasiz"? (muallifsiz izoh) SET NULL (ustun NULL bo'la olsin)
Bolasi bor ota-onani o'chirish taqiqmi? (to'lovi bor mijoz) RESTRICT / NO ACTION

Tekshirib ko'ring: muallif_id ga NOT NULL qo'shsak, ON DELETE SET NULL bilan nima bo'ladi?

Javob

Foydalanuvchini o'chirishda Postgres muallif_id ni NULL qilishga urinadi va NOT NULL cheklovi bunga yo'l qo'ymaydi — DELETE xato bilan to'xtaydi. SET NULL faqat NULL bo'la oladigan ustunda ma'noga ega. Diagrammada ham shunday: |o (nol yoki bitta) va SET NULL doim birga keladi.

3. N:M — ko'prik jadval

3.1 Nega to'g'ridan-to'g'ri bo'lmaydi

Ko'pga-ko'p (many-to-many, N:M) — ikkala tomonda ham "ko'p": bitta vazifada ko'p teg, bitta teg ko'p vazifada. Tashqi kalitni qaysi tomonga yozsak ham, u faqat bitta qiymat saqlaydi — bu N:M emas, 1:N bo'lib qoladi.

Yechim — uchinchi jadval. Ko'prik jadval (junction table) — har qatori bitta juftlikni saqlaydi: "shu vazifa — shu teg". Bizda bu vazifa_teglari, uni UPSERT va MERGE darsida qurgan edik. Uning kaliti — tarkibiy birlamchi kalit (composite primary key), ya'ni ikki ustundan iborat kalit: (vazifa_id, teg_id).

Nimaga qarang: N:M bog'lanish aslida ikkita 1:N — har ikki tomon ko'prik jadvalga "bitta → ko'p" bilan ulanadi.

erDiagram
    vazifalar ||--o{ vazifa_teglari : "vazifa_id"
    teglar ||--o{ vazifa_teglari : "teg_id"
    vazifa_teglari {
        bigint vazifa_id PK, FK
        bigint teg_id PK, FK
    }

Ko'prik orqali so'rov — ikki marta birlashtirish (JOIN). Biz o'z PostgreSQL 17.6 klasterimizda (holat 34) ishga tushirdik:

sql
SELECT v.matn, string_agg(t.nom, ' ' ORDER BY t.nom) AS teglar
FROM vazifalar v
JOIN vazifa_teglari vt ON vt.vazifa_id = v.id
JOIN teglar t ON t.id = vt.teg_id
WHERE v.id IN (1, 2)
GROUP BY v.id
ORDER BY v.id;
text
                       matn                       |     teglar
--------------------------------------------------+-----------------
 JavaScript kursining 26-qismini boshlash #o'qish | #o'qish
 Postgres'ni Docker'da ko'tarish #o'qish #docker  | #docker #o'qish
(2 rows)

3.2 Tarkibiy kalit takrorni to'sadi

Bir juftlikni ikki marta yozish ma'nosiz: "1-vazifa — #o'qish" ikki marta bo'lsa, teglar soni noto'g'ri sanaladi. Tarkibiy birlamchi kalit buni bazada to'sadi. Kanon chiqishi:

sql
INSERT INTO vazifa_teglari
VALUES (1, (SELECT teg_id FROM vazifa_teglari
            WHERE vazifa_id = 1 LIMIT 1));
text
ERROR:  duplicate key value violates unique constraint "vazifa_teglari_pkey"
DETAIL:  Key (vazifa_id, teg_id)=(1, 4) already exists.

DETAIL qatoridagi teg raqami bazaning collation'iga bog'liq: 006-teglar.sql teglarni egasi va nomi bo'yicha alifbo tartibida raqamlaydi, alifbo tartibi esa collation'dan keladi. Kanon bazasi (en_US.utf8) va kanonga o'xshash tartibli bizning PostgreSQL 17.6 klasterimiz (1, 4) berdi; C collation'li bazada raqam (1, 3) bo'lgan. Xatoning o'zi bir xil.

3.3 Kalit tartibi va ikkinchi indeks

Tarkibiy kalit (vazifa_id, teg_id) — ayni paytda indeks. U "1-vazifaning teglari" so'rovini tez bajaradi, chunki birinchi ustun bo'yicha qidiradi. Teskari so'rov — "#o'qish tegli vazifalar" — ikkinchi ustundan boshlanadi va bu indeks yordam bermaydi. Shuning uchun kanon 006-teglar.sql da alohida teg_id indeksi bor. Ko'prik jadvalda odatda: tarkibiy PK + ikkinchi ustunga indeks.

3.4 Qo'shimcha maydonli ko'prik

Ko'prik jadval faqat ikki ustundan iborat bo'lishi shart emas. Juftlikning o'ziga tegishli ma'lumot ham shu yerga yoziladi:

  • vazifaga teg qachon qo'yilgan;
  • kursga yozilgan talabaning bahosi (talaba ↔ kurs);
  • buyurtmadagi mahsulotning soni (buyurtma ↔ mahsulot).

Bunday jadval ko'pincha o'z nomini oladi: buyurtma_qatorlari, azoliklar. U endi "texnik ko'prik" emas, o'z ma'nosi bor entity. 3-mashqda shunday jadval quramiz.

Maslahat: Ko'prik jadvalga alohida id (surrogat kalit) kerakmi? Agar boshqa jadval unga havola qilmasa — yo'q, tarkibiy kalit yetarli va takrorni o'zi to'sadi. Agar havola kerak bo'lsa (masalan, buyurtma qatoriga to'lov bog'lanadi) — id qo'shing, lekin UNIQUE (buyurtma_id, mahsulot_id) ni ham qoldiring.

Tekshirib ko'ring: Talabalar va kurslar N:M. Talaba bitta kursga ikki marta yozila olmasligi kerak. Buni qanday kafolatlaysiz?

Javob

Ko'prik jadval azoliklar (talaba_id, kurs_id, …) ning birlamchi kaliti (talaba_id, kurs_id) bo'lsin. Ikkinchi yozuv duplicate key value violates unique constraint bilan rad etiladi. Ilova kodidagi tekshiruv ("avval SELECT, keyin INSERT") yetmaydi — ikki so'rov bir vaqtda kelsa, ikkalasi ham "yo'q" deb ko'radi.

4. 1:1 — birga-bir

4.1 Qachon kerak

Birga-bir (one-to-one, 1:1) — har yozuvga ko'pi bilan bitta yozuv mos keladi. Ko'pincha savol tug'iladi: unda nega alohida jadval? Ustunlarni asosiy jadvalga qo'shsa bo'lmaydimi? Ko'p hollarda — bo'ladi va shunday qilish kerak. Alohida jadval faqat sababi bo'lsa:

  • Ixtiyoriy qism: foydalanuvchilarning 5 foizida to'ldiriladigan 20 ta ustun (masalan, kompaniya rekvizitlari).
  • Katta yoki kam o'qiladigan ma'lumot: rasm, uzun bio — asosiy jadval yengil qolsin.
  • Boshqa ruxsatlar: maxfiy maydonlarni alohida jadvalda saqlab, kirishni cheklash.

4.2 PK = FK

Eng toza usul: bola jadvalning birlamchi kaliti ayni paytda tashqi kalit. Kanon buni vaqtinchalik t34_profil jadvalida sinagan (kanon sxemasida profil yo'q):

sql
BEGIN;
CREATE TABLE t34_profil (
  foydalanuvchi_id bigint PRIMARY KEY
    REFERENCES foydalanuvchilar ON DELETE CASCADE,
  telegram text,
  tema text DEFAULT 'tizim'
);
INSERT INTO t34_profil VALUES (1, '@aziz_k');
INSERT INTO t34_profil VALUES (1, '@boshqa');
text
BEGIN
CREATE TABLE
INSERT 0 1
ERROR:  duplicate key value violates unique constraint "t34_profil_pkey"
DETAIL:  Key (foydalanuvchi_id)=(1) already exists.

Xatodan keyin tranzaksiya "buzilgan" holatda: undagi keyingi buyruqlar bajarilmaydi, faqat ROLLBACK; qabul qilinadi (ACID darsidagi current transaction is aborted). ROLLBACK; yozing — jadval ham, qator ham yo'qoladi. psql seansi yopilsa, Postgres ochiq tranzaksiyani o'zi bekor qiladi.

Ikkinchi profil rad etildi — bitta foydalanuvchiga bitta profil. Kalit va havola bitta ustunda, shuning uchun alohida id kerak emas.

Ikkinchi usul: bola jadvalda o'z id si va foydalanuvchi_id bigint UNIQUE REFERENCES …. Natija bir xil (UNIQUE ikkinchi qatorni to'sadi), lekin bitta ortiqcha ustun va indeks. Agar UNIQUE ni unutsangiz — jadval jim 1:N bo'lib qoladi.

4.3 Vaqtinchalik jadval tuzog'i

Kanon tajribaning birinchi urinishida t34_profil vaqtinchalik jadval (CREATE TEMP TABLE) qilib yaratilgan va yiqilgan. Biz ham o'z klasterimizda takrorladik:

sql
CREATE TEMP TABLE t34_vaqtincha (
  foydalanuvchi_id bigint REFERENCES foydalanuvchilar
);
text
ERROR:  constraints on temporary tables may reference only temporary tables

Tarjimasi: "vaqtinchalik jadvallardagi cheklovlar faqat vaqtinchalik jadvallarga havola qila oladi". Sababi mantiqiy: vaqtinchalik jadval seans tugashi bilan yo'qoladi, doimiy jadval esa uning yozuvlarini kuzatib borolmaydi. Shuning uchun kanon oddiy jadval + BEGIN … ROLLBACK usuliga o'tgan.

5. Self-relation — jadval o'ziga

O'z-o'ziga bog'lanish (self-relation) — tashqi kalit o'sha jadvalga qaytadi. Bizda bu vazifalar.ota_id: kichik vazifa boshqa vazifaga tegishli. Uni ALTER TABLE darsida qo'shgan va Rekursiv CTE darsida daraxt sifatida o'qigan edik.

Jadval yangi narsa talab qilmaydi: bu oddiy 1:N, faqat "ota" va "bola" — bitta jadval. Ikki nozik joyni eslatamiz:

  • ota_id NULL bo'la oladi — aks holda birinchi vazifani yozib bo'lmasdi (unga ota kerak bo'lardi).
  • Bizda tashqi kalit tarkibiy: (ota_id, egasi_id) → vazifalar (id, egasi_id). U oddiy ota_id → id dan kuchliroq: Azizning vazifasi Dilnozaning vazifasiga "bola" bo'lib qololmaydi.

Boshqa misollar: xodim → rahbar (rahbar_id), izohga javob (javob_berilgan_id), kategoriya → ota kategoriya. Daraxtlarni saqlashning boshqa usullari ham bor — ularni Modellash naqshlari darsida solishtiramiz.

6. Hamma bog'lanishlar — bitta so'rovda

O'tgan darsda katalogdan tashqi kalitlar ro'yxatini oldik. Endi ON DELETE amalini ham so'z bilan chiqaramiz. Holat 34 da (biz ishga tushirdik, PG 17.6; kanon 18.6 chiqishi bir xil):

sql
SELECT conrelid::regclass AS bola,
       confrelid::regclass AS ota,
       CASE confdeltype
         WHEN 'c' THEN 'CASCADE'  WHEN 'n' THEN 'SET NULL'
         WHEN 'r' THEN 'RESTRICT' WHEN 'a' THEN 'NO ACTION'
       END AS on_delete
FROM pg_constraint
WHERE contype = 'f'
ORDER BY 1, 2;
text
      bola      |       ota        | on_delete
----------------+------------------+-----------
 vazifalar      | foydalanuvchilar | CASCADE
 vazifalar      | vazifalar        | CASCADE
 teglar         | foydalanuvchilar | CASCADE
 vazifa_teglari | vazifalar        | CASCADE
 vazifa_teglari | teglar           | CASCADE
 izohlar        | foydalanuvchilar | SET NULL
 izohlar        | vazifalar        | CASCADE
(7 rows)

Yettita qator — yettita bog'lanish: to'rtta 1:N (foydalanuvchi→vazifa, foydalanuvchi→teg, vazifa→izoh, foydalanuvchi→izoh), bitta self-relation va N:M ning ikki oyog'i. Yagona SET NULL — muallif.

Bu so'rov katta bazani o'rganishda juda foydali: "foydalanuvchini o'chirsam, nima ketadi?" — CASCADE zanjirini kuzatib chiqasiz: foydalanuvchi → vazifalari → ularning izohlari va teglari.

7. Ko'p uchraydigan xatolar

1. Tashqi kalit "bitta" tomonda. vazifalar.izoh_id — vazifada bitta izoh. Belgisi: izoh1_id, izoh2_id kabi raqamli ustunlar yoki "nechta bo'lishi mumkin?" savoliga "hozircha bitta" javobi. Tuzatish: tashqi kalit "ko'p" tomonga.

2. N:M ni vergulli matnda saqlash. teglar = 'uy,ish,oila'. Qidiruv LIKE '%uy%' uyqu ni ham topadi, teg nomini o'zgartirish — hamma qatorni tahrirlash. Bu xatoni Normalizatsiya darsida o'lchov bilan ko'ramiz. Tuzatish: ko'prik jadval.

3. Ko'prik jadvalda kalitsiz. vazifa_teglari (vazifa_id, teg_id) — PK ham, UNIQUE ham yo'q. Bir juftlik bir necha marta yoziladi va hisoblar ikki baravar chiqadi. Tuzatish: tarkibiy PK.

4. 1:1 da UNIQUE'siz tashqi kalit. profillar.foydalanuvchi_id REFERENCES … — UNIQUE yo'q. Jadval jim 1:N bo'ladi: bitta foydalanuvchida ikki profil va ilova qaysi birini olishni bilmaydi. Tuzatish: PK = FK yoki UNIQUE.

5. Tashqi kalit ustuniga indeks yo'q. CASCADE bilan ota-onani o'chirishda Postgres bola jadvalni har safar to'liq ko'rib chiqadi. Tuzatish: har tashqi kalit ustuniga indeks — kanon review'da buni so'rov bilan tekshiramiz (Real loyiha sxemasi).

6. ON DELETE ni o'ylamasdan tanlash. Hamma joyda CASCADE — bitta DELETE minglab qatorni olib ketishi mumkin (to'lov tarixi ham). Har bog'lanish uchun «CASCADE va SET NULL amalda» bo'limidagi jadval savollarini bering.

8. Mashqlar

Mashqlar holat 34 da (namuna ma'lumot bilan). Jadval yaratadigan mashqlarni BEGIN … ROLLBACK ichida bajaring — kanon sxemasi o'zgarmasin.

1-mashq (oson)

Har foydalanuvchi nechta izoh yozganini chiqaring — izoh yozmaganlar ham 0 bilan ko'rinsin. Ustunlar: ism, izohlar; ko'pidan kamiga, teng bo'lsa id bo'yicha. Ishora: «Psql'da tekshiramiz» bo'limidagi sanash so'rovi, faqat bog'lanish muallif_id orqali.

Yechim
sql
SELECT f.ism, count(i.id) AS izohlar
FROM foydalanuvchilar f
LEFT JOIN izohlar i ON i.muallif_id = f.id
GROUP BY f.id
ORDER BY izohlar DESC, f.id;
text
        ism         | izohlar
--------------------+---------
 Aziz Karimov       |       6
 Sardor Tursunov    |       3
 Javohir Olimov     |       2
 Dilnoza Rahimova   |       1
 Madina Yusupova    |       1
 Gulnora Saidova    |       1
 Bekzod Nazarov     |       1
 Shahnoza Ergasheva |       0
(8 rows)

count(*) emas, count(i.id): LEFT JOIN Shahnoza uchun bitta "bo'sh" qator beradi va count(*) uni 1 deb sanardi. count(i.id) NULL'ni sanamaydi (NULL mantiqi).

2-mashq (o'rta)

1:1 bog'lanish: profillar jadvalini yarating — foydalanuvchi_id (PK va FK, foydalanuvchi o'chsa profil ham o'chsin), telegram, shahar. 1- va 2-foydalanuvchiga profil yozing, keyin 2-foydalanuvchini o'chiring va profillarni chiqaring. Hammasi BEGIN … ROLLBACK ichida. Ishora: «PK = FK» bo'limidagi t34_profil.

Yechim
sql
BEGIN;
CREATE TABLE profillar (
  foydalanuvchi_id bigint PRIMARY KEY
    REFERENCES foydalanuvchilar ON DELETE CASCADE,
  telegram text,
  shahar   text
);
INSERT INTO profillar VALUES (1, '@sizning_nik', 'Toshkent'),
                             (2, NULL, 'Samarqand');
DELETE FROM foydalanuvchilar WHERE id = 2;
SELECT * FROM profillar;
ROLLBACK;
text
BEGIN
CREATE TABLE
INSERT 0 2
DELETE 1
 foydalanuvchi_id |   telegram   |  shahar
------------------+--------------+----------
                1 | @sizning_nik | Toshkent
(1 row)

ROLLBACK

2-foydalanuvchi o'chganda uning profili ham CASCADE bilan ketdi. DELETE 1 faqat foydalanuvchini sanaydi — kaskad bilan o'chgan bola qatorlar (profil, vazifalar, izohlar) bu songa kirmaydi.

3-mashq (qiyin)

Qo'shimcha maydonli N:M: vazifani boshqa foydalanuvchilar kuzatishi mumkin. vazifa_kuzatuvchilari jadvalini yarating: vazifa_id, foydalanuvchi_id (ikkalasi FK, CASCADE), qoshilgan_vaqt (sukut now()), tarkibiy PK, ikkinchi ustunga indeks. 1-vazifaga 2- va 3-foydalanuvchini, 5-vazifaga 2-foydalanuvchini qo'shing. Har vazifa uchun kuzatuvchilar sonini va ismlarini (vergul bilan, id tartibida) chiqaring. Oxirida (1, 2) juftini qayta qo'shib ko'ring. Hammasi BEGIN … ROLLBACK ichida. Ishora: string_agg(…, ', ' ORDER BY …) — Agregatlar.

Yechim
sql
BEGIN;
CREATE TABLE vazifa_kuzatuvchilari (
  vazifa_id        bigint NOT NULL
    REFERENCES vazifalar ON DELETE CASCADE,
  foydalanuvchi_id bigint NOT NULL
    REFERENCES foydalanuvchilar ON DELETE CASCADE,
  qoshilgan_vaqt   timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (vazifa_id, foydalanuvchi_id)
);
CREATE INDEX vazifa_kuzatuvchilari_foydalanuvchi_id_idx
  ON vazifa_kuzatuvchilari (foydalanuvchi_id);
INSERT INTO vazifa_kuzatuvchilari (vazifa_id, foydalanuvchi_id)
VALUES (1, 2), (1, 3), (5, 2);
SELECT v.id, count(k.foydalanuvchi_id) AS kuzatuvchilar,
       string_agg(f.ism, ', ' ORDER BY f.id) AS kimlar
FROM vazifalar v
JOIN vazifa_kuzatuvchilari k ON k.vazifa_id = v.id
JOIN foydalanuvchilar f ON f.id = k.foydalanuvchi_id
GROUP BY v.id
ORDER BY v.id;
INSERT INTO vazifa_kuzatuvchilari (vazifa_id, foydalanuvchi_id)
VALUES (1, 2);
text
BEGIN
CREATE TABLE
CREATE INDEX
INSERT 0 3
 id | kuzatuvchilar |              kimlar
----+---------------+-----------------------------------
  1 |             2 | Dilnoza Rahimova, Sardor Tursunov
  5 |             1 | Dilnoza Rahimova
(2 rows)

ERROR:  duplicate key value violates unique constraint "vazifa_kuzatuvchilari_pkey"
DETAIL:  Key (vazifa_id, foydalanuvchi_id)=(1, 2) already exists.

Oxirida ROLLBACK; yozing — xatodan keyin tranzaksiya faqat shuni qabul qiladi. qoshilgan_vaqt — juftlikning o'z ma'lumoti: u na vazifaga, na foydalanuvchiga tegishli. Ikkinchi indeks "Dilnoza nimalarni kuzatyapti?" so'rovi uchun. Takroriy juft tarkibiy kalit bilan rad etildi. Indeks nomi 40 belgidan uzun, lekin Postgres nom chegarasi — 63 bayt, sig'adi.

9. SQL qadami

Holat 23 → holat 34. Bazaga izohlar jadvali va namuna izohlar qo'shiladi. Fayllar:

  • sql/007-izohlar.sql — jadval, ikki tashqi kalit, CHECK, ikki indeks («Qoida: tashqi kalit "ko'p" tomonda» bo'limida to'liq matni);
  • sql/008-namuna-izohlar.sql — 15 ta izoh, 11 ta vazifada; muallif — vazifa egasi; vaqtlar aniq ('2026-10-01 20:00+05').

Namuna fayli izohni vazifa matni orqali ulaydi — id qo'lda yozilmaydi (INSERT va RETURNING darsidagi naqsh):

sql
INSERT INTO izohlar (vazifa_id, muallif_id, matn, yaratilgan_vaqt)
SELECT v.id, v.egasi_id, i.matn, i.vaqt::timestamptz
FROM (VALUES
  ('JavaScript kursining 26-qismini boshlash #o''qish', 'Docker darslaridan boshladim',                      '2026-10-01 20:00+05'),
  -- … yana 14 qator …
) AS i(vazifa_matn, matn, vaqt)

Bazani noldan qurib tekshiring (har fayl bitta tranzaksiyada, birinchi xatoda to'xtaydi) — kanon tekshir.sh 34 hamma jadvallarni namunasiz va namuna bilan qurib, exit 0 bergan. Keyin commit:

bash
git add sql/007-izohlar.sql sql/008-namuna-izohlar.sql
git commit -m "db: izohlar (1:N) — vazifa CASCADE, muallif SET NULL (007, 008-namuna)"

Holatdagi ongli qaror: hozir muallif doim vazifa egasi (bir kishilik ilova). muallif_id baribir alohida ustun — jamoaviy vazifalar keyingi qismlarda qo'shilganda sxema o'zgarmaydi.

10. Real ishda

  • Har backend loyihasi. Buyurtma → buyurtma qatorlari (1:N), mahsulot ↔ kategoriya (N:M), foydalanuvchi → sozlamalar (1:1), izoh → javob (self). Bu to'rt shakldan boshqa narsa deyarli yo'q.
  • ORM'lar. Prisma, TypeORM kabi vositalar @relation, @ManyToMany kabi yozuvlarni aynan shu jadvallarga aylantiradi. Ichida nima borligini bilsangiz, ORM xatolarini tez topasiz (27-qismda).
  • Ma'lumotni o'chirish siyosati. "Foydalanuvchi akkauntini o'chirdi — izohlari qoladimi?" — huquqiy savol ham (shaxsiy ma'lumotlar qonuni). CASCADE yoki SET NULL qarori shu yerda.
  • Intervyuda: "N:M ni qanday saqlaysiz?", "1:1 qachon alohida jadval?", "FK qaysi tomonda?" — backend intervyusining birinchi daqiqalaridagi savollar.

Xulosa

  • 1:N — tashqi kalit "ko'p" tomonda; NOT NULL — majburiy ota, NULL — ixtiyoriy.
  • ON DELETE — biznes qarori: CASCADE (bola ma'nosiz), SET NULL (bola qoladi), RESTRICT (o'chirish taqiq).
  • N:M — ko'prik jadval: ikki FK, tarkibiy PK, ikkinchi ustunga indeks.
  • Ko'prik jadval juftlikning o'z ma'lumotini ham saqlay oladi (sana, son, baho).
  • 1:1 — PK = FK (yoki FK + UNIQUE); faqat sababi bo'lsa alohida jadval.
  • Self-relation — oddiy 1:N, faqat bitta jadval ichida.

Keyingi dars: Normalizatsiya — takrorlanadigan ma'lumot qanday anomaliyalar keltirib chiqarishini ko'ramiz va jadvallarni 1NF, 2NF, 3NF ga keltiramiz.

Manbalar

  • PostgreSQL 18 hujjati: "Constraints — Foreign Keys" (ON DELETE, tarkibiy kalitlar) va "CREATE TABLE" (TEMPORARY) — postgresql.org/docs/18, 2026-10-24 da ko'rildi.
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Bog'lanishlar 1:1, 1:N va N:M: diagrammadan jadvallarga — IlmHamroh