为了获得最佳读取性能,您需要multicolumn index:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
要使 index only scans 成为可能,请在 covering index 中使用 INCLUDE 子句(Postgres 11 或更高版本)添加其他不需要的列 payload:
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 和 index skip scan (or loose index scan) (很多)效率更高。这在 Postgres 12 - work is ongoing for Postgres 14 之前还没有实现。但是有一些方法可以有效地模拟它。
Common Table Expressions 需要 Postgres 8.4+。
LATERAL 需要 Postgres 9.3+。
以下解决方案超出了Postgres Wiki 所涵盖的范围。
1。没有唯一用户的单独表格
使用单独的users 表,下面2. 中的解决方案通常更简单、更快。跳过。
1a。带有LATERAL 的递归 CTE 加入
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 章对此查询的更多解释。下面。
相关:
2。带有单独的users 表
只要保证每个相关user_id 恰好有一行,表格布局就无关紧要了。示例:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
理想情况下,该表的物理排序与log 表同步。见:
或者它足够小(低基数),它几乎不重要。否则,对查询中的行进行排序有助于进一步优化性能。 See Gang Liang's addition. 如果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 项目。见:
导致每个用户进行一次索引(仅)查找。
不为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 for combo1,如果需要,你可以在外部查询中使用WHERE 子句轻松过滤。
Nitpick:在外部查询中,你无法区分子查询是否没有'找不到行或所有列值恰好为 NULL - 结果相同。您需要在子查询中使用 NOT NULL 列来避免这种歧义。
相关子查询只能返回一个单个值。您可以将多个列包装成一个复合类型。但是为了稍后分解它,Postgres 需要一个众所周知的复合类型。只有提供列定义列表才能分解匿名记录。
使用注册类型,如现有表的行类型。或者使用CREATE TYPE 显式(并且永久地)注册一个复合类型。或者创建一个临时表(在会话结束时自动删除)以临时注册其行类型。转换语法:(log_date, payload)::combo
最后,我们不想在同一查询级别分解combo1。由于查询计划器的弱点,这将为每列评估一次子查询(在 Postgres 12 中仍然如此)。相反,将其设为子查询并在外部查询中分解。
相关:
使用 100k 日志条目和 1k 用户演示所有 4 个查询:
dbfiddle here - pg 11
旧 sqlfiddle