sql >> Database teknologi >  >> RDS >> PostgreSQL

Optimer GROUP BY-forespørgsel for at hente seneste række pr. bruger

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



  1. Hvordan vælger jeg mellem den 1. dag i den aktuelle måned og den aktuelle dag i MySQL?

  2. Vis alle forespørgsler, der kommer til en Oracle-database

  3. Generer datointerval fra to datokolonner

  4. MySQL DROP UDENLANDSKE NØGLE-begrænsning