PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN

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.

PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN

FrÄn och med PostgreSQL 12 kan vissa av de nedan beskrivna situationerna upprepas lite annorlunda pÄ grund av ingen materialisering av CTE som standard. 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 ingĂ„ende meddelanden 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 anvÀnda denna Àmneskunskap, 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;

PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN
[titta pÄ explain.tensor.ru]

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 för att generera en "ordbok" nyckel-vÀrde: 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

PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN
[titta pÄ explain.tensor.ru]

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

  1. p som en alias för hela posten i tabellen person och samlade dem i en array. Đž ŃĐŸĐ±Ń€Đ°Đ»Đž Оз ĐœĐžŃ… ĐŒĐ°ŃŃĐžĐČ.
  2. 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.
  3. Vid hÀmtning av den relaterade posten hÀmtade vi den ur ordboken med hjÀlp av nyckeln som en textstrÀng.
  4. Texten mÄste vi konvertera till en tabelltyp person (för varje tabell skapas automatiskt en med samma namn typ).
  5. 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 functioner för att arbeta med json:

...
, 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;

PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN

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;

PostgreSQL Antipatterns: slÄ till med ordboken pÄ tunga JOIN

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

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster