Herë pas here shfaqet detyra e gjetjes së të dhënave të lidhura sipas një grupi çelësash, deri sa të arrijmë numrin total të nevojshëm të regjistrimeve.
Shembulli mĂ« "jetĂ«sor" â tĂ« shfaqim 20 problemet mĂ« tĂ« vjetra, tĂ« regjistruara nĂ« listĂ«n e punonjĂ«sve (p.sh., brenda njĂ« nĂ«shtrimi). PĂ«r dashboard-et e ndryshme menaxheriale me pĂ«rmbledhje tĂ« shkurtĂ«r mbi seksionet e punĂ«s, njĂ« temĂ« e tillĂ« kĂ«rkohet mjaft shpesh.

Në këtë artikull do të shqyrtojmë implementimin në PostgreSQL të një "versioni naive" të zgjidhjes së tillë, një algoritëm "më të zgjuar" dhe një që është krejtësisht i komplikuar "ciklit" në SQL me kushte daljeje nga të dhënat e gjetura, i cili mund të jetë i dobishëm si për zhvillim të përgjithshëm, ashtu edhe për aplikim në raste të ngjashme.
Të marrim një grup testues të dhënash nga . Që regjistrimet e shfaqura të mos "kallzojnë" nga një herë në tjetrën kur vlerat e renditjes bien ndesh, do ta zgjerojmë indeksin tematik duke shtuar çelësin primar. Kështu, menjëherë do t'i japë atij unik për mbajtjen e rendit, dhe na garanton pjesën e qartë të renditjes:
Krijo indeks mbi task(owner_id, task_date, id);
-- dhe të vjetër - do ta heqim
Rrëzo indeksin task_owner_id_task_date_idx;Ashtu si dëgjohet, ashtu shkruhet
Fillimisht, do të hartojmë variantin më të thjeshtë të kërkesës, duke kaluar ID e ekzekutorëve :
Zgjidh
*
Nga
task
Ku
owner_id = ANY('{1,2,4,8,16,32,64,128,256,512}'::integer[])
Rendit sipas
task_date, id
Kufizo 20; 
ĂshtĂ« pak e trishtueshme â ne kĂ«rkuam vetĂ«m 20 regjistra, por Index Scan na ktheu 960 rreshta, tĂ« cilĂ«t pastaj duhet t'i ndajmĂ«... Le ta provojmĂ« tĂ« lexojmĂ« mĂ« pak.
unnest + ARRAY
Propozimi i parĂ« qĂ« do na ndihmojĂ« â nĂ«se na nevojiten vetĂ«m 20 regjistra tĂ« renditur , mjafton tĂ« lexojmĂ« maksimalisht 20 tĂ« renditur nĂ« tĂ« njĂ«jtin rend sipas çdo çelĂ«si. FatmirĂ«sisht, indeksi pĂ«rkatĂ«s (owner_id, task_date, id) e kemi.
Do të përdorim të njëjtin mekanizëm të nxjerrjes dhe "zgjidhjes në kolona" e regjistrit të plotë të tabelës, ashtu si në Dhe gjithashtu do të aplikojmë përmbledhjen në array përmes funksionit ARRAY():
ME T T SI (
Zgjidh
unnest(ARRAY(
Zgjidh
t
Nga
task t
Ku
owner_id = unnest
Rendit sipas
task_date, id
Kufizo 20 -- e kufizojmë këtu...
)) r
Nga
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
Zgjidh
(r).*
Nga
T
Rendit sipas
(r).task_date, (r).id
Kufizo 20; -- ... dhe këtu - po ashtu 
O, tani më mirë! 40% më shpejt, dhe 4.5 herë më pak të dhëna që duhe të lexohej.
Materializimi i regjistrimeve të tabelave përmes CTEDua të theksoj se në disa raste përpjekja për të punuar menjëherë me fushat e regjistrimit pas kërkimit të saj në nënkërkesë, pa "mbështjellje" në CTE, mund të çojë në "shumzimin" e InitPlan proporcionalisht me numrin e këtyre fushave:
SELECT
((
SELECT
t
FROM
task t
WHERE
owner_id = 1
ORDER BY
task_date, id
LIMIT 1
).*);Rezultati (kostot=4.77..4.78 rreshta=1 gjerësi=16) (koha e vërtetë=0.063..0.063 rreshta=1 cikle=1)
Buffer: hit të përbashkëta=16
Plani i iniciatorit 1 (kthen $0)
-> Kufizimi (kostot=0.42..1.19 rreshta=1 gjerësi=48) (koha e vërtetë=0.031..0.032 rreshta=1 cikle=1)
Buffer: hit të përbashkëta=4
-> Skandimi i indeksit duke përdorur task_owner_id_task_date_id_idx mbi detyrën t (kostot=0.42..387.57 rreshta=500 gjerësi=48) (koha e vërtetë=0.030..0.030 rreshta=1 cikle=1)
Kushti i indeksit: (owner_id = 1)
Buffer: hit të përbashkëta=4
Plani i iniciatorit 2 (kthen $1)
-> Kufizimi (kostot=0.42..1.19 rreshta=1 gjerësi=48) (koha e vërtetë=0.008..0.009 rreshta=1 cikle=1)
Buffer: hit të përbashkëta=4
-> Skandimi i indeksit duke përdorur task_owner_id_task_date_id_idx mbi detyrën t_1 (kostot=0.42..387.57 rreshta=500 gjerësi=48) (koha e vërtetë=0.008..0.008 rreshta=1 cikle=1)
Kushti i indeksit: (owner_id = 1)
Buffer: hit të përbashkëta=4
Plani i iniciatorit 3 (kthen $2)
-> Kufizimi (kostot=0.42..1.19 rreshta=1 gjerësi=48) (koha e vërtetë=0.008..0.008 rreshta=1 cikle=1)
Buffer: hit të përbashkëta=4
-> Skandimi i indeksit duke përdorur task_owner_id_task_date_id_idx mbi detyrën t_2 (kostot=0.42..387.57 rreshta=500 gjerësi=48) (koha e vërtetë=0.008..0.008 rreshta=1 cikle=1)
Kushti i indeksit: (owner_id = 1)
Buffer: hit të përbashkëta=4"
Plani i iniciatorit 4 (kthen $3)
-> Kufizimi (kostot=0.42..1.19 rreshta=1 gjerësi=48) (koha e vërtetë=0.009..0.009 rreshta=1 cikle=1)
Buffer: hit të përbashkëta=4
-> Skandimi i indeksit duke përdorur task_owner_id_task_date_id_idx mbi detyrën t_3 (kostot=0.42..387.57 rreshta=500 gjerësi=48) (koha e vërtetë=0.009..0.009 rreshta=1 cikle=1)
Kushti i indeksit: (owner_id = 1)
Buffer: hit të përbashkëta=4
Një e njëjta shënim «u gjet» 4 herë⊠Deri në PostgreSQL 11, ky sjellje është e zakonshme, dhe zgjidhja është «mbështjellja» në CTE, që është një kufi i patjetërsueshëm për optimizuesin në këto versione.
Akkumulatori rekursiv
NĂ« variantin e mĂ«parshĂ«m, ne lexuam gjithsej 200 rreshta pĂ«r nevojĂ«n pĂ«r 20. Tani nuk janĂ« 960, por akoma mĂ« pak â a Ă«shtĂ« e mundur?
Le të provojmë të përdorim njohuritë tona se na duhen vetëm 20 shënime. Kështu që do të iterojmë leximin e të dhënave vetëm deri sa të arrijmë numrin e dëshiruar.
Hapi 1: lista fillestare
Kjo Ă«shtĂ« evidente se lista jonĂ« «e synuar» prej 20 shĂ«nimesh duhet tĂ« fillojĂ« me «shĂ«nimet e para» nga njĂ«ra prej çelĂ«save tanĂ« owner_id. Prandaj sĂ« pari do tĂ« gjejmĂ« ato «mĂ« tĂ« parat» pĂ«r secilin nga çelĂ«sat dhe do t'i vendosim nĂ« listĂ«, duke e renditur atĂ« nĂ« rendin qĂ« dĂ«shirojmĂ« â (task_date, id).

Hapi 2: gjejmë shënimet «në vijim»
Tani, nĂ«se e marrim shĂ«nimin e parĂ« nga lista jonĂ« dhe fillojmĂ« tĂ« «nĂ«ci» mĂ« tej pĂ«rmes indeksit me ruajtjen e çelĂ«sit owner_id, atĂ«herĂ« tĂ« gjitha shĂ«nimet e gjetura â janĂ« pikĂ«risht ato qĂ« vijojnĂ« nĂ« seçim rezultues. Sigurisht, vetĂ«m derisa tĂ« mos kalojmĂ« çelĂ«sin aplikativ tĂ« shĂ«nimit tĂ« dytĂ« nĂ« listĂ«.
Nëse ka ndodhur që kemi "prishur" regjistrimin e dytë, atëherë regjistrimi i fundit të lexuar duhet të shtohet në listë në vend të atij të parë (me të njëjtin owner_id), pas së cilës lista do të riorganizohet sërish.

Pra, gjithmonë kemi në listë jo më shumë se një regjistrim për secilin nga çelsat (nëse regjistrimet mbarojnë dhe ne nuk "prishim", atëherë regjistrimi i parë thjesht do të zhduket dhe nuk do të shtohet asgjë), për më tepër ato janë gjithmonë të renditura në rendin në rritje të çelësit aplikativ (task_date, id).

Hapi 3: filtrojmë dhe "zhvillojmë" regjistrimet
NĂ« disa rreshta tĂ« zgjedhjes tonĂ« rekursive disa regjistrime rv duket se janĂ« tĂ« dyfishta â fillimisht gjejmĂ« ato qĂ« "prekĂ«n kufirin e regjistrimit tĂ« dytĂ« tĂ« listĂ«s", dhe mĂ« pas i vendosim si tĂ« parĂ«n nĂ« listĂ«. Pra, shfaqja e parĂ« duhet tĂ« filtrohet.
Kërkesa e tmerrshme përfundimtare
ME RECURSIV T SI (
-- #1 : shtojmë në listën "të parat" regjistrime për çdo nga çelësat e grumbullimit
ME wrap SI ( -- "materializojmë" regjistrimet, në mënyrë që qasja në fushat të mos shkaktojë shumim InitPlan/SubPlan
ME T SI (
ZGJEDH
(
ZGJEDH
r
NGA
detyra r
KU
owner_id = unnest
RENDIT DHE
task_date, id
LIMIT 1
) r
NGA
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
ZGJEDH
array_agg(r RENDIT DHE (r).task_date, (r).id) list -- rendisim lista në rendin e duhur
NGA
T
)
ZGJEDH
list
, list[1] rv
, FALSE not_cross
, 0 size
NGA
wrap
UNION ALL
-- #2 : lexojmë regjistrimet e çelësit të parë, derisa të kalojmë mbi regjistrimin e çelësit të dytë
ZGJEDH
RAST
-- nëse nuk gjendet asgjë për çelësin e regjistrimit të parë
KUR X._r ĂSHTĂ JO NDARĂ NGA NULL ATĂHERĂ
T.list[2:] -- e heqim nga lista
-- nëse NE NUK kaluam çelësin aplikativ të regjistrimit të dytë
KUR X.not_cross ATĂHERĂ
T.list -- thjesht vazhdojmë listën e njëjtë pa modifikime
-- nëse në listë nuk ka regjistrim të dytë
KUR T.list[2] ĂSHTĂ NULL ATĂHERĂ
-- thjesht kthejmë listë të zbrazët
'{}'
-- riparojmë fjalorin, duke hequr regjistrimin e parë dhe duke shtuar të fundit të gjendur
NDĂRQoftĂ« (
ZGJEDH
coalesce(T.list[2] || array_agg(r RENDIT DHE (r).task_date, (r).id), '{}')
NGA
unnest(T.list[3:] || X._r) r
)
FUND
, X._r
, X.not_cross
, T.size + X.not_cross::integer
NGA
T
, LATERAL(
ME wrap SI ( -- "materializojmë" regjistrimin
ZGJEDH
RAST
-- nëse gjithsesi "kaluam" mbi regjistrimin e dytë
KUR NUK T.not_cross
-- atëherë regjistrimi i nevojshëm është i pari nga lista
ATĂHERĂ T.list[1]
NDĂRQoftĂ« ( -- nĂ«se nuk kaluan, atĂ«herĂ« çelĂ«si mbetet si nĂ« regjistrimin e mĂ«parshĂ«m - e bazojmĂ« nga ajo
ZGJEDH
_r
NGA
detyra _r
KU
owner_id = (rv).owner_id DHE
(task_date, id) > ((rv).task_date, (rv).id)
RENDIT DHE
task_date, id
LIMIT 1
)
FUND _r
)
ZGJEDH
_r
, RAST
-- nëse regjistrimi i dytë nuk është më në listë, por ndonjë gjë kemi gjetur
KUR list[2] ĂSHTĂ NULL DHE _r ĂSHTĂ NDARĂ NGA NULL ATĂHERĂ
TĂ VERTETĂ
NDĂRQoftĂ« -- nuk gjeta asgjĂ« ose "kaluam"
coalesce(((_r).task_date, (_r).id) < ((list[2]).task_date, (list[2]).id), FALSE)
FUND not_cross
NGA
wrap
) X
KU
T.size < 20 DHE -- kufizojmë këtu sasinë
T.list ĂSHTĂ JO NDARĂ NGA '{}' -- ose pĂ«r sa kohĂ« qĂ« lista nuk ka mbaruar
)
-- #3 : "zhvillojmë" regjistrimet - rendi është i garantuar sipas ndërtimit
ZGJEDH
(rv).*
NGA
T
KU
not_cross; -- marrim vetëm regjistrimet "jo të kaluara" 
Kështu, ne këmben 50% të leximeve të të dhënave për 20% kohë ekzekutimi. Kështu që, nëse keni arsye për të besuar se leximi mund të jetë i gjatë (p.sh., të dhënat shpesh nuk janë në cache dhe duhet të shkohet në disk për to), në këtë mënyrë mund të varfëroheni në lexim më pak.
NĂ« çdo rast, koha e ekzekutimit rezultoi mĂ« e mirĂ« se nĂ« versionin e parĂ« ânaivâ. Por cili nga kĂ«to 3 versione tĂ« pĂ«rdorni â Ă«shtĂ« vendimi juaj.
Burimi: habr.com
