PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Ne vazhdojmë serinë tonë të artikujve që eksplorojnë mënyra pak të njohura për të përmirësuar performancën e pyetjeve në dukje të thjeshta në PostgreSQL:

Mos mendo se nuk më pëlqen aq shumë JOIN... 🙂

Por shpesh, pa të, pyetja rezulton të jetë dukshëm më produktive sesa me të. Pra, sot do ta provojmë plotësisht. hiqni qafe JOIN-in që kërkon shumë burime — duke përdorur një fjalor.

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Duke filluar me PostgreSQL 12, disa nga situatat e përshkruara më poshtë mund të riprodhohen paksa ndryshe për shkak të mos-materializimi i CTE-ve si parazgjedhjeKjo sjellje mund të rikthehet në sjelljen e mëparshme duke specifikuar një çelës MATERIALIZED.

Shumë "fakte" në një fjalor të kufizuar

Le të marrim një problem shumë të vërtetë të aplikuar - duhet të nxjerrim një listë mesazhet hyrëse ose detyra aktive me dërgues:

25.01 | Иванов И.И. | Подготовить описание нового алгоритма.
22.01 | Иванов И.И. | Написать статью на Хабр: жизнь без JOIN.
20.01 | Петров П.П. | Помочь оптимизировать запрос.
18.01 | Иванов И.И. | Написать статью на Хабр: JOIN с учетом распределения данных.
16.01 | Петров П.П. | Помочь оптимизировать запрос.

Në një botë abstrakte, autorët e detyrave duhet të shpërndahen në mënyrë të barabartë midis të gjithë punonjësve të organizatës sonë, por në realitet Detyrat vijnë, si rregull, nga një numër mjaft i kufizuar njerëzish — “nga menaxhmenti” lart në hierarki ose “nga njerëz të lidhur” nga departamentet fqinje (analistë, dizajnerë, marketing, ...).

Le të supozojmë se në organizatën tonë prej 1000 personash, vetëm 20 autorë (zakonisht edhe më pak) caktojnë detyra për secilin interpretues specifik dhe le të përfitojmë nga njohuritë e kësaj lëndepër të shpejtuar pyetjen "tradicionale".

Gjenerator skriptesh

-- сотрудники
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);

Do të tregojmë 100 detyrat e fundit për një interpretues specifik:

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: Dictionary Hit Heavy JOIN
[shikoni në shpjegojnë.tensor.ru]

Ajo rezulton se 1/3 e kohës totale dhe 3/4 e leximeve Faqet e të dhënave u krijuan vetëm për të kërkuar autorin 100 herë - për secilën detyrë të shfaqur. Por ne e dimë se midis këtyre qindra vetëm 20 të ndryshme — A është e mundur të përdoret kjo njohuri?

fjalor hstore

Le të përfitojmë shkruani hstore për të gjeneruar një "fjalor" me çelës-vlerë:

CREATE EXTENSION hstore

Na duhet vetëm të vendosim ID-në dhe emrin e autorit në fjalor, në mënyrë që ta nxjerrim atë duke përdorur këtë çelës:

-- формируем целевую выборку
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;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN
[shikoni në shpjegojnë.tensor.ru]

U desh kohë për të mbledhur informacion në lidhje me individët 2 herë më pak kohë dhe 7 herë më pak të dhëna të lexuaraPërveç "fjalorizimit", na ndihmuan edhe për të arritur këto rezultate nxjerrja e të dhënave në masë nga tabela me një kalim të vetëm duke përdorur = ANY(ARRAY(...)).

Hyrjet në tabelë: serializimi dhe deserializimi

Po sikur të na duhet të ruajmë jo vetëm një fushë teksti, por një regjistrim të tërë në fjalor? Në këtë rast, aftësia e PostgreSQL për të trajtoni një hyrje në tabelë si një vlerë të vetme:

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

