PostgreSQL Antipatterns: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral

Folytatjuk egy sor cikket, amelyek az ismeretlen módszerek teljesítményjavítåsåra összpontosítanak a "låtszólag egyszerƱ" PostgreSQL lekérdezések esetében:

Ne gondolja, hogy annyira nem szeretem a JOIN-t... 🙂

De sokszor a lekĂ©rdezĂ©s jelentƑsen teljesĂ­tmĂ©nyesebb, ha elkerĂŒljĂŒk. EzĂ©rt ma megprĂłbĂĄljuk egyĂĄltalĂĄn megszabadulni a nagy erƑforrĂĄst igĂ©nylƑ JOIN-tĂłl — egy szĂłtĂĄr segĂ­tsĂ©gĂ©vel.

PostgreSQL Antipatterns: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral

A PostgreSQL 12-tƑl kezdƑdƑen a lenti szituĂĄciĂłk egy rĂ©sze kissĂ© mĂĄskĂ©pp jelentkezhet a CTE alapĂ©rtelmezĂ©s szerint nem anyagiasĂ­tĂĄsĂĄnak köszönhetƑen. Ez a viselkedĂ©s visszaĂĄllĂ­thatĂł a korĂĄbbi ĂĄllapotba a kulcs megadĂĄsĂĄval MATERIALIZED.

Sok "tény" egy korlåtozott szótår miatt

NĂ©zzĂŒnk egy teljesen valĂłs alkalmazĂĄsi feladatot — ki kell listĂĄznunk a bejövƑ ĂŒzeneteket vagy aktĂ­v feladatokat a kĂŒldƑkkel:

25.01 | Ivanov I.I. | Új algoritmus leĂ­rĂĄsĂĄnak elƑkĂ©szĂ­tĂ©se.
22.01 | Ivanov I.I. | Cikk Ă­rĂĄsa a Habr-ra: Ă©let JOIN nĂ©lkĂŒl.
20.01 | Petrov P.P. | Segítség a lekérdezés optimalizålåsåban.
18.01 | Ivanov I.I. | Cikk íråsa a Habr-ra: JOIN a dåtumok eloszlåsånak figyelembevételével.
16.01 | Petrov P.P. | Segítség a lekérdezés optimalizålåsåban.

Absztrakt vilĂĄgban a feladatok Ă­rĂłinak egyenletesen kellene eloszlaniuk a szervezet összes munkatĂĄrsa között, de a valĂłsĂĄgban a feladatok ĂĄltalĂĄban egy meglehetƑsen korlĂĄtozott szĂĄmĂș embertƑl Ă©rkeznek — "a fƑnöksĂ©gtƑl" felfelĂ© a hierarchiĂĄban, vagy "a szomszĂ©dos bokor" munkatĂĄrsaitĂłl (elemzƑk, tervezƑk, marketing, 
).

TegyĂŒk fel, hogy a szervezetĂŒnkben 1000 ember közĂŒl csak 20 Ă­rĂł (ĂĄltalĂĄban mĂ©g kevesebb) ad feladatokat minden egyes konkrĂ©t vĂ©grehajtĂłnak Ă©s hasznĂĄljuk ezt a szaktudĂĄst, hogy felgyorsĂ­tsuk a "hagyomĂĄnyos" lekĂ©rdezĂ©st.

Script-generĂĄtor

-- munkatĂĄrsak
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);

-- feladatok a megadott eloszlĂĄssal
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);

Mutassuk meg az utolsó 100 feladatot egy adott végrehajtónak:

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: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral
[megtekintés az explain.tensor.ru-n]

EzĂ©rt a 1/3 az egĂ©sz idƑbƑl Ă©s 3/4 az olvasott adatokbĂłl az adatlapok csak azĂ©rt kĂ©szĂŒltek el, hogy 100 alkalommal keressĂŒk meg a szerzƑt — minden megjelenĂ­tett feladathoz. De tudjuk, hogy ebbƑl a szĂĄzbĂłl csak 20 kĂŒlönbözƑ — nem lehetne ezt a tudĂĄst kihasznĂĄlni?

