sql >> Databasteknik >  >> RDS >> PostgreSQL

Optimera GROUP BY-fråga för att hämta den senaste raden per användare

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 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



  1. Hur man skapar en främmande nyckel i SQL Server (T-SQL-exempel)

  2. Hur man skapar vy i PostgreSQL

  3. Säkerhetskopiera en SQLite-databas

  4. 4 viktiga databasövervakningsaktiviteter som varje DBA bör känna till