IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL27/64-dars17 daqiqa
Mundarija (29)

SQL rekursiv CTE: WITH RECURSIVE bilan daraxt va graflarni aylanib chiqish

Qisqacha: Rekursiv CTE (WITH RECURSIVE) — o'zini o'zi chaqiradigan CTE. U ikki qismdan iborat: anchor (boshlang'ich qatorlar, masalan daraxt ildizi) va UNION ALL dan keyingi rekursiv qism (oldingi qadamda topilganlarning "farzandlari"). Postgres yangi qator chiqmay qolguncha qadamlarni takrorlaydi. Shu bilan kichik vazifalar, kategoriyalar, izohlar zanjiri kabi har qanday chuqurlikdagi daraxt bitta so'rovda o'qiladi. Siklli ma'lumotda cheksiz aylanishdan CYCLE bandi (PG 14+) yoki chuqurlik chegarasi himoya qiladi.

Bu darsda

  • WITH RECURSIVE ning ikki qismini va qadamma-qadam bajarilishini tushuntirasiz.
  • Daraxtni yuqoridan pastga chuqurlik, yo'l massivi va chekinish bilan chiqarasiz.
  • Pastdan yuqoriga — vazifaning hamma "ota-bobolari"ni topasiz.
  • CYCLE, SEARCH bandlari va chuqurlik chegarasi bilan cheksiz sikldan himoyalanasiz.
  • Vazifa o'chirilganda u bilan birga nechta kichik vazifa ketishini hisoblaysiz.

Oldin bilishingiz kerak: CTE (WITH), JOIN'lar (self-join), Rekursiya asoslari.

1. Nega bu kerak?

vazifalar jadvalida ota_id ustuni bor: kichik vazifa o'z otasini ko'rsatadi. Madinaning kurs ishi shunday ko'rinadi:

flowchart TD
  A["7: Kurs ishi"] --> B["41: Jadvallarni loyihalash"]
  A --> C["42: Adabiyotlar ro'yxati"]
  B --> D["46: ER diagramma chizish"]
  B --> E["47: Normalizatsiyani tekshirish"]

Diagrammada uch daraja bor: kurs ishi, uning qadamlari va qadamning qadamlari. JOIN'lar darsidagi self-join bitta darajani ko'rardi: "46 ning otasi 41". 7 ning hamma avlodlarini topish uchun self-join'ni necha marta yozish kerak? Ikki marta — bugun. Ertaga Madina to'rtinchi daraja qo'shsa — uch marta. Chuqurlik oldindan noma'lum, so'rov esa qat'iy.

Dasturlashda bu masala tanish: Rekursiya asoslari darsida funksiya o'zini chaqirib, daraxtsimon ma'lumotni aylanib chiqqan edi. SQL'da xuddi shu g'oya — rekursiv CTE. O'xshatish: oila shajarasi. "Bobomning hamma avlodlari" ni topish uchun avval bobomning farzandlarini yozasiz, keyin ularning farzandlarini, keyin ularnikini — yangi ism chiqmay qolguncha.

2. Tuzilma va bajarilish

2.1 Ikki qism

sql
WITH RECURSIVE daraxt AS (
  SELECT id, matn, ota_id, 1 AS chuqurlik
  FROM vazifalar WHERE id = 7
  UNION ALL
  SELECT v.id, v.matn, v.ota_id, d.chuqurlik + 1
  FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
)
SELECT id, matn, chuqurlik FROM daraxt ORDER BY chuqurlik, id;
text
 id |                     matn                      | chuqurlik
----+-----------------------------------------------+-----------
  7 | Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish |         1
 41 | Jadvallarni loyihalash                        |         2
 42 | Adabiyotlar ro'yxati                          |         2
 46 | ER diagramma chizish                          |         3
 47 | Normalizatsiyani tekshirish                   |         3
(5 rows)

Biz ishga tushirdik (PostgreSQL 17.6, vazifalar holat 23). Qismlar:

Qism Vazifasi
WITH RECURSIVE daraxt CTE o'ziga murojaat qila oladi
1-SELECT (anchor) boshlang'ich qator: 7-vazifa
UNION ALL qadamlar natijasini qo'shib boradi
2-SELECT (rekursiv qism) daraxt dagi qatorlarning farzandlari

Anchor (langar) — rekursiya boshlanadigan nuqta, o'ziga murojaat qilmaydi. Rekursiv qism daraxt ni o'zi ishlatadi: JOIN daraxt d ON v.ota_id = d.id — "otasi daraxtda bo'lgan vazifalar". chuqurlik + 1 — har qadamda bir daraja pastga.

2.2 Qadamma-qadam

Nom "rekursiv" bo'lsa ham, Postgres buni aslida sikl bilan bajaradi. U ishchi jadval (working table) yuritadi — oxirgi qadamda topilgan qatorlar:

Qadam Ishchi jadval (yangi topilganlar) Natijada jami
1 (anchor) 7 1
2 41, 42 (otasi 7) 3
3 46, 47 (otasi 41 yoki 42) 5
4 — (otasi 46 yoki 47 bo'lgan yo'q) to'xtadi

Muhim nuqta: rekursiv qism har qadamda faqat oldingi qadam topganlarini ko'radi, butun natijani emas. 3-qadamda daraxt = {41, 42}, 7 emas. Shuning uchun 41 va 42 qayta topilmaydi. Ishchi jadval bo'sh qolganda sikl tugaydi.

flowchart LR
  A["anchor:<br/>7"] --> W1["1-qadam:<br/>41, 42"]
  W1 --> W2["2-qadam:<br/>46, 47"]
  W2 --> W3["3-qadam:<br/>bo'sh"]
  W3 --> R["natija:<br/>5 qator"]

Diagramma — sikl iteratsiyalari: har quti oldingisidan "farzandlar" ni oladi. Bo'sh quti — to'xtash sharti.

2.3 Har qadam — bitta JOIN

Har iteratsiyada Postgres bitta oddiy JOIN bajaradi: "ishchi jadvaldagi id'lar — vazifalar.ota_id da". Demak ota_id bo'yicha qidiruv tez bo'lishi kerak. ALTER TABLE darsida vazifalar_ota_id_idx indeksini aynan shuning uchun qo'shgan edik. U faqat ota_id IS NOT NULL qatorlarni saqlaydi (partial indeks): ko'p vazifaning otasi yo'q, ularni indeksga yozish behuda.

Daraxt chuqurligi 3 bo'lsa — 3–4 ta JOIN, 10 bo'lsa — 10–11 ta. Qatorlar soni esa faqat shu daraxtdagi tugunlar soniga bog'liq, butun jadvalga emas. 2 mln vazifali bazada ham bitta loyihaning daraxti millisekundlarda o'qiladi — indeks bor bo'lsa.

Tekshirib ko'ring: anchor'da WHERE id = 7 o'rniga WHERE id = 41 yozsak, natijada nechta qator bo'ladi?

Javob

3 ta: 41 (anchor), keyin uning farzandlari 46 va 47. Ularning farzandi yo'q — to'xtaydi. Rekursiya faqat pastga yuradi, 41 ning otasi (7) va "akasi" (42) chiqmaydi.

3. Butun daraxt: chuqurlik, yo'l va chekinish

3.1 Bir nechta ildiz

Anchor bitta qator bo'lishi shart emas. Madinaning hamma yuqori darajadagi vazifalari (otasi yo'qlar) — ildizlar. Kanon tajribasi (PostgreSQL 18.6):

