Ing sistem ERP kompleks akeh entitas duwe sifat hirarkisnalika obyek homogen baris ing wit relasi leluhur-turunan - iki minangka struktur organisasi perusahaan (kabeh cabang, departemen lan kelompok kerja iki), lan katalog barang, lan wilayah kerja, lan geografi titik penjualan, ...
Nyatane, ora ana
Ana akeh cara kanggo nyimpen wit kasebut ing DBMS, nanging dina iki kita bakal fokus ing siji pilihan:
CREATE TABLE hier(
id
integer
PRIMARY KEY
, pid
integer
REFERENCES hier
, data
json
);
CREATE INDEX ON hier(pid); -- Π½Π΅ Π·Π°Π±ΡΠ²Π°Π΅ΠΌ, ΡΡΠΎ FK Π½Π΅ ΠΏΠΎΠ΄ΡΠ°Π·ΡΠΌΠ΅Π²Π°Π΅Ρ Π°Π²ΡΠΎΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΈΠ½Π΄Π΅ΠΊΡΠ°, Π² ΠΎΡΠ»ΠΈΡΠΈΠ΅ ΠΎΡ PK
Lan nalika sampeyan ndeleng jero hierarki, sampeyan kudu sabar ngenteni kepiye cara "naif" sampeyan nggarap struktur kasebut kanthi efektif.
Ayo goleki masalah khas sing muncul, implementasine ing SQL, lan nyoba nambah kinerja.
#1. Sepira jerone bolongan kelinci?
Ayo kita, kanggo definiteness, nampa yen struktur iki bakal nggambarake subordination departemen ing struktur organisasi: departemen, divisi, sektor, cabang, kelompok kerja ... - apa wae sing diarani.
Pisanan, ayo ngasilake 'wit' saka unsur 10K
INSERT INTO hier
WITH RECURSIVE T AS (
SELECT
1::integer id
, '{1}'::integer[] pids
UNION ALL
SELECT
id + 1
, pids[1:(random() * array_length(pids, 1))::integer] || (id + 1)
FROM
T
WHERE
id < 10000
)
SELECT
pids[array_length(pids, 1)] id
, pids[array_length(pids, 1) - 1] pid
FROM
T;
Ayo diwiwiti kanthi tugas sing paling gampang - nemokake kabeh karyawan sing kerja ing sektor tartamtu, utawa babagan hierarki - golek kabeh anak saka simpul. Iku uga bakal becik kanggo njaluk "ambane" saka turunane ... Kabeh iki bisa uga perlu, contone, kanggo mbangun sawetara jenis
Kabeh bakal dadi apik yen mung ana sawetara tingkat saka turunane lan nomer ing rolas, nanging yen ana luwih saka 5 tingkat, lan wis ana puluhan turunane, bisa uga ana masalah. Ayo goleki carane opsi telusuran mudhun-the-tree tradisional ditulis (lan bisa digunakake). Nanging pisanan, ayo nemtokake simpul sing paling menarik kanggo riset kita.
Paling akeh "jero" subtree:
WITH RECURSIVE T AS (
SELECT
id
, pid
, ARRAY[id] path
FROM
hier
WHERE
pid IS NULL
UNION ALL
SELECT
hier.id
, hier.pid
, T.path || hier.id
FROM
T
JOIN
hier
ON hier.pid = T.id
)
TABLE T ORDER BY array_length(path, 1) DESC;
id | pid | path
---------------------------------------------
7624 | 7623 | {7615,7620,7621,7622,7623,7624}
4995 | 4994 | {4983,4985,4988,4993,4994,4995}
4991 | 4990 | {4983,4985,4988,4989,4990,4991}
...
Paling akeh "lebar" subtree:
...
SELECT
path[1] id
, count(*)
FROM
T
GROUP BY
1
ORDER BY
2 DESC;
id | count
------------
5300 | 30
450 | 28
1239 | 27
1573 | 25
Kanggo pitakonan iki kita nggunakake khas rekursif JOIN:
Temenan, karo model request iki jumlah iterasi bakal cocog karo jumlah turunane (lan ana sawetara rolas mau), lan iki bisa njupuk sumber daya cukup pinunjul, lan, minangka asil, wektu.
Ayo mriksa subtree "paling jembar":
WITH RECURSIVE T AS (
SELECT
id
FROM
hier
WHERE
id = 5300
UNION ALL
SELECT
hier.id
FROM
T
JOIN
hier
ON hier.pid = T.id
)
TABLE T;
Kaya sing dikarepake, kita nemokake kabeh 30 rekaman. Nanging padha nglampahi 60% saka total wektu iki - amarga padha uga nindakake 30 panelusuran ing indeks. Apa bisa ditindakake kurang?
Koreksi massal kanthi indeks
Apa kita kudu nggawe pitakon indeks sing kapisah kanggo saben simpul? Pranyata ora - kita bisa maca saka indeks nggunakake sawetara tombol bebarengan ing siji telpon kanthi pitulung saka = ANY(array)
.
Lan ing saben klompok pengenal kasebut, kita bisa njupuk kabeh ID sing ditemokake ing langkah sadurunge kanthi "simpul". Sing, ing saben langkah sabanjure kita bakal goleki kabeh turunan saka tingkat tartamtu bebarengan.
Mung, iki masalah, ing pilihan rekursif, sampeyan ora bisa ngakses dhewe ing query nested, nanging kita kudu piye wae milih mung apa sing ditemokake ing tingkat sadurunge ... Pranyata ora bisa nggawe query nested kanggo kabeh pilihan, nanging kanggo lapangan tartamtu iku bisa. Lan lapangan iki uga bisa dadi array - sing kudu digunakake ANY
.
Iku muni sethitik edan, nanging ing diagram kabeh iku prasaja.
WITH RECURSIVE T AS (
SELECT
ARRAY[id] id$
FROM
hier
WHERE
id = 5300
UNION ALL
SELECT
ARRAY(
SELECT
id
FROM
hier
WHERE
pid = ANY(T.id$)
) id$
FROM
T
WHERE
coalesce(id$, '{}') <> '{}' -- ΡΡΠ»ΠΎΠ²ΠΈΠ΅ Π²ΡΡ
ΠΎΠ΄Π° ΠΈΠ· ΡΠΈΠΊΠ»Π° - ΠΏΡΡΡΠΎΠΉ ΠΌΠ°ΡΡΠΈΠ²
)
SELECT
unnest(id$) id
FROM
T;
Lan ing kene sing paling penting ora malah menang 1.5 kaping ing wektu, lan kita nyuda buffer kurang, awit kita mung duwe 5 telpon kanggo indeks tinimbang 30!
Bonus tambahan iku kasunyatan sing sawise unnest final, pengenal bakal tetep dhawuh dening "tingkat".
Tandha simpul
Pertimbangan sabanjure sing bakal mbantu ningkatake kinerja yaiku β "godhong" ora bisa duwe anak, yaiku, kanggo wong-wong mau ora perlu katon "mudhun" kabeh. Ing rumusan tugas kita, iki tegese yen kita ngetutake rantai departemen lan tekan karyawan, mula ora perlu goleki maneh ing cabang iki.
Ayo mlebu ing meja kita tambahan boolean
- lapangan, sing bakal langsung ngandhani apa entri tartamtu ing wit kita minangka "simpul" - yaiku, apa bisa duwe keturunan.
ALTER TABLE hier
ADD COLUMN branch boolean;
UPDATE
hier T
SET
branch = TRUE
WHERE
EXISTS(
SELECT
NULL
FROM
hier
WHERE
pid = T.id
LIMIT 1
);
-- ΠΠ°ΠΏΡΠΎΡ ΡΡΠΏΠ΅ΡΠ½ΠΎ Π²ΡΠΏΠΎΠ»Π½Π΅Π½: 3033 ΡΡΡΠΎΠΊ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ Π·Π° 42 ΠΌΡ.
apik tenan! Pranyata mung luwih saka 30% saka kabeh unsur wit duwe turunan.
Saiki ayo nggunakake mekanik sing rada beda - sambungan menyang bagean rekursif liwat LATERAL
, sing bakal ngidini kita langsung ngakses kolom "tabel" rekursif, lan nggunakake fungsi agregat kanthi kondisi nyaring adhedhasar simpul kanggo nyuda set tombol:
WITH RECURSIVE T AS (
SELECT
array_agg(id) id$
, array_agg(id) FILTER(WHERE branch) ns$
FROM
hier
WHERE
id = 5300
UNION ALL
SELECT
X.*
FROM
T
JOIN LATERAL (
SELECT
array_agg(id) id$
, array_agg(id) FILTER(WHERE branch) ns$
FROM
hier
WHERE
pid = ANY(T.ns$)
) X
ON coalesce(T.ns$, '{}') <> '{}'
)
SELECT
unnest(id$) id
FROM
T;
Kita padha bisa nyuda siji telpon indeks liyane lan menang luwih saka 2 kaping ing volume proofread.
#2. Ayo bali menyang oyod
Algoritma iki bakal migunani yen sampeyan kudu ngumpulake cathetan kanggo kabeh unsur "munggah wit", nalika nahan informasi babagan lembar sumber (lan karo indikator apa) sing nyebabake dilebokake ing sampel - contone, kanggo nggawe laporan ringkesan. kanthi agregasi menyang node.
Apa ing ngisor iki kudu dijupuk mung minangka bukti-konsep, amarga panjaluk kasebut dadi rumit banget. Nanging yen dominasi database sampeyan, sampeyan kudu mikir babagan nggunakake teknik sing padha.
Ayo dadi miwiti karo sawetara statements prasaja:
- Rekaman sing padha saka database Luwih becik maca sepisan wae.
- Cathetan saka database Iku luwih efisien kanggo maca ing kumpulantinimbang piyambak.
Saiki ayo nyoba mbangun panjaluk sing dibutuhake.
langkah 1
Temenan, nalika miwiti rekursi (ing ngendi kita bakal tanpa iku!) Kita kudu nyuda cathetan saka godhong kasebut dhewe adhedhasar set pengenal awal:
WITH RECURSIVE tree AS (
SELECT
rec -- ΡΡΠΎ ΡΠ΅Π»ΡΠ½Π°Ρ Π·Π°ΠΏΠΈΡΡ ΡΠ°Π±Π»ΠΈΡΡ
, id::text chld -- ΡΡΠΎ "Π½Π°Π±ΠΎΡ" ΠΏΡΠΈΠ²Π΅Π΄ΡΠΈΡ
ΡΡΠ΄Π° ΠΈΡΡ
ΠΎΠ΄Π½ΡΡ
Π»ΠΈΡΡΡΠ΅Π²
FROM
hier rec
WHERE
id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
UNION ALL
...
Yen ketoke aneh kanggo wong sing "set" disimpen minangka senar lan ora Uploaded, banjur ana panjelasan prasaja kanggo iki. Ana fungsi "gluing" agregasi sing dibangun kanggo senar string_agg
, nanging ora kanggo susunan. Senajan dheweke
langkah 2
Saiki kita bakal entuk set ID bagean sing kudu diwaca maneh. Meh mesthi padha bakal diduplikasi ing macem-macem cathetan saka pesawat asli - supaya kita bakal klompok mau, nalika njaga informasi babagan godhong sumber.
Nanging ing kene ana telung masalah sing nunggu kita:
- Bagian "subrekursif" saka pitakonan ora bisa ngemot fungsi agregat karo
GROUP BY
. - A referensi kanggo "tabel" rekursif ora bisa ing subquery nested.
- Panjaluk ing bagean rekursif ora bisa ngemot CTE.
Begjanipun, kabeh masalah iki cukup gampang kanggo ngatasi. Ayo miwiti saka pungkasan.
CTE ing bagean rekursif
Kene mangkono ora nyambut gawe:
WITH RECURSIVE tree AS (
...
UNION ALL
WITH T (...)
SELECT ...
)
Lan supaya bisa, kurung nggawe prabΓ©dan!
WITH RECURSIVE tree AS (
...
UNION ALL
(
WITH T (...)
SELECT ...
)
)
Pitakon bersarang marang "tabel" rekursif
Hmm... CTE rekursif ora bisa diakses ing subquery. Nanging bisa uga ana ing CTE! Lan panjalukan nested wis bisa ngakses CTE iki!
GROUP BY nang recursion
Iku ora nyenengake, nanging ... Kita duwe cara prasaja kanggo niru GROUP BY nggunakake DISTINCT ON
lan fungsi jendhela!
SELECT
(rec).pid id
, string_agg(chld::text, ',') chld
FROM
tree
WHERE
(rec).pid IS NOT NULL
GROUP BY 1 -- Π½Π΅ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ!
Lan iki cara kerjane!
SELECT DISTINCT ON((rec).pid)
(rec).pid id
, string_agg(chld::text, ',') OVER(PARTITION BY (rec).pid) chld
FROM
tree
WHERE
(rec).pid IS NOT NULL
Saiki kita ngerti sebabe ID numerik diowahi dadi teks - supaya bisa digabungake kanthi dipisahake karo koma!
langkah 3
Kanggo final, kita ora duwe apa-apa:
- kita maca cathetan "bagean" adhedhasar pesawat saka ID klompok
- kita mbandhingake bagean sing dikurangi karo "set" saka lembaran asli
- "expand" set-string nggunakake
unnest(string_to_array(chld, ',')::integer[])
WITH RECURSIVE tree AS (
SELECT
rec
, id::text chld
FROM
hier rec
WHERE
id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
UNION ALL
(
WITH prnt AS (
SELECT DISTINCT ON((rec).pid)
(rec).pid id
, string_agg(chld::text, ',') OVER(PARTITION BY (rec).pid) chld
FROM
tree
WHERE
(rec).pid IS NOT NULL
)
, nodes AS (
SELECT
rec
FROM
hier rec
WHERE
id = ANY(ARRAY(
SELECT
id
FROM
prnt
))
)
SELECT
nodes.rec
, prnt.chld
FROM
prnt
JOIN
nodes
ON (nodes.rec).id = prnt.id
)
)
SELECT
unnest(string_to_array(chld, ',')::integer[]) leaf
, (rec).*
FROM
tree;