For den bedste læseydelse skal du bruge et indeks med flere kolonner:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
For at lave kun indeksscanninger muligt, tilføje den ellers ikke nødvendige kolonne payload
i et dækkende indeks med INCLUDE
klausul (Postgres 11 eller senere):
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Se:
- Hjælper dækning af indekser i PostgreSQL JOIN-kolonner?
Fallback for ældre versioner:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Hvorfor DESC NULLS LAST
?
- Ubrugt indeks inden for datointerval forespørgsel
For få rækker pr. user_id
eller små tabeller DISTINCT ON
er typisk hurtigst og enklest:
- Vælg første række i hver GROUP BY-gruppe?
For mange rækker pr. user_id
en indeksspringsscanning (eller løs indeksscanning ) er (meget) mere effektiv. Det er ikke implementeret op til Postgres 12 - arbejdet er i gang med Postgres 14. Men der er måder at efterligne det effektivt.
Almindelige tabeludtryk kræver Postgres 8.4+ .LATERAL
kræver Postgres 9.3+ .
Følgende løsninger går ud over, hvad der er dækket i Postgres Wiki .
1. Ingen separat tabel med unikke brugere
Med en separat users
tabel, løsninger i 2. nedenfor er typisk enklere og hurtigere. Spring videre.
1a. Rekursiv CTE med LATERAL
deltage
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;
Dette er nemt at hente vilkårlige kolonner og sandsynligvis bedst i nuværende Postgres. Mere forklaring i kapitel 2a. nedenfor.
1b. Rekursiv CTE med korreleret underforespørgsel
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;
Praktisk at hente en enkelt kolonne eller hele rækken . Eksemplet bruger hele tabellens rækketype. Andre varianter er mulige.
For at hævde, at en række blev fundet i den forrige iteration, skal du teste en enkelt IKKE NULL-kolonne (som den primære nøgle).
Mere forklaring på denne forespørgsel i kapitel 2b. nedenfor.
Relateret:
- Forespørg på de sidste N relaterede rækker pr. række
- GRUPPER EFTER én kolonne, mens du sorterer efter en anden i PostgreSQL
2. Med separate users
tabel
Tabellayout betyder næppe noget, så længe præcis én række pr. relevant user_id
er garanteret. Eksempel:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
Ideelt set er tabellen fysisk sorteret synkroniseret med log
bord. Se:
- Optimer Postgres tidsstempelforespørgselsinterval
Eller den er lille nok (lav kardinalitet), at det næsten ikke betyder noget. Ellers kan sortering af rækker i forespørgslen hjælpe med at optimere ydeevnen yderligere. Se Gang Liangs tilføjelse. Hvis den fysiske sorteringsrækkefølge for users
tabel matcher tilfældigvis indekset på log
, dette kan være irrelevant.
2a. LATERAL
deltage
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
giver mulighed for at henvise til foran FROM
elementer på samme forespørgselsniveau. Se:
- Hvad er forskellen mellem LATERAL JOIN og en underforespørgsel i PostgreSQL?
Resulterer i ét (kun) indeksopslag pr. bruger.
Returnerer ingen række for brugere, der mangler i users
bord. Typisk en fremmednøgle begrænsninger, der håndhæver referentiel integritet, ville udelukke det.
Der er heller ingen række for brugere uden matchende indtastning i log
- i overensstemmelse med det oprindelige spørgsmål. For at beholde disse brugere i resultatet, brug LEFT JOIN LATERAL ... ON true
i stedet for CROSS JOIN LATERAL
:
- Kald en sæt-returnerende funktion med et array-argument flere gange
Brug LIMIT n
i stedet for LIMIT 1
for at hente mere end én række (men ikke alle) pr. bruger.
Faktisk gør alle disse det samme:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
Den sidste har dog lavere prioritet. Eksplicit JOIN
binder før komma. Den subtile forskel kan have betydning med flere jointabeller. Se:
- "ugyldig reference til FROM-klausulindtastning for tabel" i Postgres-forespørgsel
2b. Korreleret underforespørgsel
Godt valg til at hente en enkelt kolonne fra en enkelt række . Kodeeksempel:
- Optimer gruppevis maksimal forespørgsel
Det samme er muligt for flere kolonner , men du har brug for mere 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;
Ligesom LEFT JOIN LATERAL
ovenfor inkluderer denne variant alle brugere, selv uden indtastninger i log
. Du får NULL
for combo1
, som du nemt kan filtrere med en WHERE
klausul i den ydre forespørgsel, hvis det er nødvendigt.
Nitpick:i den ydre forespørgsel kan du ikke skelne mellem, om underforespørgslen ikke fandt en række, eller om alle kolonneværdier tilfældigvis er NULL - samme resultat. Du skal bruge en NOT NULL
kolonne i underforespørgslen for at undgå denne tvetydighed.
En korreleret underforespørgsel kan kun returnere en enkelt værdi . Du kan ombryde flere kolonner til en sammensat type. Men for at nedbryde det senere efterspørger Postgres en velkendt komposittype. Anonyme poster kan kun dekomponeres med en kolonnedefinitionsliste.
Brug en registreret type som rækketypen i en eksisterende tabel. Eller registrer en sammensat type eksplicit (og permanent) med CREATE TYPE
. Eller opret en midlertidig tabel (falder automatisk ved slutningen af sessionen) for at registrere dens rækketype midlertidigt. Cast-syntaks:(log_date, payload)::combo
Endelig ønsker vi ikke at dekomponere combo1
på samme forespørgselsniveau. På grund af en svaghed i forespørgselsplanlæggeren ville dette evaluere underforespørgslen én gang for hver kolonne (stadig sandt i Postgres 12). Gør det i stedet til en underforespørgsel og dekomponér i den ydre forespørgsel.
Relateret:
- Få værdier fra første og sidste række pr. gruppe
Demonstrerer alle 4 forespørgsler med 100.000 logposter og 1.000 brugere:
db<>spil her - side 11
Gamle sqlfiddle