За най-добра производителност при четене имате нужда от многоколонен индекс:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
За да направите само индексно сканиране възможно, добавете иначе ненужната колона payload
в покриващ индекс с INCLUDE
клауза (Postgres 11 или по-нова версия):
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Вижте:
- Покриването на индекси в PostgreSQL помага ли на JOIN колони?
Резервно за по-стари версии:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Защо DESC NULLS LAST
?
- Неизползван индекс в заявката за диапазон от дати
Заняколко редове за user_id
или малки таблици DISTINCT ON
обикновено е най-бързият и прост:
- Изберете ли първия ред във всяка група GROUP BY?
Замного редове за user_id
сканиране с пропускане на индекс (или разхлабено сканиране на индекс ) е (много) по-ефективен. Това не е внедрено до Postgres 12 – работата е в ход за Postgres 14. Но има начини да се емулира ефективно.
Общите таблични изрази изискват Postgres 8.4+ .LATERAL
изисква Postgres 9.3+ .
Следните решения надхвърлят това, което е покрито в Postgres Wiki .
1. Няма отделна таблица с уникални потребители
С отделни users
таблица, решения в2. по-долу обикновено са по-прости и по-бързи. Прескочете напред.
1a. Рекурсивен CTE с LATERAL
присъединете се
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;
Това е лесно за извличане на произволни колони и вероятно най-добре в текущия Postgres. Повече обяснения в глава 2a. по-долу.
1b. Рекурсивна CTE с корелирана подзаявка
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;
Удобно е за извличане на единична колона или целия ред . Примерът използва целия тип ред на таблицата. Възможни са и други варианти.
За да потвърдите, че в предишната итерация е намерен ред, тествайте една колона NOT NULL (като първичния ключ).
Повече обяснение за тази заявка в глава 2b. по-долу.
Свързано:
- Запитване за последните N свързани реда на ред
- ГРУПИРАНЕ ПО една колона, докато сортиране по друга в PostgreSQL
2. С отделни users
маса
Оформлението на таблицата едва ли има значение, стига точно един ред на релевантен user_id
е гарантирано. Пример:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
В идеалния случай таблицата е физически сортирана в синхрон с log
маса. Вижте:
- Оптимизиране на обхвата на заявки за времеви печати в Postgres
Или е достатъчно малък (ниска мощност), че едва ли има значение. В противен случай сортирането на редове в заявката може да помогне за допълнително оптимизиране на производителността. Вижте допълнението на Gang Liang. Ако физическият ред на сортиране на users
таблицата съвпада с индекса в log
, това може да е без значение.
2a. LATERAL
присъединете се
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
позволява препратка към предходния FROM
елементи на същото ниво на заявка. Вижте:
- Каква е разликата между LATERAL JOIN и подзаявка в PostgreSQL?
Резултатът е едно търсене в индекс (само) на потребител.
Не връща ред за потребители, липсващи в users
маса. Обикновено външен ключ ограничението, налагащо референтната цялост, би изключило това.
Също така няма ред за потребители без съответстващ запис в log
- в съответствие с първоначалния въпрос. За да запазите тези потребители в резултата, използвайте LEFT JOIN LATERAL ... ON true
вместо CROSS JOIN LATERAL
:
- Извикване на функция за връщане на набор с аргумент на масив няколко пъти
Използвайте LIMIT n
вместо LIMIT 1
за извличане на повече от един ред (но не всички) на потребител.
На практика всички те правят едно и също:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
Последният обаче има по-нисък приоритет. Изрично JOIN
се свързва преди запетая. Тази фина разлика може да има значение за повече таблици за присъединяване. Вижте:
- „невалидна препратка към запис на клауза FROM за таблица“ в заявката на Postgres
2b. Корелирана подзаявка
Добър избор за извличане на единична колона от единедин ред . Пример за код:
- Оптимизиране на груповата максимална заявка
Същото е възможно замножество колони , но имате нужда от повече интелигентност:
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;
Като LEFT JOIN LATERAL
по-горе, този вариант включва всички потребители, дори без записи в log
. Получавате NULL
за combo1
, който можете лесно да филтрирате с WHERE
клауза във външната заявка, ако е необходимо.
Нитка:във външната заявка не можете да различите дали подзаявката не е намерила ред или всички стойности на колоните са NULL - същият резултат. Нуждаете се от NOT NULL
колона в подзаявката, за да избегнете тази неяснота.
Свързана подзаявка може да върне само единична стойност . Можете да увиете няколко колони в композитен тип. Но за да го разложи по-късно, Postgres изисква добре познат композитен тип. Анонимните записи могат да бъдат разложени само при предоставяне на списък с дефиниции на колони.
Използвайте регистриран тип като типа на ред на съществуваща таблица. Или регистрирайте съставен тип изрично (и за постоянно) с CREATE TYPE
. Или създайте временна таблица (отпадаща автоматично в края на сесията), за да регистрирате временно нейния тип ред. Синтаксис за предаване:(log_date, payload)::combo
И накрая, ние не искаме да разлагаме combo1
на същото ниво на заявка. Поради слабост в планировчика на заявки това ще оцени подзаявката веднъж за всяка колона (все още е вярно в Postgres 12). Вместо това я направете като подзаявка и я разложете във външната заявка.
Свързано:
- Вземете стойности от първия и последния ред на група
Демонстриране на всички 4 заявки със 100 000 записи в регистрационния файл и 1 000 потребители:
db<>fiddle тук - стр. 11
Стар sqlfiddle