Pana-panahon, ang gawain ng paghahanap para sa mga nauugnay na data gamit ang isang hanay ng mga susi ay lumitaw. hanggang makuha namin ang kinakailangang kabuuang bilang ng mga talaan.
Ang pinaka "tunay na buhay" na halimbawa ay ang pagpapakita 20 pinakalumang problema, nakalista sa listahan ng mga empleyado (halimbawa, sa loob ng isang dibisyon). Para sa iba't ibang "dashboard" ng pamamahala na may maikling buod ng mga lugar ng trabaho, ang isang katulad na paksa ay kinakailangan nang madalas.
Sa artikulong ito titingnan natin ang pagpapatupad sa PostgreSQL ng isang "walang muwang" na solusyon sa naturang problema, isang "mas matalino" at napakakomplikadong algorithm "loop" sa SQL na may kondisyon sa paglabas mula sa nahanap na data, na maaaring maging kapaki-pakinabang para sa pangkalahatang pag-unlad at para sa paggamit sa iba pang katulad na mga kaso.
Kumuha tayo ng set ng data ng pagsubok mula sa
CREATE INDEX ON task(owner_id, task_date, id);
-- Π° ΡΡΠ°ΡΡΠΉ - ΡΠ΄Π°Π»ΠΈΠΌ
DROP INDEX task_owner_id_task_date_idx;
Kung paanong narinig, gayon din ang nakasulat
Una, i-sketch natin ang pinakasimpleng bersyon ng kahilingan, na ipinapasa ang mga ID ng mga gumaganap
SELECT
*
FROM
task
WHERE
owner_id = ANY('{1,2,4,8,16,32,64,128,256,512}'::integer[])
ORDER BY
task_date, id
LIMIT 20;
Medyo malungkot - nag-order lang kami ng 20 record, ngunit ibinalik ito sa amin ng Index Scan 960 linya, na noon ay kailangan ding pagbukud-bukurin... Subukan nating magbasa nang kaunti.
unnest + ARRAY
Ang unang pagsasaalang-alang na makakatulong sa atin ay kung kailangan natin 20 lang ang nakaayos records, tapos basahin lang hindi hihigit sa 20 na pinagsunod-sunod sa parehong pagkakasunud-sunod para sa bawat isa susi. mabuti, angkop na index (owner_id, task_date, id) mayroon kami.
Gamitin natin ang parehong mekanismo para sa pagkuha at "pagkalat sa mga hanay" mahalagang talaan ng talahanayan, tulad ng sa ARRAY()
:
WITH T AS (
SELECT
unnest(ARRAY(
SELECT
t
FROM
task t
WHERE
owner_id = unnest
ORDER BY
task_date, id
LIMIT 20 -- ΠΎΠ³ΡΠ°Π½ΠΈΡΠΈΠ²Π°Π΅ΠΌ ΡΡΡ...
)) r
FROM
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SELECT
(r).*
FROM
T
ORDER BY
(r).task_date, (r).id
LIMIT 20; -- ... ΠΈ ΡΡΡ - ΡΠΎΠΆΠ΅
Oh, mas mabuti na! 40% mas mabilis at 4.5 beses na mas kaunting data Kinailangan kong basahin ito.
Pagpapatupad ng mga talaan ng talahanayan sa pamamagitan ng CTEHayaan mong ituon ko ang iyong pansin sa katotohanang iyon sa ibang Pagkakataon Ang isang pagtatangka na agad na magtrabaho kasama ang mga patlang ng isang talaan pagkatapos na hanapin ito sa isang subquery, nang hindi "binalot" ito sa isang CTE, ay maaaring humantong sa "multiply" InitPlan proporsyonal sa bilang ng mga parehong field na ito:
SELECT
((
SELECT
t
FROM
task t
WHERE
owner_id = 1
ORDER BY
task_date, id
LIMIT 1
).*);
Result (cost=4.77..4.78 rows=1 width=16) (actual time=0.063..0.063 rows=1 loops=1)
Buffers: shared hit=16
InitPlan 1 (returns $0)
-> Limit (cost=0.42..1.19 rows=1 width=48) (actual time=0.031..0.032 rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using task_owner_id_task_date_id_idx on task t (cost=0.42..387.57 rows=500 width=48) (actual time=0.030..0.030 rows=1 loops=1)
Index Cond: (owner_id = 1)
Buffers: shared hit=4
InitPlan 2 (returns $1)
-> Limit (cost=0.42..1.19 rows=1 width=48) (actual time=0.008..0.009 rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using task_owner_id_task_date_id_idx on task t_1 (cost=0.42..387.57 rows=500 width=48) (actual time=0.008..0.008 rows=1 loops=1)
Index Cond: (owner_id = 1)
Buffers: shared hit=4
InitPlan 3 (returns $2)
-> Limit (cost=0.42..1.19 rows=1 width=48) (actual time=0.008..0.008 rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using task_owner_id_task_date_id_idx on task t_2 (cost=0.42..387.57 rows=500 width=48) (actual time=0.008..0.008 rows=1 loops=1)
Index Cond: (owner_id = 1)
Buffers: shared hit=4"
InitPlan 4 (returns $3)
-> Limit (cost=0.42..1.19 rows=1 width=48) (actual time=0.009..0.009 rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using task_owner_id_task_date_id_idx on task t_3 (cost=0.42..387.57 rows=500 width=48) (actual time=0.009..0.009 rows=1 loops=1)
Index Cond: (owner_id = 1)
Buffers: shared hit=4
Ang parehong record ay "hinanap" ng 4 na beses... Hanggang sa PostgreSQL 11, ang gawi na ito ay nangyayari nang regular, at ang solusyon ay "i-wrap" ito sa isang CTE, na isang ganap na limitasyon para sa optimizer sa mga bersyong ito.
Recursive accumulator
Sa nakaraang bersyon, sa kabuuan ay nabasa natin 200 linya para sa kapakanan ng kinakailangang 20. Hindi 960, ngunit kahit na mas mababa - posible ba?
Subukan nating gamitin ang kaalaman na kailangan natin kabuuang 20 mga tala. Ibig sabihin, uulitin natin ang pagbabasa ng data hanggang sa maabot natin ang halagang kailangan natin.
Hakbang 1: Panimulang Listahan
Malinaw, ang aming "target" na listahan ng 20 talaan ay dapat magsimula sa "unang" mga tala para sa isa sa aming mga owner_id key. Samakatuwid, mahahanap muna natin ang ganoon "napaka una" para sa bawat isa sa mga susi at idagdag ito sa listahan, pag-uuri-uriin ito sa pagkakasunud-sunod na gusto namin - (task_date, id).
Hakbang 2: Hanapin ang "susunod" na mga entry
Ngayon kung kukunin namin ang unang entry mula sa aming listahan at magsimula "hakbang" pa kasama ang index pinapanatili ang owner_id key, pagkatapos ang lahat ng nahanap na tala ay eksaktong susunod sa resultang pagpili. Syempre, lang hanggang sa tumawid kami sa butt key pangalawang entry sa listahan.
Kung ito ay lumabas na "tinawid" namin ang pangalawang rekord, kung gayon ang huling entry na nabasa ay dapat idagdag sa listahan sa halip na ang una (na may kaparehong owner_id), pagkatapos ay muli naming inuri-uriin muli ang listahan.
Iyon ay, palagi nating nakukuha na ang listahan ay may hindi hihigit sa isang entry para sa bawat isa sa mga susi (kung ang mga entry ay maubusan at hindi tayo "tumawid", kung gayon ang unang entry mula sa listahan ay mawawala lang at walang maidaragdag. ), at sila laging nakaayos sa pataas na pagkakasunud-sunod ng application key (task_date, id).
Hakbang 3: i-filter at "palawakin" ang mga tala
Sa ilan sa mga row ng aming recursive selection, ilang record rv
ay nadoble - unang nakita namin tulad ng "pagtawid sa hangganan ng 2nd entry ng listahan", at pagkatapos ay palitan ito bilang ang 1st mula sa listahan. Kaya kailangang i-filter ang unang pangyayari.
Ang kinatatakutang huling query
WITH RECURSIVE T AS (
-- #1 : Π·Π°Π½ΠΎΡΠΈΠΌ Π² ΡΠΏΠΈΡΠΎΠΊ "ΠΏΠ΅ΡΠ²ΡΠ΅" Π·Π°ΠΏΠΈΡΠΈ ΠΏΠΎ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡ ΠΈΠ· ΠΊΠ»ΡΡΠ΅ΠΉ Π½Π°Π±ΠΎΡΠ°
WITH wrap AS ( -- "ΠΌΠ°ΡΠ΅ΡΠΈΠ°Π»ΠΈΠ·ΡΠ΅ΠΌ" record'Ρ, ΡΡΠΎΠ±Ρ ΠΎΠ±ΡΠ°ΡΠ΅Π½ΠΈΠ΅ ΠΊ ΠΏΠΎΠ»ΡΠΌ Π½Π΅ Π²ΡΠ·ΡΠ²Π°Π»ΠΎ ΡΠΌΠ½ΠΎΠΆΠ΅Π½ΠΈΡ InitPlan/SubPlan
WITH T AS (
SELECT
(
SELECT
r
FROM
task r
WHERE
owner_id = unnest
ORDER BY
task_date, id
LIMIT 1
) r
FROM
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SELECT
array_agg(r ORDER BY (r).task_date, (r).id) list -- ΡΠΎΡΡΠΈΡΡΠ΅ΠΌ ΡΠΏΠΈΡΠΎΠΊ Π² Π½ΡΠΆΠ½ΠΎΠΌ ΠΏΠΎΡΡΠ΄ΠΊΠ΅
FROM
T
)
SELECT
list
, list[1] rv
, FALSE not_cross
, 0 size
FROM
wrap
UNION ALL
-- #2 : Π²ΡΡΠΈΡΡΠ²Π°Π΅ΠΌ Π·Π°ΠΏΠΈΡΠΈ 1-Π³ΠΎ ΠΏΠΎ ΠΏΠΎΡΡΠ΄ΠΊΡ ΠΊΠ»ΡΡΠ°, ΠΏΠΎΠΊΠ° Π½Π΅ ΠΏΠ΅ΡΠ΅ΡΠ°Π³Π½Π΅ΠΌ ΡΠ΅ΡΠ΅Π· Π·Π°ΠΏΠΈΡΡ 2-Π³ΠΎ
SELECT
CASE
-- Π΅ΡΠ»ΠΈ Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ Π΄Π»Ρ ΠΊΠ»ΡΡΠ° 1-ΠΉ Π·Π°ΠΏΠΈΡΠΈ
WHEN X._r IS NOT DISTINCT FROM NULL THEN
T.list[2:] -- ΡΠ±ΠΈΡΠ°Π΅ΠΌ Π΅Π΅ ΠΈΠ· ΡΠΏΠΈΡΠΊΠ°
-- Π΅ΡΠ»ΠΈ ΠΌΡ ΠΠ ΠΏΠ΅ΡΠ΅ΡΠ΅ΠΊΠ»ΠΈ ΠΏΡΠΈΠΊΠ»Π°Π΄Π½ΠΎΠΉ ΠΊΠ»ΡΡ 2-ΠΉ Π·Π°ΠΏΠΈΡΠΈ
WHEN X.not_cross THEN
T.list -- ΠΏΡΠΎΡΡΠΎ ΠΏΡΠΎΡΡΠ³ΠΈΠ²Π°Π΅ΠΌ ΡΠΎΡ ΠΆΠ΅ ΡΠΏΠΈΡΠΎΠΊ Π±Π΅Π· ΠΌΠΎΠ΄ΠΈΡΠΈΠΊΠ°ΡΠΈΠΉ
-- Π΅ΡΠ»ΠΈ Π² ΡΠΏΠΈΡΠΊΠ΅ ΡΠΆΠ΅ Π½Π΅Ρ 2-ΠΉ Π·Π°ΠΏΠΈΡΠΈ
WHEN T.list[2] IS NULL THEN
-- ΠΏΡΠΎΡΡΠΎ Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌ ΠΏΡΡΡΠΎΠΉ ΡΠΏΠΈΡΠΎΠΊ
'{}'
-- ΠΏΠ΅ΡΠ΅ΡΠΎΡΡΠΈΡΠΎΠ²ΡΠ²Π°Π΅ΠΌ ΡΠ»ΠΎΠ²Π°ΡΡ, ΡΠ±ΠΈΡΠ°Ρ 1-Ρ Π·Π°ΠΏΠΈΡΡ ΠΈ Π΄ΠΎΠ±Π°Π²Π»ΡΡ ΠΏΠΎΡΠ»Π΅Π΄Π½ΡΡ ΠΈΠ· Π½Π°ΠΉΠ΄Π΅Π½Π½ΡΡ
ELSE (
SELECT
coalesce(T.list[2] || array_agg(r ORDER BY (r).task_date, (r).id), '{}')
FROM
unnest(T.list[3:] || X._r) r
)
END
, X._r
, X.not_cross
, T.size + X.not_cross::integer
FROM
T
, LATERAL(
WITH wrap AS ( -- "ΠΌΠ°ΡΠ΅ΡΠΈΠ°Π»ΠΈΠ·ΡΠ΅ΠΌ" record
SELECT
CASE
-- Π΅ΡΠ»ΠΈ Π²ΡΠ΅-ΡΠ°ΠΊΠΈ "ΠΏΠ΅ΡΠ΅ΡΠ°Π³Π½ΡΠ»ΠΈ" ΡΠ΅ΡΠ΅Π· 2-Ρ Π·Π°ΠΏΠΈΡΡ
WHEN NOT T.not_cross
-- ΡΠΎ Π½ΡΠΆΠ½Π°Ρ Π·Π°ΠΏΠΈΡΡ - ΠΏΠ΅ΡΠ²Π°Ρ ΠΈΠ· ΡΠΏΠΏΠΈΡΠΊΠ°
THEN T.list[1]
ELSE ( -- Π΅ΡΠ»ΠΈ Π½Π΅ ΠΏΠ΅ΡΠ΅ΡΠ΅ΠΊΠ»ΠΈ, ΡΠΎ ΠΊΠ»ΡΡ ΠΎΡΡΠ°Π»ΡΡ ΠΊΠ°ΠΊ Π² ΠΏΡΠ΅Π΄ΡΠ΄ΡΡΠ΅ΠΉ Π·Π°ΠΏΠΈΡΠΈ - ΠΎΡΡΠ°Π»ΠΊΠΈΠ²Π°Π΅ΠΌΡΡ ΠΎΡ Π½Π΅Π΅
SELECT
_r
FROM
task _r
WHERE
owner_id = (rv).owner_id AND
(task_date, id) > ((rv).task_date, (rv).id)
ORDER BY
task_date, id
LIMIT 1
)
END _r
)
SELECT
_r
, CASE
-- Π΅ΡΠ»ΠΈ 2-ΠΉ Π·Π°ΠΏΠΈΡΠΈ ΡΠΆΠ΅ Π½Π΅Ρ Π² ΡΠΏΠΈΡΠΊΠ΅, Π½ΠΎ ΠΌΡ Ρ
ΠΎΡΡ ΡΡΠΎ-ΡΠΎ Π½Π°ΡΠ»ΠΈ
WHEN list[2] IS NULL AND _r IS DISTINCT FROM NULL THEN
TRUE
ELSE -- Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ Π½Π°ΡΠ»ΠΈ ΠΈΠ»ΠΈ "ΠΏΠ΅ΡΠ΅ΡΠ°Π³Π½ΡΠ»ΠΈ"
coalesce(((_r).task_date, (_r).id) < ((list[2]).task_date, (list[2]).id), FALSE)
END not_cross
FROM
wrap
) X
WHERE
T.size < 20 AND -- ΠΎΠ³ΡΠ°Π½ΠΈΡΠΈΠ²Π°Π΅ΠΌ ΡΡΡ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ
T.list IS DISTINCT FROM '{}' -- ΠΈΠ»ΠΈ ΠΏΠΎΠΊΠ° ΡΠΏΠΈΡΠΎΠΊ Π½Π΅ ΠΊΠΎΠ½ΡΠΈΠ»ΡΡ
)
-- #3 : "ΡΠ°Π·Π²ΠΎΡΠ°ΡΠΈΠ²Π°Π΅ΠΌ" Π·Π°ΠΏΠΈΡΠΈ - ΠΏΠΎΡΡΠ΄ΠΎΠΊ Π³Π°ΡΠ°Π½ΡΠΈΡΠΎΠ²Π°Π½ ΠΏΠΎ ΠΏΠΎΡΡΡΠΎΠ΅Π½ΠΈΡ
SELECT
(rv).*
FROM
T
WHERE
not_cross; -- Π±Π΅ΡΠ΅ΠΌ ΡΠΎΠ»ΡΠΊΠΎ "Π½Π΅ΠΏΠ΅ΡΠ΅ΡΠ΅ΠΊΠ°ΡΡΠΈΠ΅" Π·Π°ΠΏΠΈΡΠΈ
Kaya, kami ipinagpalit ang 50% ng mga nabasang data para sa 20% ng oras ng pagpapatupad. Iyon ay, kung mayroon kang mga dahilan upang maniwala na ang pagbabasa ay maaaring tumagal ng mahabang panahon (halimbawa, ang data ay madalas na wala sa cache, at kailangan mong pumunta sa disk para dito), kung gayon sa paraang ito maaari kang umasa nang mas kaunti sa pagbabasa .
Sa anumang kaso, ang oras ng pagpapatupad ay naging mas mahusay kaysa sa "naive" na unang pagpipilian. Ngunit alin sa 3 opsyong ito ang iyong gagamitin.
Pinagmulan: www.habr.com