sql
WITH RECURSIVE daraxt AS (
  SELECT id, matn, ota_id, 1 AS chuqurlik,
         ARRAY[id] AS yol
  FROM vazifalar WHERE ota_id IS NULL AND egasi_id = 4
  UNION ALL
  SELECT v.id, v.matn, v.ota_id, d.chuqurlik + 1,
         d.yol || v.id
  FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
)
SELECT repeat('  ', chuqurlik - 1) || matn AS daraxt,
       chuqurlik, yol
FROM daraxt ORDER BY yol;
text
                    daraxt                     | chuqurlik |    yol
-----------------------------------------------+-----------+-----------
 Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish |         1 | {7}
   Jadvallarni loyihalash                      |         2 | {7,41}
     ER diagramma chizish                      |         3 | {7,41,46}
     Normalizatsiyani tekshirish               |         3 | {7,41,47}
   Adabiyotlar ro'yxati                        |         2 | {7,42}
 Kutubxonaga kitob qaytarish #o'qish           |         1 | {13}
 Yotoqxona to'lovi #to'lov                     |         1 | {18}
 Python laboratoriya ishi #o'qish              |         1 | {31}
 Uyga qo'ng'iroq qilish #oila                  |         1 | {37}
(9 rows)

Kanon so'rovida yana yol_matni ustuni bor edi (ota matnlari zanjiri) — bu yerda qisqartirish uchun olib tashladik, natija o'zgarmaydi. Uchta yangi narsa:

  • yol (yo'l) — ildizdan shu qatorgacha bo'lgan id'lar massivi. Anchor'da ARRAY[id], har qadamda d.yol || v.id — massiv oxiriga qo'shish.
  • ORDER BY yol — massivlar element-element solishtiriladi: {7} < {7,41} < {7,41,46} < {7,42}. Natija — har ota darhol farzandlari bilan, xuddi papkalar daraxtidek.
  • repeat(' ', chuqurlik - 1) — chuqurlikka qarab bo'sh joy qo'yadi: ekranda chekinish (indentatsiya) bilan daraxt.

3.2 SEARCH bandi (PG 14+)

Yo'l massivini qo'lda yasash o'rniga, SQL standartidagi SEARCH bandi tartib ustunini o'zi yaratadi. Ikki rejim bor: DEPTH FIRST (avval chuqurga — yuqoridagi ORDER BY yol kabi) va BREADTH FIRST (avval kengga — daraja-daraja). Biz sinadik (PostgreSQL 17.6):

sql
WITH RECURSIVE daraxt AS (
  SELECT id, matn, ota_id FROM vazifalar WHERE id = 7
  UNION ALL
  SELECT v.id, v.matn, v.ota_id
  FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
) SEARCH DEPTH FIRST BY id SET tartib
SELECT id, matn, tartib FROM daraxt ORDER BY tartib;
text
 id |                     matn                      |     tartib
----+-----------------------------------------------+-----------------
  7 | Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish | {(7)}
 41 | Jadvallarni loyihalash                        | {(7),(41)}
 46 | ER diagramma chizish                          | {(7),(41),(46)}
 47 | Normalizatsiyani tekshirish                   | {(7),(41),(47)}
 42 | Adabiyotlar ro'yxati                          | {(7),(42)}
(5 rows)

tartib ustunini Postgres o'zi qo'shdi — u bizning yol ning o'zi (har element qavsda, chunki BY dan keyin bir nechta ustun bo'lishi mumkin). BREADTH FIRST bilan tartib 7, 41, 42, 46, 47 bo'ladi — "Qadamma-qadam" jadvalidagi kabi.

