PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Zoptymalizuj zapytanie GROUP BY, aby pobrać ostatni wiersz na użytkownika

Aby uzyskać najlepszą wydajność odczytu, potrzebujesz indeksu wielokolumnowego:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Aby wykonać skanowanie tylko indeksujące to możliwe, dodaj niepotrzebną kolumnę payload w indeksie obejmującym z INCLUDE klauzula (Postgres 11 lub nowszy):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Zobacz:

  • Czy pokrywanie indeksów w PostgreSQL pomaga w JOIN kolumnach?

Powrót do starszych wersji:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Dlaczego DESC NULLS LAST ?

  • Nieużywany indeks w zapytaniu zakresu dat

Dla niewielu wierszy na user_id lub małe tabele DISTINCT ON jest zazwyczaj najszybszy i najprostszy:

  • Wybrać pierwszy wiersz w każdej grupie GROUP BY?

Dla wielu wierszy na user_id pomijanie skanowania indeksu (lub luźne skanowanie indeksu ) jest (znacznie) wydajniejszy. Nie jest to zaimplementowane do Postgresa 12 — prace nad Postgresem 14 trwają. Istnieją jednak sposoby na jego skuteczną emulację.

Typowe wyrażenia tabelowe wymagają Postgresa 8.4+ .
LATERAL wymaga Postgresa 9.3+ .
Następujące rozwiązania wykraczają poza to, co opisano w Wiki Postgres .

1. Brak oddzielnej tabeli z unikalnymi użytkownikami

Z oddzielnymi users tabela, rozwiązania w 2. poniżej są zazwyczaj prostsze i szybsze. Przejdź dalej.

1a. Rekurencyjne CTE z LATERAL dołącz

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;

Jest to proste do pobrania dowolnych kolumn i prawdopodobnie najlepsze w obecnym Postgresie. Więcej wyjaśnień w rozdziale 2a. poniżej.

1b. Rekurencyjne CTE ze skorelowanym podzapytaniem

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;

Wygodne pobieranie pojedynczej kolumny lub cały wiersz . W przykładzie zastosowano cały typ wiersza tabeli. Możliwe są inne warianty.

Aby potwierdzić, że wiersz został znaleziony w poprzedniej iteracji, przetestuj pojedynczą kolumnę NOT NULL (jak klucz podstawowy).

Więcej wyjaśnień dla tego zapytania w rozdziale 2b. poniżej.

Powiązane:

  • Zapytaj ostatnie N powiązanych wierszy na wiersz
  • GRUPUJ WEDŁUG jednej kolumny, podczas sortowania według drugiej w PostgreSQL

2. Z oddzielnymi users stół

Układ tabeli nie ma większego znaczenia, jeśli dokładnie jeden wiersz na odpowiedni user_id Jest gwarantowana. Przykład:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

W idealnym przypadku tabela jest fizycznie posortowana zsynchronizowana z log stół. Zobacz:

  • Optymalizuj zakres zapytań sygnatury czasowej Postgres

Albo jest na tyle mały (niska kardynalność), że nie ma to większego znaczenia. W przeciwnym razie sortowanie wierszy w zapytaniu może pomóc w dalszej optymalizacji wydajności. Zobacz dodatek Gang Lianga. Jeśli fizyczna kolejność sortowania users tabela pasuje do indeksu w log , może to być nieistotne.

2a. LATERAL dołącz

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 pozwala odwoływać się do poprzedzającego FROM elementy na tym samym poziomie zapytania. Zobacz:

  • Jaka jest różnica między ŁĄCZENIEM BOCZNYM a podzapytanie w PostgreSQL?

Powoduje jedno wyszukiwanie indeksu (tylko) na użytkownika.

Nie zwraca żadnego wiersza dla użytkowników, których brakuje w grupie users stół. Zazwyczaj klucz obcy ograniczenie wymuszające integralność referencyjną mogłoby to wykluczyć.

Ponadto brak wiersza dla użytkowników bez pasującego wpisu w log - zgodnie z pierwotnym pytaniem. Aby utrzymać tych użytkowników w wynikach, użyj LEFT JOIN LATERAL ... ON true zamiast CROSS JOIN LATERAL :

  • Wywołaj wielokrotnie funkcję zwracającą zestaw z argumentem tablicowym

Użyj LIMIT n zamiast LIMIT 1 aby pobrać więcej niż jeden wiersz (ale nie wszystkie) na użytkownika.

W rzeczywistości wszystkie z nich robią to samo:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Ten ostatni ma jednak niższy priorytet. Jawne JOIN wiąże przed przecinkiem. Ta subtelna różnica może mieć znaczenie przy większej liczbie tabel ze złączami. Zobacz:

  • "nieprawidłowe odniesienie do wpisu klauzuli FROM dla tabeli" w zapytaniu Postgres

2b. Skorelowane podzapytanie

Dobry wybór, aby pobrać pojedynczą kolumnę z jednego rzędu . Przykład kodu:

  • Optymalizuj maksymalne zapytanie grupowe

To samo jest możliwe w przypadku wielu kolumn , ale potrzebujesz więcej sprytu:

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;

Jak LEFT JOIN LATERAL powyżej ten wariant obejmuje wszystkie użytkowników, nawet bez wpisów w log . Otrzymujesz NULL dla combo1 , który można łatwo filtrować za pomocą WHERE klauzula w zapytaniu zewnętrznym, jeśli to konieczne.
Nitpick:w zapytaniu zewnętrznym nie można odróżnić, czy podzapytanie nie znalazło wiersza, czy też wszystkie wartości kolumn są NULL – ten sam wynik. Potrzebujesz NOT NULL kolumny w podzapytaniu, aby uniknąć tej niejednoznaczności.

Skorelowane podzapytanie może zwrócić tylko pojedynczą wartość . Możesz zawinąć wiele kolumn w typ złożony. Ale żeby go później rozłożyć, Postgres wymaga dobrze znanego typu kompozytu. Rekordy anonimowe można dekomponować tylko podając listę definicji kolumn.
Użyj zarejestrowanego typu, takiego jak typ wiersza istniejącej tabeli. Lub zarejestruj typ złożony jawnie (i na stałe) za pomocą CREATE TYPE . Lub utwórz tabelę tymczasową (usuwaną automatycznie pod koniec sesji), aby tymczasowo zarejestrować jej typ wiersza. Składnia Cast:(log_date, payload)::combo

Na koniec nie chcemy rozkładać combo1 na tym samym poziomie zapytania. Ze względu na słabość planera zapytań, podzapytanie zostanie ocenione raz dla każdej kolumny (nadal prawdziwe w Postgres 12). Zamiast tego utwórz podzapytanie i dokonaj rozkładu w zewnętrznym zapytaniu.

Powiązane:

  • Pobierz wartości z pierwszego i ostatniego wiersza na grupę

Demonstracja wszystkich 4 zapytań z 100 tys. wpisów w dzienniku i 1 tys. użytkowników:
db<>zagraj tutaj - str. 11
Stary sqlfiddle



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Indeksowane ORDER BY z LIMIT 1

  2. Jak AT TIME ZONE działa w PostgreSQL

  3. nie można utworzyć rozszerzenia bez roli administratora

  4. Jak Cotd() działa w PostgreSQL

  5. Zwróć typ tabeli z funkcji A w PostgreSQL