sql >> Databáze >  >> RDS >> PostgreSQL

Optimalizujte dotaz GROUP BY pro načtení nejnovějšího řádku na uživatele

Pro nejlepší výkon při čtení potřebujete vícesloupcový index:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Chcete-li provést pouze prohledávání indexu je to možné, přidejte jinak nepotřebný sloupec payload v krycím rejstříku s INCLUDE klauzule (Postgres 11 nebo novější):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Viz:

  • Pomáhá zakrytí indexů v PostgreSQL PŘIPOJENÍ sloupců?

Záloha pro starší verze:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Proč DESC NULLS LAST ?

  • Nepoužitý index v dotazu na rozsah dat

Pro málo řádků na user_id nebo malé tabulky DISTINCT ON je obvykle nejrychlejší a nejjednodušší:

  • Vybrat první řádek v každé skupině GROUP BY?

Pro mnoho řádků na user_id skenování přeskočení indexu (nebo uvolněný index skenování ) je (mnohem) efektivnější. To není implementováno až do Postgres 12 – práce na Postgres 14 pokračují. Existují však způsoby, jak to efektivně napodobit.

Běžné tabulkové výrazy vyžadují Postgres 8.4+ .
LATERAL vyžaduje Postgres 9.3+ .
Následující řešení jdou nad rámec toho, co je zahrnuto v Postgres Wiki .

1. Žádná samostatná tabulka s jedinečnými uživateli

Se samostatnými users tabulka, řešení v 2. níže jsou obvykle jednodušší a rychlejší. Přeskočit dopředu.

1a. Rekurzivní CTE s LATERAL připojit se

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Toto je snadné načíst libovolné sloupce a pravděpodobně nejlepší v současném Postgresu. Další vysvětlení v kapitole 2a. níže.

1b. Rekurzivní CTE s korelovaným poddotazem

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Pohodlné načtení jednoho sloupce nebo celý řádek . V příkladu je použit celý typ řádku tabulky. Jiné varianty jsou možné.

Chcete-li potvrdit, že řádek byl nalezen v předchozí iteraci, otestujte jeden sloupec NOT NULL (jako primární klíč).

Další vysvětlení tohoto dotazu v kapitole 2b. níže.

Související:

  • Dotaz na posledních N souvisejících řádků na řádek
  • GROUP BY jeden sloupec a řazení podle jiného v PostgreSQL

2. Se samostatnými users tabulka

Rozvržení tabulky stěží záleží, pokud přesně jeden řádek na relevantní user_id je zaručena. Příklad:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

V ideálním případě je tabulka fyzicky řazena synchronizovaně s log stůl. Viz:

  • Optimalizujte rozsah dotazu časového razítka Postgres

Nebo je dostatečně malý (nízká mohutnost), že na něm téměř nezáleží. Jinak může řazení řádků v dotazu pomoci k další optimalizaci výkonu. Viz dodatek Gang Liang. Pokud je fyzické řazení users tabulka náhodou odpovídá indexu v log , může to být irelevantní.

2a. LATERAL připojit se

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL umožňuje odkazovat na předchozí FROM položky na stejné úrovni dotazu. Viz:

  • Jaký je rozdíl mezi LATERAL JOIN a dílčím dotazem v PostgreSQL?

Výsledkem je jedno vyhledávání (pouze) indexu na uživatele.

Nevrací žádný řádek pro uživatele, kteří chybí v users stůl. Obvykle cizí klíč referenční integrita vynucující omezení by to vyloučila.

Také žádný řádek pro uživatele bez odpovídající položky v log - v souladu s původní otázkou. Chcete-li tyto uživatele udržet ve výsledku, použijte LEFT JOIN LATERAL ... ON true namísto CROSS JOIN LATERAL :

  • Volejte funkci vracející sadu s argumentem pole vícekrát

Použijte LIMIT n místo LIMIT 1 k načtení více než jednoho řádku (ale ne všechny) na uživatele.

Ve skutečnosti všechny tyto dělají totéž:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Poslední má však nižší prioritu. Explicitní JOIN váže před čárkou. Tento jemný rozdíl může mít význam u více spojovacích tabulek. Viz:

  • "neplatný odkaz na záznam klauzule FROM pro tabulku" v dotazu Postgres

2b. Korelovaný poddotaz

Dobrá volba pro načtení jednoho sloupce z jedné řady . Příklad kódu:

  • Optimalizujte maximální počet dotazů ve skupině

Totéž je možné pro více sloupců , ale potřebujete více chytrosti:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Jako LEFT JOIN LATERAL výše, tato varianta zahrnuje vše uživatelů, a to i bez záznamů v log . Získáte NULL pro combo1 , které můžete snadno filtrovat pomocí WHERE klauzule v případě potřeby ve vnějším dotazu.
Nitpick:ve vnějším dotazu nemůžete rozlišit, zda poddotaz nenašel řádek nebo všechny hodnoty sloupců jsou náhodou NULL - stejný výsledek. Potřebujete NOT NULL v poddotazu, abyste se vyhnuli této nejednoznačnosti.

Korelovaný dílčí dotaz může vrátit pouze jedinou hodnotu . Do složeného typu můžete zabalit více sloupců. Ale aby se to později rozložilo, Postgres požaduje známý kompozitní typ. Anonymní záznamy lze rozložit pouze pomocí seznamu definic sloupců.
Použijte registrovaný typ, jako je typ řádku existující tabulky. Nebo zaregistrujte složený typ explicitně (a trvale) pomocí CREATE TYPE . Nebo vytvořte dočasnou tabulku (automaticky zrušenou na konci relace), abyste dočasně zaregistrovali její typ řádku. Syntaxe Cast:(log_date, payload)::combo

Nakonec nechceme rozkládat combo1 na stejné úrovni dotazu. Kvůli slabosti v plánovači dotazů by to vyhodnotilo poddotaz jednou pro každý sloupec (stále platí v Postgres 12). Místo toho z něj udělejte dílčí dotaz a rozložte jej ve vnějším dotazu.

Související:

  • Získejte hodnoty z prvního a posledního řádku na skupinu

Ukázka všech 4 dotazů se 100 000 záznamy v protokolu a 1 000 uživateli:
db<>zde si pohrajte - str. 11
Staré sqlfiddle



  1. Zdroje clusteru Galera

  2. Jak používat připravené příkazy mysqli?

  3. Sledování aktualizací synchronních statistik

  4. Audit v Oracle