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