Ответ 1
Для лучшей производительности чтения вам нужен многоколонный индекс:
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);
См:
Откат для более старых версий:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Почему DESC NULLS LAST
?
Для нескольких строк в user_id
или небольших таблиц DISTINCT ON
обычно самый быстрый и простой:
Для большого количества строк в user_id
сканирование с пропуском индекса (или сканирование свободного индекса) (намного) более эффективно. Это не реализовано до Postgres 12 - работа продолжается для Postgres 13. Но есть способы эффективно имитировать.
Стандартные табличные выражения требуют 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. Больше объяснения в главе 2а. ниже.
1б. Рекурсивный 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 связанных строк в строке
- GROUP BY один столбец, а сортировка по другому в PostgreSQL
2. С отдельной таблицей users
Расположение таблицы вряд ли имеет значение, поскольку гарантируется ровно одна строка на соответствующий user_id
. Пример:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
В идеале таблица физически сортируется синхронно с таблицей log
. См:
Или он достаточно мал (низкая мощность), что вряд ли имеет значение. В противном случае сортировка строк в запросе может помочь в дальнейшей оптимизации производительности. См. дополнение Gang Liang. Если физический порядок сортировки таблицы users
совпадает с индексом в log
, это может быть неактуально.
2а. 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
на одном уровне запроса. См:
Результаты поиска по одному индексу (-only) на пользователя.
Не возвращает строки для пользователей, отсутствующих в таблице users
. Как правило, ограничение внешнего ключа, обеспечивающее ссылочную целостность, исключает это.
Также, нет строки для пользователей без соответствующей записи в log
- в соответствии с исходным вопросом. Чтобы сохранить этих пользователей в результате, используйте LEFT JOIN LATERAL ... ON true
вместо CROSS JOIN LATERAL
:
Используйте LIMIT n
вместо LIMIT 1
, чтобы получить более одной строки (но не все) для пользователя.
По сути, все они делают то же самое:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
Последний имеет более низкий приоритет. Явный JOIN
связывает перед запятой. Эта тонкая разница может иметь значение при большем количестве таблиц соединений. См:
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
во внешнем запросе.
Nitpick: во внешнем запросе вы не можете различить, не найден ли подзапрос строкой или все значения столбца оказываются NULL - тот же результат. Вам нужен столбецNOT NULL
в подзапросе, чтобы избежать этой неоднозначности.Коррелированный подзапрос может возвращать только одно значение. Вы можете заключить несколько столбцов в составной тип. Но чтобы разложить его позже, Postgres требует хорошо известного составного типа. Анонимные записи могут быть разложены только с помощью списка определений столбцов.
Используйте зарегистрированный тип, такой как тип строки существующей таблицы. Или зарегистрируйте составной тип явно (и постоянно) с помощьюCREATE TYPE
. Или создайте временную таблицу (автоматически удаляемую в конце сеанса), чтобы временно зарегистрировать ее тип строки. Синтаксис приведения:(log_date, payload)::combo
Наконец, мы не хотим декомпозировать
combo1
на одном уровне запроса. Из-за слабости в планировщике запросов это оценило бы подзапрос один раз для каждого столбца (все еще верно в Postgres 12). Вместо этого сделайте его подзапросом и разложите во внешнем запросе.
Связанный:
Демонстрация всех 4 запросов с записями в журнале 100k и пользователями 1k:
db & lt;> скрипка здесь - стр. 11
Старый sqlfiddle - стр. 9,6