Vi fortsÀtter serien av artiklar som rör utforskning av mindre kÀnda sÀtt att förbÀttra prestandan för "till synes enkla" förfrÄgningar i PostgreSQL:
Tro inte att jag avskyr JOIN sĂ„ mycket... đ
Men ofta blir en förfrĂ„gan betydligt mer effektiv utan det. DĂ€rför ska vi idag försöka bli av med den resurskrĂ€vande JOIN â med hjĂ€lp av en ordbok.

FrÄn och med PostgreSQL 12 kan vissa av de nedan beskrivna situationerna upprepas lite annorlunda pÄ grund av . Detta beteende kan ÄterstÀllas till det tidigare genom att ange nyckeln
MATERIALIZED.
MÄnga "fakta" om det begrÀnsade lexikonet
LĂ„t oss ta en helt verklig applikationsuppgift â vi behöver visa en lista över eller aktiva uppgifter med avsĂ€ndare:
25.01 | Ivanov I.I. | Förbereda beskrivning av den nya algoritmen.
22.01 | Ivanov I.I. | Skriva en artikel pÄ Habr: livet utan JOIN.
20.01 | Petrov P.P. | HjÀlpa till att optimera förfrÄgan.
18.01 | Ivanov I.I. | Skriva en artikel pÄ Habr: JOIN med hÀnsyn till datadistribution.
16.01 | Petrov P.P. | HjÀlpa till att optimera förfrÄgan.
I en abstrakt vĂ€rld skulle uppgiftsförfattarna spridas jĂ€mnt bland alla anstĂ€llda i vĂ„r organisation, men i verkligheten kommer uppgifter oftast frĂ„n ett ganska begrĂ€nsat antal personer â "frĂ„n ledningen" uppĂ„t i hierarkin eller "frĂ„n kollegor" frĂ„n nĂ€rliggande avdelningar (analytiker, designers, marknadsföring, âŠ).
LÄt oss anta att det i vÄr organisation finns 1000 personer och endast 20 författare (oftast Ànnu fÀrre) stÀller uppgifter till varje specifik utförare och , för att snabba upp den "traditionella" förfrÄgan.
Skapande av skript
-- anstÀllda
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);
-- uppgifter med angiven distribution
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);
Vi visar de senaste 100 uppgifterna för en viss pÄverkad person:
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; 
Det visar sig att 1/3 av den totala tiden och 3/4 av lĂ€sningarna datablad har gjorts enbart för att 100 gĂ„nger söka efter författaren â för varje utgiven uppgift. Men vi vet ju att det finns 20 olika det gĂ„r att anvĂ€nda denna kunskap? hstore-ordbok
hstore typ
LÄt oss anvÀnda CREATE EXTENSION hstore
I ordboken rÀcker det att inkludera författarens ID och namn, för att senare kunna hÀmta det med detta nyckel:-- skapar en mÄlvÀrdesuppsÀttning WITH T AS ( SELECT * FROM task WHERE owner_id = 777 ORDER BY task_date DESC LIMIT 100 ) -- skapar en ordbok för unika vÀrden , 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 )) ) -- fÄr relaterade vÀrden frÄn ordboken SELECT * , (TABLE dict) -> author_id::text -- hstore -> nyckel FROM T;
Information om personerna tog 
hÀlften sÄ lÄng tid och 7 gÄnger mindre data lÀstes ! Förutom "ordboksgörande" har dessa resultat ocksÄ uppnÄtts genommassavhÀmtning av poster frÄn tabellen med ett enda pass med hjÀlp av = ANY(ARRAY(...)) Tabellposter: serialisering och deserialisering.
Men vad gör vi om vi behöver spara mer Àn ett textfÀlt i ordboken, utan hela posten? I detta fall kan PostgreSQL
arbeta med tabellposten som ett enda vÀrde ... , dict AS ( SELECT hstore( array_agg(id)::text[] , array_agg(p)::text[] -- magi #1 ) FROM person p WHERE ... ) SELECT * , (((TABLE dict) -> author_id::text)::person).* -- magi #2 FROM T;:
LÄt oss bryta ner vad som verkligen hÀnde hÀr:Vi tog
- p som en alias för hela posten i tabellen person och samlade dem i en array. Đž ŃĐŸĐ±ŃалО Оз ĐœĐžŃ ĐŒĐ°ŃŃĐžĐČ.
- Denna datamÀngd har typats om till ett array av textstrÀngar (person[]::text[]), för att placera den i ett hstore-ordbok som ett vÀrdearray.
- Vid hÀmtning av den relaterade posten hÀmtade vi den ur ordboken med hjÀlp av nyckeln som en textstrÀng.
- Texten mÄste vi konvertera till en tabelltyp person (för varje tabell skapas automatiskt en med samma namn typ).
- Vi "utvecklade" den typade posten till kolumner med hjÀlp av
(...).*.
json-ordbok
Men ett sÄdant trick som vi anvÀnde ovan fungerar inte, om det inte finns en motsvarande tabelltyp för att göra "typomvandlingen". Lika kommer det ske, om vi försöker anvÀnda CTE-strÀngen, snarare Àn en "real" tabell.
I detta fall hjÀlper oss :
...
, p AS ( -- detta Àr redan CTE
SELECT
*
FROM
person
WHERE
...
)
, dict AS (
SELECT
json_object( -- nu Àr det redan json
array_agg(id)::text[]
, array_agg(row_to_json(p))::text[] -- och inuti json för varje rad
)
FROM
p
)
SELECT
*
FROM
T
, LATERAL(
SELECT
*
FROM
json_to_record(
((TABLE dict) ->> author_id::text)::json -- hÀmtades frÄn ordboken som json
) AS j(name text, birth_date date) -- fyllde i den struktur vi behövde
) j; Det Àr vÀrt att nÀmna att vid beskrivning av den mÄlsatta strukturen kan vi lista inte alla fÀlt i den ursprungliga strÀngen, utan endast de som vi verkligen behöver. Om vi har en "inhemsk" tabell, Àr det bÀttre att anvÀnda funktionen json_populate_record.
à tkomsten till ordboken sker fortfarande pÄ en gÄng, men kostnaderna för json-[de]serialisering Àr ganska höga, dÀrför Àr det rimligt att anvÀnda detta sÀtt endast i vissa fall nÀr en "Àrlig" CTE Scan visar sig vara sÀmre.
Testar prestanda
SĂ„, vi har fĂ„tt tvĂ„ sĂ€tt att serialisera data till en ordbok â hstore / json_object. Utöver detta kan sjĂ€lva arrayer av nycklar och vĂ€rden genereras pĂ„ tvĂ„ sĂ€tt, med intern eller extern konvertering till text: array_agg(i::text) / array_agg(i)::text[].
LĂ„t oss kontrollera effektiviteten av olika typer av serialisering pĂ„ ett helt syntetiskt exempel â serialiserar olika antal nycklar:
WITH dict AS (
SELECT
hstore(
array_agg(i::text)
, array_agg(i::text)
)
FROM
generate_series(1, ...) i
)
TABLE dict;UtvÀrderingsskript: serialisering
MED T SOM (
VĂLJ
*
, (
VĂLJ
regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
FRĂ
N
(
VĂLJ
array_agg(el) ea
FRĂ
N
dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
explain analyze
MED dict SOM (
VĂLJ
hstore(
array_agg(i::text)
, array_agg(i::text)
)
FRĂ
N
generate_series(1, $$ || (1 << v) || $$) i
)
TABELL dict
$$) T(el text)
) T
) et
FRĂ
N
generate_series(0, 19) v
, LATERAL generate_series(1, 7) i
ORDER BY
1, 2
)
VĂLJ
v
, avg(et)::numeric(32,3)
FRĂ
N
T
GRUPPERA EFTER
1
ORDER BY
1; 
I PostgreSQL 11 ungefÀr upp till storleken av ordboken pÄ 2^12 nycklar serialisering till json tar mindre tid. Den mest effektiva kombinationen Àr json_object och 'intern' typomvandling array_agg(i::text).
Nu ska vi försöka lĂ€sa vĂ€rdet av varje nyckel 8 gĂ„nger â om vi inte interagerar med ordboken, vad Ă€r dĂ„ dess syfte?
UtvÀrderingsskript: lÀsning frÄn ordboken
MED T SOM (
VĂLJ
*
, (
VĂLJ
regexp_replace(ea[array_length(ea, 1)], '^Execution Time: (d+.d+) ms$', '1')::real et
FRĂ
N
(
VĂLJ
array_agg(el) ea
FRĂ
N
dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
explain analyze
MED dict SOM (
VĂLJ
json_object(
array_agg(i::text)
, array_agg(i::text)
)
FRĂ
N
generate_series(1, $$ || (1 << v) || $$) i
)
VĂLJ
(TABELL dict) -> (i % ($$ || (1 << v) || $$) + 1)::text
FRĂ
N
generate_series(1, $$ || (1 << (v + 3)) || $$) i
$$) T(el text)
) T
) et
FRĂ
N
generate_series(0, 19) v
, LATERAL generate_series(1, 7) i
ORDER BY
1, 2
)
VĂLJ
v
, avg(et)::numeric(32,3)
FRĂ
N
T
GRUPPERA EFTER
1
ORDER BY
1; 
Och⊠ungefÀr vid 2^6 nycklar börjar lÀsning frÄn json-ordboken att förlora kraftigt jÀmfört med hstore, för jsonb hÀnder detsamma vid 2^9.
Slutliga slutsatser:
- om du behöver göra JOIN med upprepade poster â Ă€r det bĂ€ttre att anvĂ€nda 'ordbok' för tabellen
- om din ordbok förvĂ€ntas vara liten och du kommer att lĂ€sa lite frĂ„n den â kan json[b] anvĂ€ndas
- i alla andra fall hstore + array_agg(i::text) kommer att vara mer effektivt
KĂ€lla: habr.com