hstore-szĂłtĂĄr

Hasznåljuk ki az hstore típust kulcs-érték "szótår" generålåsåhoz:

CREATE EXTENSION hstore

A szĂłtĂĄrba elegendƑ lesz beletenni a szerzƑ ID-jĂĄt Ă©s a nevĂ©t, Ă­gy meg tudjuk majd keresni ezen a kulcson:

-- a célzott kivålasztås kialakítåsa
WITH T AS (
  SELECT
    *
  FROM
    task
  WHERE
    owner_id = 777
  ORDER BY
    task_date DESC
  LIMIT 100
)
-- a szótår kialakítåsa az egyedi értékekhez
, 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
    ))
)
-- a szótår kapcsolt értékeinek lekérdezése
SELECT
  *
, (TABLE dict) -> author_id::text -- hstore -> key
FROM
  T;

PostgreSQL Antipatterns: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral
[megtekintés az explain.tensor.ru-n]

Az informĂĄciĂłk megszerzĂ©se a szemĂ©lyekrƑl kĂ©t alkalommal kevesebb idƑt Ă©s hĂ©t alkalommal kevesebb adatot igĂ©nyelt! Az "szĂłtĂĄrazĂĄs" mellett ezeket az eredmĂ©nyeket a tömeges rekordlekĂ©rdezĂ©s segĂ­tsĂ©gĂ©vel is elĂ©rtĂŒk egyetlen ĂĄthaladĂĄs alatt = ANY(ARRAY(...)).

A tåblåzat bejegyzései: sorozatosítås és desorozås

De mi a teendƑ, ha nem csak egy szövegmezƑt, hanem egy teljes bejegyzĂ©st kell megƑriznĂŒnk a szĂłtĂĄrban? Ekkor PostgreSQL kĂ©pessĂ©ge segĂ­t, hogy a tĂĄblĂĄzat bejegyzĂ©sĂ©t egy egysĂ©ges Ă©rtĂ©kkĂ©nt kezeljĂŒk:

...
, dict AS (
  SELECT
    hstore(
      array_agg(id)::text[]
    , array_agg(p)::text[] -- mĂĄgia #1
    )
  FROM
    person p
  WHERE
    ...
)
SELECT
  *
, (((TABLE dict) -> author_id::text)::person).* -- mĂĄgia #2
FROM
  T;

NĂ©zzĂŒk meg, hogy itt tulajdonkĂ©ppen mi törtĂ©nik:

  1. VettĂŒk p-t, mint alias a person tĂĄblĂĄzat teljes bejegyzĂ©sĂ©re Ă©s összegyƱjtöttĂŒk Ƒket egy tömbbĂ©.
  2. Ez a tömb a rekordokat ĂĄtkonvertĂĄltuk szövegsorok tömbjĂ©vĂ© (person[]::text[]), hogy elhelyezzĂŒk Ƒket egy hstore szĂłtĂĄrban Ă©rtĂ©kek tömbjekĂ©nt.
  3. Kapcsolt rekord nyerĂ©sekor azt kihĂșztuk a szĂłtĂĄrbĂłl a kulcs alapjĂĄn mint szövegsort.
  4. A szöveget tĂĄblĂĄzattĂ­pusĂș Ă©rtĂ©kkĂ© kell alakĂ­tanunk a person (minden tĂĄblĂĄzathoz automatikusan lĂ©trejön ugyanaz a nevƱ tĂ­pus).
  5. A tĂ­pusĂș rekordokat „kiterjesztettĂŒk” az oszlopokra (...).*.

json szĂłtĂĄr

