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