4. Pastdan yuqoriga

Teskari savol: "ER diagramma chizish" qaysi katta ishning qismi? Endi anchor — barg (pastdagi tugun), rekursiv qism esa otasini topadi (v.id = y.ota_id):

sql
WITH RECURSIVE yuqoriga AS (
  SELECT id, matn, ota_id FROM vazifalar
  WHERE matn = 'ER diagramma chizish'
  UNION ALL
  SELECT v.id, v.matn, v.ota_id
  FROM vazifalar v JOIN yuqoriga y ON v.id = y.ota_id
)
SELECT * FROM yuqoriga;
text
 id |                     matn                      | ota_id
----+-----------------------------------------------+--------
 46 | ER diagramma chizish                          |     41
 41 | Jadvallarni loyihalash                        |      7
  7 | Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish |
(3 rows)

Uch qadam: 46 → otasi 41 → otasi 7 → 7 ning otasi NULL, JOIN hech narsa topmadi — to'xtadi. Yuqoriga yo'l doim bitta zanjir (har vazifaning bitta otasi), pastga esa tarmoqlanadi. Bu so'rov ilovada "non" yo'li (breadcrumbs) uchun kerak: "Kurs ishi › Jadvallarni loyihalash › ER diagramma chizish".

5. Rekursiv natija bilan hisob

Har yuqori darajadagi vazifaning hamma darajadagi kichik vazifalari soni va ulardan nechtasi bajarilgan. Asosiy hiyla: rekursiv CTE ildiz ni (qaysi daraxtdan kelganini) har qadamda uzatib boradi:

sql
WITH RECURSIVE avlod AS (
  SELECT id AS ildiz, id FROM vazifalar WHERE ota_id IS NULL
  UNION ALL
  SELECT a.ildiz, v.id
  FROM vazifalar v JOIN avlod a ON v.ota_id = a.id
)
SELECT r.matn, count(*) - 1 AS kichiklari,
       count(*) FILTER (WHERE v.bajarildi AND v.id <> r.id)
         AS bajarilgan
FROM avlod a JOIN vazifalar v ON v.id = a.id
JOIN vazifalar r ON r.id = a.ildiz
GROUP BY r.id, r.matn HAVING count(*) > 1 ORDER BY r.id;
text
                     matn                      | kichiklari | bajarilgan
-----------------------------------------------+------------+------------
 Kurs ishi: ma'lumotlar bazasi bo'limi #o'qish |          4 |          1
 Portfolio saytiga blog yozish #portfolio      |          3 |          1
(2 rows)

Kanon tajribasi. count(*) - 1 — ildizning o'zini ayirib tashlaymiz. HAVING count(*) > 1 — kichik vazifasi yo'q ildizlar chiqmasin. Ilovada bu "4 tadan 1 tasi bajarildi" ko'rsatkichi bo'ladi.

Tekshirib ko'ring: nega rekursiv qismda a.ildiz bor, v.id emas?

Javob

ildiz — qaysi daraxtga tegishlilik belgisi. U anchor'da bir marta o'rnatiladi (id AS ildiz) va pastga o'zgarmasdan uzatiladi. Agar v.id yozsak, har qadamda yangi qator o'zini ildiz deb hisoblaydi va guruhlash buziladi.

6. Sikl va cheksiz rekursiya

6.1 Siklli ma'lumot

Bizning daraxtda sikl yo'q: vazifalar_ota_id_check (ota_id <> id) o'ziga ota bo'lishni taqiqlaydi. Lekin 1 → 2 → 1 kabi uzunroq sikl baza darajasida taqiqlanmagan. Graflarda (do'stlar tarmog'i, yo'llar xaritasi) sikl esa odatiy hol. Siklli ma'lumotda rekursiya hech qachon to'xtamaydi.

Kanon tajribasida vaqtinchalik t27_graf jadvali: 1 → 2 → 3 → 1. Sukut himoyasiz yozilsa — cheksiz. Birinchi himoya — chuqurlik chegarasi:

sql
CREATE TEMP TABLE t27_graf (id int, keyingi int);
INSERT INTO t27_graf VALUES (1, 2), (2, 3), (3, 1);
WITH RECURSIVE yur AS (
  SELECT id, keyingi, 1 AS n FROM t27_graf WHERE id = 1
  UNION ALL
  SELECT g.id, g.keyingi, y.n + 1
  FROM t27_graf g JOIN yur y ON g.id = y.keyingi
  WHERE y.n < 7
)
SELECT * FROM yur;
text
CREATE TABLE
INSERT 0 3
 id | keyingi | n
----+---------+---
  1 |       2 | 1
  2 |       3 | 2
  3 |       1 | 3
  1 |       2 | 4
  2 |       3 | 5
  3 |       1 | 6
  1 |       2 | 7
(7 rows)

WHERE y.n < 7 — 7-qadamda to'xtash. Sikl ko'rinib turibdi: 1, 2, 3, 1, 2, 3... Chegara sikldan himoya qiladi, lekin uni "aniqlamaydi" — 7 qadam chuqur daraxt ham kesiladi.

6.2 CYCLE bandi (PG 14+)

CYCLE bandi siklni o'zi aniqlaydi: yo'lni yuritadi va qator yo'lda allaqachon bo'lsa, uni belgilab, davom ettirmaydi:

sql
CREATE TEMP TABLE t27_graf (id int, keyingi int);
INSERT INTO t27_graf VALUES (1, 2), (2, 3), (3, 1);
WITH RECURSIVE yur AS (
  SELECT id, keyingi FROM t27_graf WHERE id = 1
  UNION ALL
  SELECT g.id, g.keyingi
  FROM t27_graf g JOIN yur y ON g.id = y.keyingi
) CYCLE id SET siklmi USING yol
SELECT * FROM yur;
text
CREATE TABLE
INSERT 0 3
 id | keyingi | siklmi |        yol
----+---------+--------+-------------------
  1 |       2 | f      | {(1)}
  2 |       3 | f      | {(1),(2)}
  3 |       1 | f      | {(1),(2),(3)}
  1 |       2 | t      | {(1),(2),(3),(1)}
