Bet bieži vien bez tā pieprasījums izrādās ievērojami produktīvāks nekā ar to. Tāpēc šodien mēģināsim atbrīvoties no resursietilpīga JOIN - izmantojot vārdnīcu.
Sākot ar PostgreSQL 12, dažas no tālāk aprakstītajām situācijām var tikt reproducētas nedaudz atšķirīgi, jo noklusējuma nematerializācija CTE. Šo darbību var atsaukt, norādot atslēgu MATERIALIZED.
Daudz “faktu” ierobežotā vārdu krājumā
Ņemsim ļoti reālu lietojumprogrammas uzdevumu - mums ir jāparāda saraksts ienākošās ziņas vai aktīvi uzdevumi ar sūtītājiem:
25.01 | Иванов И.И. | Подготовить описание нового алгоритма.
22.01 | Иванов И.И. | Написать статью на Хабр: жизнь без JOIN.
20.01 | Петров П.П. | Помочь оптимизировать запрос.
18.01 | Иванов И.И. | Написать статью на Хабр: JOIN с учетом распределения данных.
16.01 | Петров П.П. | Помочь оптимизировать запрос.
Abstraktajā pasaulē uzdevumu autoriem vajadzētu būt vienmērīgi sadalītiem starp visiem mūsu organizācijas darbiniekiem, bet patiesībā uzdevumus, kā likums, veic diezgan ierobežots cilvēku skaits - "no vadības" uz augšu pa hierarhiju vai "no apakšuzņēmējiem" no blakus esošajām nodaļām (analītiķi, dizaineri, mārketings, ...).
Pieņemsim, ka mūsu 1000 cilvēku organizācijā tikai 20 autori (parasti pat mazāk) izvirza uzdevumus katram konkrētajam izpildītājam un Izmantosim šīs priekšmeta zināšanaslai paātrinātu "tradicionālo" vaicājumu.
Skriptu ģenerators
-- сотрудники
CREATE TABLE person AS
SELECT
id
, repeat(chr(ascii('a') + (id % 26)), (id % 32) + 1) "name"
, '2000-01-01'::date - (random() * 1e4)::integer birth_date
FROM
generate_series(1, 1000) id;
ALTER TABLE person ADD PRIMARY KEY(id);
-- задачи с указанным распределением
CREATE TABLE task AS
WITH aid AS (
SELECT
id
, array_agg((random() * 999)::integer + 1) aids
FROM
generate_series(1, 1000) id
, generate_series(1, 20)
GROUP BY
1
)
SELECT
*
FROM
(
SELECT
id
, '2020-01-01'::date - (random() * 1e3)::integer task_date
, (random() * 999)::integer + 1 owner_id
FROM
generate_series(1, 100000) id
) T
, LATERAL(
SELECT
aids[(random() * (array_length(aids, 1) - 1))::integer + 1] author_id
FROM
aid
WHERE
id = T.owner_id
LIMIT 1
) a;
ALTER TABLE task ADD PRIMARY KEY(id);
CREATE INDEX ON task(owner_id, task_date);
CREATE INDEX ON task(author_id);
Parādīsim pēdējos 100 uzdevumus konkrētam izpildītājam:
SELECT
task.*
, person.name
FROM
task
LEFT JOIN
person
ON person.id = task.author_id
WHERE
owner_id = 777
ORDER BY
task_date DESC
LIMIT 100;
Izrādās, ka 1/3 kopējā laika un 3/4 rādījumu datu lapas tika veiktas tikai, lai meklētu autoru 100 reizes - katram izvades uzdevumam. Bet mēs zinām, ka starp šiem simtiem tikai 20 dažādi – Vai šīs zināšanas ir iespējams izmantot?
hstore-vārdnīca
Izmantosim priekšrocības hstore tips lai ģenerētu “vārdnīcas” atslēgas vērtību:
CREATE EXTENSION hstore
Mums vienkārši jāievieto vārdnīcā autora ID un viņa vārds, lai pēc tam varētu izvilkt, izmantojot šo atslēgu:
-- формируем целевую выборку
WITH T AS (
SELECT
*
FROM
task
WHERE
owner_id = 777
ORDER BY
task_date DESC
LIMIT 100
)
-- формируем словарь для уникальных значений
, dict AS (
SELECT
hstore( -- hstore(keys::text[], values::text[])
array_agg(id)::text[]
, array_agg(name)::text[]
)
FROM
person
WHERE
id = ANY(ARRAY(
SELECT DISTINCT
author_id
FROM
T
))
)
-- получаем связанные значения словаря
SELECT
*
, (TABLE dict) -> author_id::text -- hstore -> key
FROM
T;
Iztērēti informācijas iegūšanai par personām 2 reizes mazāk laika un 7 reizes mazāk nolasīto datu! Papildus “vārdu krājumam” arī tas, kas mums palīdzēja sasniegt šos rezultātus, bija lielapjoma ierakstu izguve no galda vienā piegājienā, izmantojot = ANY(ARRAY(...)).
Tabulas ieraksti: Serializācija un Deserializācija
Bet ko darīt, ja mums jāsaglabā ne tikai viens teksta lauks, bet viss vārdnīcas ieraksts? Šajā gadījumā PostgreSQL spēja mums palīdzēs apstrādāt tabulas ierakstu kā vienu vērtību:
...
, dict AS (
SELECT
hstore(
array_agg(id)::text[]
, array_agg(p)::text[] -- магия #1
)
FROM
person p
WHERE
...
)
SELECT
*
, (((TABLE dict) -> author_id::text)::person).* -- магия #2
FROM
T;
Apskatīsim, kas šeit notika:
Mēs paņēmām p kā aizstājvārdu pilnas personas tabulas ierakstam un apkopoja to masīvu.
Šis ierakstu masīvs tika pārstrādāts teksta virkņu masīvam (person[]::teksts[]), lai ievietotu to hstore vārdnīcā kā vērtību masīvu.
Kad saņemam saistītu ierakstu, mēs izvilkts no vārdnīcas pēc atslēgas kā teksta virkne.
Mums vajag tekstu pārvērst par tabulas tipa vērtību persona (katrai tabulai automātiski tiek izveidots tāda paša nosaukuma tips).
“Izvērsiet” ierakstīto ierakstu kolonnās, izmantojot (...).*.
json vārdnīca
Bet šāds triks, ko izmantojām iepriekš, nedarbosies, ja nav atbilstoša tabulas veida, lai veiktu “casting”. Tieši tāda pati situācija radīsies, un, ja mēs mēģināsim izmantot CTE rinda, nevis "īsta" tabula.
...
, p AS ( -- это уже CTE
SELECT
*
FROM
person
WHERE
...
)
, dict AS (
SELECT
json_object( -- теперь это уже json
array_agg(id)::text[]
, array_agg(row_to_json(p))::text[] -- и внутри json для каждой строки
)
FROM
p
)
SELECT
*
FROM
T
, LATERAL(
SELECT
*
FROM
json_to_record(
((TABLE dict) ->> author_id::text)::json -- извлекли из словаря как json
) AS j(name text, birth_date date) -- заполнили нужную нам структуру
) j;
Jāatzīmē, ka, aprakstot mērķa struktūru, mēs nevaram uzskaitīt visus avota virknes laukus, bet tikai tos, kas mums patiešām ir nepieciešami. Ja mums ir “native” tabula, tad labāk ir izmantot funkciju json_populate_record.
Mēs joprojām piekļūstam vārdnīcai vienreiz, bet json-[de]serializācijas izmaksas ir diezgan augstas, tāpēc šo metodi ir saprātīgi izmantot tikai atsevišķos gadījumos, kad “godīgais” CTE Scan sevi parāda sliktāk.
Testēšanas veiktspēja
Tātad, mums ir divi veidi, kā serializēt datus vārdnīcā - hstore/json_object. Turklāt pašus atslēgu un vērtību masīvus var ģenerēt arī divos veidos, veicot iekšēju vai ārēju pārveidošanu par tekstu: masīvs_agg(i::teksts) / masīvs_agg(i)::teksts[].
Pārbaudīsim dažādu serializācijas veidu efektivitāti, izmantojot tīri sintētisku piemēru - serializēt dažādus atslēgu numurus:
WITH dict AS (
SELECT
hstore(
array_agg(i::text)
, array_agg(i::text)
)
FROM
generate_series(1, ...) i
)
TABLE dict;
Novērtēšanas skripts: serializācija
WITH T AS (
SELECT
*
, (
SELECT
regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
FROM
(
SELECT
array_agg(el) ea
FROM
dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
explain analyze
WITH dict AS (
SELECT
hstore(
array_agg(i::text)
, array_agg(i::text)
)
FROM
generate_series(1, $$ || (1 << v) || $$) i
)
TABLE dict
$$) T(el text)
) T
) et
FROM
generate_series(0, 19) v
, LATERAL generate_series(1, 7) i
ORDER BY
1, 2
)
SELECT
v
, avg(et)::numeric(32,3)
FROM
T
GROUP BY
1
ORDER BY
1;
Operētājsistēmā PostgreSQL 11 — līdz aptuveni vārdnīcas lielumam 2^12 atslēgas serializācija uz JSON aizņem mazāk laika. Šajā gadījumā visefektīvākā ir json_object un “iekšējā” tipa konvertēšanas kombinācija array_agg(i::text).
Tagad mēģināsim nolasīt katras atslēgas vērtību 8 reizes - galu galā, ja jūs nepiekļūstat vārdnīcai, tad kāpēc tas ir vajadzīgs?
Novērtēšanas skripts: lasīšana no vārdnīcas
WITH T AS (
SELECT
*
, (
SELECT
regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
FROM
(
SELECT
array_agg(el) ea
FROM
dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
explain analyze
WITH dict AS (
SELECT
json_object(
array_agg(i::text)
, array_agg(i::text)
)
FROM
generate_series(1, $$ || (1 << v) || $$) i
)
SELECT
(TABLE dict) -> (i % ($$ || (1 << v) || $$) + 1)::text
FROM
generate_series(1, $$ || (1 << (v + 3)) || $$) i
$$) T(el text)
) T
) et
FROM
generate_series(0, 19) v
, LATERAL generate_series(1, 7) i
ORDER BY
1, 2
)
SELECT
v
, avg(et)::numeric(32,3)
FROM
T
GROUP BY
1
ORDER BY
1;
Un... jau aptuveni ar 2^6 taustiņiem, lasīšana no json vārdnīcas sāk zaudēt vairākas reizes lasot no hstore, jsonb tas pats notiek pie 2^9.
Galīgie secinājumi:
ja jums tas jādara PIEVIENOJIES ar vairākiem atkārtotiem ierakstiem — labāk izmantot tabulas “vārdnīcu”.
ja tiek gaidīta jūsu vārdnīca mazs un no tā neko daudz neizlasīsi - varat izmantot json[b]
visos citos gadījumos hstore + array_agg(i::text) būs efektīvāka