sql >> Database >  >> RDS >> PostgreSQL

Optimaliseer de GROUP BY-query om de laatste rij per gebruiker op te halen

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



  1. Op zoek naar snelle lokale opslag

  2. Multi-statement Table Valued Function vs Inline Table Valued Function

  3. MySql Error 150 - Buitenlandse sleutels

  4. Retourneer een lijst met tabellen en weergaven in SQL Server met behulp van T-SQL (sp_tables)