(4 rows)

CYCLE id SET siklmi USING yol — "id takrorlansa, siklmi ni rost qil, yo'lni yol ustunida saqla". To'rtinchi qatorda 1 qayta keldi — siklmi = t va rekursiya shu tarmoqda to'xtadi. Kanon tajribasi (PostgreSQL 18.6).

PG 14 dan oldin buni qo'lda qilishardi: yol massivini yuritib, rekursiv qismda WHERE NOT g.id = ANY(y.yol) sharti bilan.

6.3 UNION va UNION ALL

UNION ALL o'rniga UNION yozilsa, Postgres takror qatorlarni tashlaydi — oldin chiqqan aynan bir xil qator qayta qo'shilmaydi va sikl to'xtaydi. Lekin bu faqat qatordagi hamma ustunlar takrorlanganda ishlaydi. n yoki yol kabi o'zgaruvchi ustun bo'lsa, qatorlar hech qachon bir xil bo'lmaydi va UNION himoya qilmaydi. Ishonchli yo'l — CYCLE yoki chegara.

Tekshirib ko'ring: vazifalar da hozir sikl yo'q. Nega baribir ilovada daraxt so'roviga chuqurlik chegarasi (masalan WHERE d.chuqurlik < 20) qo'yish foydali?

Javob

Ikki sabab. Birinchisi: ota_id <> id faqat "o'ziga ota" ni taqiqlaydi; 41 → 46 → 41 kabi uzun sikl xato UPDATE bilan paydo bo'lishi mumkin. Ikkinchisi: juda chuqur daraxt (masalan kimdir skript bilan 100 000 darajali zanjir yaratsa) serverni band qiladi. Chegara — arzon sug'urta. Haqiqiy himoya esa sikl paydo bo'lishiga bazada yo'l qo'ymaslik, masalan trigger bilan. Trigger yozishni Denormalizatsiya va hisoblangan maydonlar darsida o'rganamiz.

Diqqat: himoyasiz rekursiv so'rov serverni to'xtovsiz band qiladi va xotirani to'ldiradi. Ilovada so'rovlarga vaqt chegarasi qo'ying (SET statement_timeout = '5s') — u haqda Connection pooling va Deadlock va serialization failure bilan ishlash darslarida.

7. Rekursiya — faqat daraxt uchun emas

Rekursiv CTE ketma-ketlik ham yasay oladi. 1-oktyabrdan 5-oktyabrgacha sanalar:

sql
WITH RECURSIVE kunlar AS (
  SELECT '2026-10-01'::date AS kun
  UNION ALL
  SELECT kun + 1 FROM kunlar WHERE kun < '2026-10-05'
)
SELECT * FROM kunlar;
text
    kun
------------
 2026-10-01
 2026-10-02
 2026-10-03
 2026-10-04
 2026-10-05
(5 rows)

Bu kanon tajribasi. Postgres'da buning uchun tayyor funksiya bor — generate_series (Tahliliy SQL naqshlari darsida). Rekursiv variant boshqa bazalarda yoki murakkab qoidali ketma-ketlikda (masalan "faqat ish kunlari") kerak bo'ladi.

8. Ko'p uchraydigan xatolar

8.1 RECURSIVE so'zi unutildi

sql
WITH daraxt AS (
  SELECT id FROM vazifalar WHERE id = 7
  UNION ALL
  SELECT v.id FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
)
SELECT * FROM daraxt;
text
ERROR:  relation "daraxt" does not exist
LINE 4:   SELECT v.id FROM vazifalar v JOIN daraxt d ON v.ota_id = d...
                                            ^
DETAIL:  There is a WITH item named "daraxt", but it cannot be referenced from this part of the query.
HINT:  Use WITH RECURSIVE, or re-order the WITH items to remove forward references.

Tarjimasi: "daraxt munosabati yo'q. Tafsilot: shu nomli WITH elementi bor, lekin so'rovning bu qismidan unga murojaat qilib bo'lmaydi. Maslahat: WITH RECURSIVE ishlating". Oddiy WITH o'ziga murojaatni bilmaydi. RECURSIVE bitta WITH ga bir marta yoziladi, hatto CTE'lardan faqat bittasi rekursiv bo'lsa ham.

