För bästa läsprestanda behöver du ett index med flera kolumner:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
För att göra endast indexsökningar möjligt, lägg till kolumnen payload
som annars inte behövs i ett täckande index med INCLUDE
klausul (Postgres 11 eller senare):
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Se:
- Hjälper det att täcka index i PostgreSQL JOIN-kolumner?
Reserv för äldre versioner:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Varför DESC NULLS LAST
?
- Oanvänt index inom datumintervallfrågan
För få rader per user_id
eller små tabeller DISTINCT ON
är vanligtvis snabbast och enklast:
- Välj första raden i varje GROUP BY-grupp?
För många rader per user_id
en indexhoppningssökning (eller lös indexskanning ) är (mycket) effektivare. Det är inte implementerat fram till Postgres 12 - arbetet pågår för Postgres 14. Men det finns sätt att efterlikna det effektivt.
Vanliga tabelluttryck kräver Postgres 8.4+ .LATERAL
kräver Postgres 9.3+ .
Följande lösningar går utöver vad som tas upp i Postgres Wiki .
1. Ingen separat tabell med unika användare
Med en separat users
tabell, lösningar i 2. nedan är vanligtvis enklare och snabbare. Hoppa vidare.
1a. Rekursiv CTE med LATERAL
gå med
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;
Detta är enkelt att hämta godtyckliga kolumner och förmodligen bäst i nuvarande Postgres. Mer förklaring i kapitel 2a. nedan.
1b. Rekursiv CTE med korrelerad underfråga
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;
Bekvämt att hämta en enkel kolumn eller hela raden . I exemplet används tabellens hela radtyp. Andra varianter är möjliga.
För att hävda att en rad hittades i föregående iteration, testa en enda INTE NULL-kolumn (som primärnyckeln).
Mer förklaring till denna fråga i kapitel 2b. nedan.
Relaterat:
- Fråga de sista N relaterade raderna per rad
- GRUPPERA EFTER en kolumn, medan du sorterar efter en annan i PostgreSQL
2. Med separata users
tabell
Tabelllayout spelar knappast någon roll så länge som exakt en rad per relevant user_id
är garanterad. Exempel:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
Helst är tabellen fysiskt sorterad i synk med log
tabell. Se:
- Optimera Postgres tidsstämpelfrågeintervall
Eller så är den tillräckligt liten (låg kardinalitet) att det knappast spelar någon roll. Annars kan sortering av rader i frågan hjälpa till att optimera prestandan ytterligare. Se Gang Liangs tillägg. Om den fysiska sorteringsordningen för users
Tabellen råkar matcha indexet på log
, detta kan vara irrelevant.
2a. LATERAL
gå med
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
tillåter att referera till föregående FROM
objekt på samma frågenivå. Se:
- Vad är skillnaden mellan LATERAL JOIN och en underfråga i PostgreSQL?
Resulterar i en indexuppslagning (endast) per användare.
Returnerar ingen rad för användare som saknas i users
tabell. Vanligtvis en främmande nyckel begränsning som upprätthåller referensintegritet skulle utesluta det.
Dessutom, ingen rad för användare utan matchande post i log
- överensstämmer med den ursprungliga frågan. För att behålla dessa användare i resultatet använd LEFT JOIN LATERAL ... ON true
istället för CROSS JOIN LATERAL
:
- Anropa en setreturerande funktion med ett matrisargument flera gånger
Använd LIMIT n
istället för LIMIT 1
för att hämta mer än en rad (men inte alla) per användare.
Alla dessa gör faktiskt samma sak:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
Den sista har dock lägre prioritet. Explicit JOIN
binder före komma. Den subtila skillnaden kan ha betydelse med fler sammanfogningsbord. Se:
- "ogiltig referens till FROM-klausulpost för tabell" i Postgres-fråga
2b. Korrelerad underfråga
Bra val för att hämta en enkel kolumn från en en rad . Kodexempel:
- Optimera gruppvis maximal fråga
Detsamma är möjligt för flera kolumner , men du behöver mer smart:
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;
Som LEFT JOIN LATERAL
ovan inkluderar denna variant alla användare, även utan poster i log
. Du får NULL
för combo1
, som du enkelt kan filtrera med en WHERE
sats i den yttre frågan om det behövs.
Nitpick:i den yttre frågan kan du inte skilja på om underfrågan inte hittade en rad eller om alla kolumnvärden råkar vara NULL - samma resultat. Du behöver en NOT NULL
kolumnen i underfrågan för att undvika denna oklarhet.
En korrelerad underfråga kan bara returnera ett enkelt värde . Du kan slå in flera kolumner till en sammansatt typ. Men för att bryta ner det senare kräver Postgres en välkänd komposittyp. Anonyma poster kan endast dekomponeras med en kolumndefinitionslista.
Använd en registrerad typ som radtypen för en befintlig tabell. Eller registrera en sammansatt typ explicit (och permanent) med CREATE TYPE
. Eller skapa en tillfällig tabell (släpps automatiskt i slutet av sessionen) för att tillfälligt registrera dess radtyp. Cast-syntax:(log_date, payload)::combo
Slutligen vill vi inte dekomponera combo1
på samma frågenivå. På grund av en svaghet i frågeplaneraren skulle detta utvärdera underfrågan en gång för varje kolumn (fortfarande sant i Postgres 12). Gör det istället till en underfråga och dekomponera i den yttre frågan.
Relaterat:
- Hämta värden från första och sista raden per grupp
Demonstrerar alla fyra frågorna med 100 000 loggposter och 1 000 användare:
db<>spela här - sid 11
Gammal sqlfiddle