Voor de beste leesprestaties heeft u een index met meerdere kolommen nodig:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
Om alleen index-scans te maken mogelijk, voeg de anders niet benodigde kolom toe payload
in een dekkende index met de INCLUDE
clausule (Postgres 11 of later):
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Zie:
- Helpen indexen in PostgreSQL om JOIN-kolommen te gebruiken?
Terugval voor oudere versies:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Waarom DESC NULLS LAST
?
- Ongebruikte index in zoekopdracht voor datumbereik
Voor enkele rijen per user_id
of kleine tabellen DISTINCT ON
is doorgaans het snelst en eenvoudigst:
- Selecteer de eerste rij in elke GROUP BY-groep?
Voor velen rijen per user_id
een index overslaan scan (of losse indexscan ) is (veel) efficiënter. Dat is niet geïmplementeerd tot Postgres 12 - er wordt gewerkt aan Postgres 14. Maar er zijn manieren om het efficiënt te emuleren.
Voor algemene tabeluitdrukkingen is Postgres 8.4+ . vereist .LATERAL
vereist Postgres 9.3+ .
De volgende oplossingen gaan verder dan wat wordt behandeld in de Postgres Wiki .
1. Geen aparte tabel met unieke gebruikers
Met een aparte users
tabel, oplossingen in 2. hieronder zijn doorgaans eenvoudiger en sneller. Ga vooruit.
1a. Recursieve CTE met LATERAL
doe mee
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;
Dit is eenvoudig om willekeurige kolommen op te halen en waarschijnlijk het beste in het huidige Postgres. Meer uitleg in hoofdstuk 2a. hieronder.
1b. Recursieve CTE met gecorreleerde subquery
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;
Handig om een enkele kolom op te halen of de hele rij . In het voorbeeld wordt het hele rijtype van de tabel gebruikt. Andere varianten zijn mogelijk.
Om te bevestigen dat een rij is gevonden in de vorige iteratie, test u een enkele NOT NULL-kolom (zoals de primaire sleutel).
Meer uitleg voor deze vraag in hoofdstuk 2b. hieronder.
Gerelateerd:
- Bezoek laatste N gerelateerde rijen per rij
- GROEPEREN OP één kolom, sorteren op een andere in PostgreSQL
2. Met aparte users
tafel
Tabellay-out doet er nauwelijks toe, zolang er maar één rij per relevante user_id
is is gegarandeerd. Voorbeeld:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
Idealiter wordt de tabel fysiek gesorteerd synchroon met het log
tafel. Zie:
- Optimaliseer het zoekopdrachtbereik van Postgres-tijdstempel
Of het is klein genoeg (lage kardinaliteit) dat het er nauwelijks toe doet. Anders kan het sorteren van rijen in de query helpen om de prestaties verder te optimaliseren. Zie de toevoeging van Gang Liang. Als de fysieke sorteervolgorde van de users
tabel komt toevallig overeen met de index op log
, is dit misschien niet relevant.
2a. LATERAL
doe mee
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
staat toe om te verwijzen naar voorafgaande FROM
items op hetzelfde zoekniveau. Zie:
- Wat is het verschil tussen LATERAL JOIN en een subquery in PostgreSQL?
Resulteert in één index (-only) look-up per gebruiker.
Retourneert geen rij voor gebruikers die ontbreken in de users
tafel. Meestal een buitenlandse sleutel beperking die referentiële integriteit afdwingt, zou dat uitsluiten.
Ook geen rij voor gebruikers zonder overeenkomende invoer in log
- conform de oorspronkelijke vraag. Om die gebruikers in het resultaat te houden, gebruikt u LEFT JOIN LATERAL ... ON true
in plaats van CROSS JOIN LATERAL
:
- Een set-retournerende functie met een array-argument meerdere keren aanroepen
Gebruik LIMIT n
in plaats van LIMIT 1
om meer dan één rij op te halen (maar niet alle) per gebruiker.
In feite doen deze allemaal hetzelfde:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
De laatste heeft echter een lagere prioriteit. Expliciete JOIN
bindt voor de komma. Dat subtiele verschil kan van belang zijn met meer samenvoegtabellen. Zie:
- "ongeldige verwijzing naar FROM-clausule voor tabel" in Postgres-query
2b. Gecorreleerde subquery
Goede keuze om een enkele kolom op te halen uit een enkele rij . Codevoorbeeld:
- Optimaliseer groepsgewijze maximale zoekopdracht
Hetzelfde is mogelijk voor meerdere kolommen , maar je hebt meer intelligentie nodig:
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;
Like LEFT JOIN LATERAL
hierboven bevat deze variant alle gebruikers, zelfs zonder vermeldingen in log
. Je krijgt NULL
voor combo1
, die u eenvoudig kunt filteren met een WHERE
clausule in de buitenste query indien nodig.
Nitpick:in de buitenste query kunt u niet onderscheiden of de subquery geen rij heeft gevonden of dat alle kolomwaarden NULL zijn - hetzelfde resultaat. Je hebt een NOT NULL
nodig kolom in de subquery om deze dubbelzinnigheid te voorkomen.
Een gecorreleerde subquery kan slechts een enkele waarde retourneren . U kunt meerdere kolommen in een samengesteld type inpakken. Maar om het later te ontleden, eist Postgres een bekend composiettype. Anonieme records kunnen alleen worden ontleed door een lijst met kolomdefinities te geven.
Gebruik een geregistreerd type zoals het rijtype van een bestaande tabel. Of registreer een samengesteld type expliciet (en permanent) met CREATE TYPE
. Of maak een tijdelijke tabel (automatisch verwijderd aan het einde van de sessie) om het rijtype tijdelijk te registreren. Cast-syntaxis:(log_date, payload)::combo
Ten slotte willen we combo1
niet ontleden op hetzelfde zoekniveau. Vanwege een zwakte in de queryplanner zou dit de subquery één keer evalueren voor elke kolom (nog steeds waar in Postgres 12). Maak er in plaats daarvan een subquery van en ontbind het in de buitenste query.
Gerelateerd:
- Krijg waarden van de eerste en laatste rij per groep
Demonstratie van alle 4 zoekopdrachten met 100.000 logboekvermeldingen en 1k gebruikers:
db<>fiddle here - pg 11
Oude sqlfiddle