Le të kuptojmë se çfarë po ndodhte këtu:

  1. Ne morëm p si një pseudonim për regjistrimin e plotë të tabelës së personave dhe mblodhi një grup prej tyre.
  2. kjo grupi i të dhënave u rimodelua në një varg vargjesh teksti (person[]::text[]) për ta vendosur atë në fjalorin hstore si një varg vlerash.
  3. Kur marrim një regjistrim përkatës, ne u nxor nga fjalori me anë të çelësit si një varg teksti.
  4. Na duhet teksti konverto në vlerën e tipit tabelë person (për secilën tabelë krijohet automatikisht një lloj me të njëjtin emër).
  5. "Zgjerova" regjistrimin e shtypur në kolona duke përdorur (...).*.

fjalor json

Por truku që përdorëm më sipër nuk do të funksionojë nëse nuk ka një lloj tabele përkatëse për të kryer "çaktivizimin". Pikërisht e njëjta situatë do të lindë nëse përpiqemi të përdorim [të dhënat] si burim të dhënash për serializim. Rreshti CTE, jo tabela "e vërtetë".

Në këtë rast, ata do të na ndihmojnë funksione për të punuar me JSON:

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

Vlen të përmendet se kur përshkruajmë strukturën e synuar, nuk kemi nevojë të rendisim të gjitha fushat në rreshtin burimor, por vetëm ato që na nevojiten vërtet. Nëse kemi një tabelë vendase, është më mirë të përdorim funksionin json_populate_record.

Ne ende kemi qasje një herëshe në fjalor, por Kostoja e JSON-[de]serializimit është mjaft e lartë, kështu që ka kuptim ta përdorim këtë metodë vetëm në disa raste kur skanimi "i ndershëm" CTE ka performancë më të keqe.

Testimi i performancës

Pra, kemi dy mënyra për të serializuar të dhënat në një fjalor - hstore/json_objectPërveç kësaj, vargjet e çelësave dhe vlerave vetë mund të gjenerohen gjithashtu në dy mënyra, me konvertim të brendshëm ose të jashtëm në tekst: array_agg(i::tekst) / array_agg(i)::tekst[].

Le të testojmë efektivitetin e llojeve të ndryshme të serializimit duke përdorur një shembull thjesht sintetik - serializoni një numër të ndryshëm çelësash:

WITH dict AS (
  SELECT
    hstore(
      array_agg(i::text)
    , array_agg(i::text)
    )
  FROM
    generate_series(1, ...) i
)
TABLE dict;

Skripti i Vlerësimit: Serializimi

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;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Në PostgreSQL 11, deri në një madhësi fjalori prej afërsisht 2^12 çelësash Serializimi në JSON kërkon më pak kohëNë këtë rast, kombinimi më efektiv është json_object dhe konvertimi i tipit "i brendshëm". array_agg(i::text).

Tani le të përpiqemi ta lexojmë kuptimin e secilit çelës 8 herë - në fund të fundit, nëse nuk konsultoheni me një fjalor, atëherë çfarë kuptimi ka kjo?

Skript vlerësimi: lexim nga një fjalor

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;

PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN

Dhe... tashmë afërsisht me 2^6 çelësa, leximi nga një fjalor json fillon të humbasë shumëfisha Duke lexuar nga hstore, për jsonb e njëjta gjë ndodh në 2^9.

Përfundimet përfundimtare:

  • nëse duhet të bëhet JOIN me të dhëna të shumëfishta të dyfishta — është më mirë të përdoret "fjalori" i tabelës
  • nëse fjalori juaj pritet Është e vogël dhe nuk do të mund të lexosh shumë prej saj — mund të përdorni json[b]
  • në të gjitha rastet e tjera hstore + array_agg(i::text) do të jetë më efektive

Burimi: www.habr.com

Bleni një host të besueshëm për faqet me mbrojtje DDoS, serverë VPS VDS 🔥 Bleni hosting të besueshëm të faqeve të internetit me mbrojtje DDoS, servera VPS VDS | ProHoster