8.2 Turlar mos emas

sql
WITH RECURSIVE sonlar AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 0.5 FROM sonlar WHERE n < 3
)
SELECT * FROM sonlar;
text
ERROR:  recursive query "sonlar" column 1 has type integer in non-recursive term but type numeric overall
LINE 2:   SELECT 1 AS n
                 ^
HINT:  Cast the output of the non-recursive term to the correct type.

Tarjimasi: "rekursiv so'rovning 1-ustuni anchor'da integer, umumiy natijada esa numeric. Maslahat: anchor chiqishini kerakli turga o'giring". Anchor 1 (butun), rekursiv qism n + 0.5 (kasr). Yechim: SELECT 1::numeric AS n. Matnda ham shunday: varchar(100) va text aralashsa — ::text bilan tenglang.

8.3 So'rov tugamayapti

Sikl bor yoki to'xtash sharti yo'q. CYCLE yoki chuqurlik chegarasi qo'shing, psql'da Ctrl+C bilan bekor qiling.

8.4 Daraxt tartibsiz chiqdi

Rekursiv CTE natijasi tartibi kafolatlanmagan. Daraxt ko'rinishi uchun ORDER BY yol yoki SEARCH DEPTH FIRST.

9. Mashqlar

Mashq fayllari kurs/mashqlar/26/27-rekursiv/ da. Hammasi vazifalar holat 23 da.

1-mashq (oson): 1 dan 10 gacha

Rekursiv CTE bilan 1 dan 10 gacha sonlarni yasang va ularning yig'indisi hamda sonini chiqaring. Yordam: anchor SELECT 1 AS n, rekursiv qismda WHERE n < 10.

Yechim
sql
WITH RECURSIVE sonlar AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM sonlar WHERE n < 10
)
SELECT sum(n) AS yigindi, count(*) AS soni FROM sonlar;
text
 yigindi | soni
---------+------
      55 |   10
(1 row)

To'xtash sharti WHERE n < 10: n = 10 bo'lgan qatordan keyin yangi qator yaratilmaydi. 1 + 2 + … + 10 = 55.

2-mashq (o'rta): Azizning portfolio ishi

Azizning "Portfolio saytiga blog yozish #portfolio" vazifasini (id 17) va uning hamma kichik vazifalarini chuqurlik bilan chiqaring: id, matn, chuqurlik; chuqurlik, keyin id bo'yicha. Yordam: «Ikki qism» bo'limidagi so'rov, anchor'da id = 17.

Yechim
sql
WITH RECURSIVE daraxt AS (
  SELECT id, matn, 1 AS chuqurlik FROM vazifalar WHERE id = 17
  UNION ALL
  SELECT v.id, v.matn, d.chuqurlik + 1
  FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
)
SELECT id, matn, chuqurlik FROM daraxt ORDER BY chuqurlik, id;
text
 id |                   matn                   | chuqurlik
----+------------------------------------------+-----------
 17 | Portfolio saytiga blog yozish #portfolio |         1
 43 | Blog uchun mavzu tanlash                 |         2
 44 | Qoralama yozish                          |         2
 45 | Rasmlarni siqish                         |         2
(4 rows)

Bu daraxt bir darajali (3 ta kichik vazifa, ularning farzandi yo'q). Rekursiya 3-qadamda bo'sh ishchi jadval bilan to'xtadi.

3-mashq (qiyin): barglardan ildizgacha yo'l

Har barg (kichik vazifa, o'zining farzandi yo'q) uchun ildizdan shu bargacha id'lar yo'lini 7/41/46 ko'rinishida chiqaring: barg, yol. Yordam: anchor — barglar (ota_id IS NOT NULL va NOT EXISTS farzand), rekursiv qism yuqoriga yuradi va yo'l boshiga ota id sini qo'shadi (o.id || '/' || y.yol); ildizga yetganda (ota_id IS NULL) qatorni oling.

Yechim
sql
WITH RECURSIVE yuqoriga AS (
  SELECT k.id AS barg, k.ota_id, k.id::text AS yol
  FROM vazifalar k
  WHERE k.ota_id IS NOT NULL
    AND NOT EXISTS (SELECT 1 FROM vazifalar b WHERE b.ota_id = k.id)
  UNION ALL
  SELECT y.barg, o.ota_id, o.id || '/' || y.yol
  FROM yuqoriga y JOIN vazifalar o ON o.id = y.ota_id
)
SELECT barg, yol FROM yuqoriga
WHERE ota_id IS NULL ORDER BY barg;
text
 barg |   yol
------+---------
   42 | 7/42
   43 | 17/43
   44 | 17/44
   45 | 17/45
   46 | 7/41/46
   47 | 7/41/47
(6 rows)

Har qadamda ota_id bir daraja yuqoriga suriladi. Ildizga yetganda ota_id NULL bo'ladi — oxirgi WHERE faqat shu "to'liq" yo'llarni oladi, oraliq qadamlarni tashlaydi. k.id::text anchor'da turini text qiladi — «Turlar mos emas» xatosiga yo'l qo'ymaydi. Bunday yo'l satri materialized path naqshining asosi (Modellash naqshlari).

4-mashq: SQL qadami — o'chirishdan oldin ogohlantirish

25-qism API'sida vazifa o'chirilganda kichik vazifalar ham o'chadi (vazifalar_ota_id_fkey ... ON DELETE CASCADE). Ilova foydalanuvchiga oldindan aytishi kerak: "Bu vazifa bilan 4 ta kichik vazifa ham o'chadi". 7-vazifa uchun shu sonni va o'chadigan id'larni chiqaring. Sxema o'zgarmaydi — commit yo'q. Yordam: rekursiv CTE, keyin count(*) - 1 va array_agg(id ORDER BY id).

Yechim
sql
WITH RECURSIVE daraxt AS (
  SELECT id FROM vazifalar WHERE id = 7
  UNION ALL
  SELECT v.id FROM vazifalar v JOIN daraxt d ON v.ota_id = d.id
)
SELECT count(*) - 1 AS kichiklari,
       array_agg(id ORDER BY id) AS idlar
FROM daraxt;
text
 kichiklari |      idlar
------------+-----------------
          4 | {7,41,42,46,47}
(1 row)

ON DELETE CASCADE ham ichkarida xuddi shu daraxtni yuradi: 7 o'chsa — 41, 42; 41 o'chsa — 46, 47. Endi siz buni o'chirishdan oldin ko'rsata olasiz.

10. Real ishda

  • Daraxtlar hamma joyda: do'kon kategoriyalari (Elektronika › Telefonlar › Smartfonlar), izohlarga javoblar, kompaniya tuzilmasi (kim kimga bo'ysunadi), fayl papkalari, menyu.
  • Graflar: "do'stlarning do'stlari", marshrutlar, bog'liqliklar (qaysi paket qaysiga bog'liq) — CYCLE bilan.
  • Muqobil modellar: juda chuqur va ko'p o'qiladigan daraxt uchun materialized path (ltree kengaytmasi) yoki closure table — Modellash naqshlari darsida. Bizning ota_id (adjacency list) — eng sodda va yozish uchun eng qulay.
  • ORM'lar (masalan Prisma) rekursiv CTE'ni to'g'ridan qo'llab-quvvatlamaydi — bunday so'rov xom SQL bilan yoziladi (27-qism). Kysely kabi query builder'larda esa withRecursive bor.
  • Intervyu: "xodimlar jadvalida har xodimning hamma boshliqlarini toping" — klassik rekursiv CTE masalasi.

Xulosa

  • WITH RECURSIVE: anchor (boshlanish) + UNION ALL + rekursiv qism (o'ziga JOIN).
  • Postgres ishchi jadval bilan sikl qiladi: har qadam faqat oldingi qadam topganlarini ko'radi, bo'sh qolsa — to'xtaydi.
  • Chuqurlik — n + 1, yo'l — ARRAY[id] va yol || id; daraxt tartibi — ORDER BY yol yoki SEARCH DEPTH FIRST.
  • Pastga yurish — v.ota_id = d.id, yuqoriga — v.id = y.ota_id.
  • Siklli ma'lumotda — CYCLE (PG 14+) yoki chuqurlik chegarasi; anchor va rekursiv qism turlari bir xil bo'lsin.

Keyingi dars: Window funksiyalar 1: ranking — qatorlarni guruhlamasdan, har biriga guruh ichidagi o'rnini beramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
SQL rekursiv CTE: WITH RECURSIVE bilan daraxt va graflarni aylanib chiqish — IlmHamroh