De az a trĂŒkk, amit fent alkalmaztunk, nem mƱködik, ha nincs megfelelƑ tĂĄblĂĄzatos tĂ­pus a „dekĂłdolĂĄshoz”. Ugyanez a helyzet akkor is, ha adatforrĂĄskĂ©nt CTE sort prĂłbĂĄlunk hasznĂĄlni, nem pedig „valĂłdi” tĂĄblĂĄt. Ebben az esetben segĂ­tenek nekĂŒnk.

a json kezelƑ funkciĂłk ... , p AS ( -- ez mĂĄr CTE SELECT * FROM person WHERE ... ) , dict AS ( SELECT json_object( -- most mĂĄr json array_agg(id)::text[] , array_agg(row_to_json(p))::text[] -- Ă©s a json belsejĂ©ben minden sor ) FROM p ) SELECT * FROM T , LATERAL( SELECT * FROM json_to_record( ((TABLE dict) ->> author_id::text)::json -- kinyertĂŒk a szĂłtĂĄrbĂłl jsonkĂ©nt ) AS j(name text, birth_date date) -- kitöltöttĂŒk a szĂŒksĂ©ges struktĂșrĂĄt ) j;:

Meg kell jegyeznĂŒnk, hogy a cĂ©lstruktĂșra leĂ­rĂĄsakor nem szĂŒksĂ©ges minden mezƑt felsorolnunk az eredeti sorbĂłl, csak azokat, amelyek valĂłban szĂŒksĂ©gesek. Ha azonban van „bennszĂŒlött” tĂĄblĂĄnk, akkor jobb, ha a

json_populate_record funkciĂłt hasznĂĄljuk..

A szĂłtĂĄrhoz valĂł hozzĂĄfĂ©rĂ©s tovĂĄbbra is egyszeri, de a json-[de]szerializĂĄlĂĄs költsĂ©gei elĂ©ggĂ© jelentƑsek, ezĂ©rt ilyen mĂłdon Ă©rdemes csak bizonyos esetekben hasznĂĄlni, amikor a „civilebb” CTE Scan gyengĂ©bben teljesĂ­t.

A teljesítmény tesztelése

TehĂĄt kĂ©t mĂłdot kaptunk az adatok szĂłtĂĄrba törtĂ©nƑ szerializĂĄlĂĄsĂĄra — hstore / json_object. Ezen kĂ­vĂŒl magukat a kulcsok Ă©s Ă©rtĂ©kek tömbjeit is kĂ©t mĂłdon lehet elƑállĂ­tani, belsƑ vagy kĂŒlsƑ szöveggĂ© alakĂ­tĂĄssal: array_agg(i::text) / array_agg(i)::text[].

EllenƑrizzĂŒk a kĂŒlönfĂ©le tĂ­pusĂș szerializĂĄciĂłk hatĂ©konysĂĄgĂĄt egy teljesen szintetikus pĂ©ldĂĄval — kĂŒlönbözƑ mennyisĂ©gƱ kulcsot szerializĂĄlva:

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

ÉrtĂ©kelƑ script: szerializĂĄlĂĄs

A T AS (
  KIVÁLASZTÁS
    *
  , (
      KIVÁLASZTÁS
        regexp_replace(ea[array_length(ea, 1)], '^VĂ©grehajtĂĄsi idƑ: (d+.d+) ms$', '1')::real et
      BƐVÍTÉS
        (
          KIVÁLASZTÁS
            array_agg(el) ea
          BƐVÍTÉS
            dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
              explain analyze
              A SZÓTÁRNÁL (
                KIVÁLASZTÁS
                  hstore(
                    array_agg(i::text)
                  , array_agg(i::text)
                  )
                BƐVÍTÉS
                  generate_series(1, $$ || (1 << v) || $$) i
              )
              TÁBLÁZAT dict
            $$) T(el text)
        ) T
    ) et
  BƐVÍTÉS
    generate_series(0, 19) v
  ,   LATERAL generate_series(1, 7) i
  RENDSZER SZERINT
    1, 2
)
KIVÁLASZTÁS
  v
, avg(et)::numeric(32,3)
BƐVÍTÉS
  T
GROUP BY
  1
RENDSZER SZERINT
  1;

PostgreSQL Antipatterns: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral

A PostgreSQL 11 körĂŒl a szĂłtĂĄr mĂ©retĂ©ig 2^12 kulcsig a json-be valĂł sorosĂ­tĂĄs kevesebb idƑt igĂ©nyel. A legnagyobb hatĂ©konysĂĄgot a json_object Ă©s az „belsƑ” tĂ­puskonverziĂł kombinĂĄciĂłja kĂ©pviseli array_agg(i::text).

Most prĂłbĂĄljuk meg minden kulcs Ă©rtĂ©kĂ©t nyolcszor kiolvasni — hiszen ha a szĂłtĂĄrhoz nem nyĂșlunk, akkor minek van szĂŒksĂ©g rĂĄ?

ÉrtĂ©kelƑ script: olvasĂĄs a szĂłtĂĄrbĂłl

A T AS (
  KIVÁLASZTÁS
    *
  , (
      KIVÁLASZTÁS
        regexp_replace(ea[array_length(ea, 1)], '^VĂ©grehajtĂĄsi idƑ: (d+.d+) ms$', '1')::real et
      BƐVÍTÉS
        (
          KIVÁLASZTÁS
            array_agg(el) ea
          BƐVÍTÉS
            dblink('port= ' || current_setting('port') || ' dbname=' || current_database(), $$
              explain analyze
              A SZÓTÁRNÁL (
                KIVÁLASZTÁS
                  json_object(
                    array_agg(i::text)
                  , array_agg(i::text)
                  )
                BƐVÍTÉS
                  generate_series(1, $$ || (1 << v) || $$) i
              )
              KIVÁLASZTÁS
                (TABLE dict) -> (i % ($$ || (1 << v) || $$) + 1)::text
              BƐVÍTÉS
                generate_series(1, $$ || (1 << (v + 3)) || $$) i
            $$) T(el text)
        ) T
    ) et
  BƐVÍTÉS
    generate_series(0, 19) v
  , LATERAL generate_series(1, 7) i
  RENDSZER SZERINT
    1, 2
)
KIVÁLASZTÁS
  v
, avg(et)::numeric(32,3)
BƐVÍTÉS
  T
GROUP BY
  1
RENDSZER SZERINT
  1;

PostgreSQL Antipatterns: ĂŒssĂŒk le a nehĂ©z JOIN-t egy szĂłtĂĄrral

ÉS
 mĂĄr körĂŒlbelĂŒl 2^6 kulcsnĂĄl a json-szĂłtĂĄrbĂłl valĂł olvasĂĄs több mint többszörös hĂĄtrĂĄnyba kerĂŒl az hstore-bĂłl valĂł olvasĂĄshoz, a jsonb esetĂ©ben ugyanez törtĂ©nik 2^9-kor.

VĂ©gsƑ következtetĂ©sek:

  • ha szĂŒksĂ©ges JOIN többször ismĂ©tlƑdƑ rekordokkal — jobb a tĂĄblĂĄzat „szĂłtĂĄrba helyezĂ©se”
  • ha a szĂłtĂĄr vĂĄrhatĂłan kicsi, Ă©s keveset olvasol belƑle — hasznĂĄlhatsz json[b]-t
  • minden mĂĄs esetben hstore + array_agg(i::text) hatĂ©konyabb

ForrĂĄs: habr.com

MegbĂ­zhatĂł hosting vĂĄsĂĄrlĂĄsa weboldalak szĂĄmĂĄra, DDoS vĂ©delemmel, VPS VDS szerverek. đŸ”„ MegbĂ­zhatĂł webtĂĄrhely vĂĄsĂĄrlĂĄsa DDoS vĂ©delemmel, VPS VDS szerverek | ProHoster