我在Postgres 9.2中有以下用户消息日志表(简化形式):
CREATE TABLE log (
log_date DATE,
user_id INTEGER,
payload INTEGER
);
每个用户每天最多包含一条记录。300天内每天将有大约500K条记录。每个用户的有效负载不断增加(如果有问题的话)。
我想高效地检索每个用户在特定日期之前的最新记录。我的查询是:
SELECT user_id, max(log_date), max(payload)
FROM log
WHERE log_date <= :mydate
GROUP BY user_id
这是非常缓慢的。我也尝试过:
SELECT DISTINCT ON(user_id), log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC;
其具有相同的计划并且同样慢。
到目前为止,我在log(log_date)
上只有一个索引,但没有多大帮助。
我有一个包含所有用户的users
表,我还想检索一些用户的结果(使用payload > :value
的用户)。
我是否应该使用任何其他索引来加快速度,或者使用任何其他方法来实现我想要的结果?
3条答案
按热度按时间dced5bon1#
要获得最佳读取性能,您需要使用multicolumn index:
要使**index only scans**成为可能,请使用
INCLUDE
子句(Postgres 11或更高版本)在covering index中添加原本不需要的列payload
:参见:
旧版本的回退:
为什么是
DESC NULLS LAST
?对于每个
user_id
***很少***行或小表,DISTINCT ON
通常是最快和最简单的:对于每个
user_id
有*多*行的情况,index skip scan (or loose index scan)(要)高效得多。这在Postgres 15 (work is ongoing)之前还没有实现。但是有一些方法可以有效地模拟它。Common Table Expressions需要Postgres8.4 +。
LATERAL
需要Postgres9.3 +。以下解决方案超出了Postgres Wiki的涵盖范围。
1.没有包含唯一用户的单独表
users
表,下面**2.*中的解决方案通常更简单、更快速。跳过。1a.使用
LATERAL
连接的递归CTE这对于检索任意列是很简单的,而且在当前的Postgres中可能是最好的。更多的解释在下面的章节 * 2a. * 中。
1b.具有相关子查询的递归CTE
便于检索 * 单列 * 或 * 整行 *。本示例使用表的整行类型。其他变体也是可能的。
要Assert在上一次迭代中找到了一行,请测试单个NOT NULL列(如主键)。
相关:
2.使用单独的
users
表只要保证每个相关的
user_id
正好有一行,表格布局就不重要。理想情况下,表的物理排序与
log
表同步。请参阅:或者它足够小(低基数),几乎不起作用。另外,对查询中的行进行排序可以帮助进一步优化性能。See Gang Liang's addition.如果
users
表的物理排序顺序恰好与log
上的索引匹配,这可能是无关紧要的。2a.
LATERAL
连接JOIN LATERAL
允许引用同一查询级别上的前面FROM
项。请参阅:每个用户只进行一次索引(仅-)查找。
对于
users
表中缺少的用户,不返回任何行。通常,实施参照完整性的外键约束会排除这种情况。此外,
log
中没有匹配条目的用户没有行-与原始问题一致。要将这些用户保留在结果中,请使用**LEFT JOIN LATERAL ... ON true
**代替CROSS JOIN LATERAL
:使用**
LIMIT n
而不是LIMIT 1
来检索每个用户的多行**(但不是全部)。实际上,所有这些都是相同的:
但是最后一个优先级较低。显式
JOIN
绑定在逗号之前。这个细微的差别可能会影响更多的连接表。请参见:2b.相关子查询
从单行检索单列的好选择。代码示例:
对于多个列也可以实现同样的操作,但您需要更聪明的方法:
与上面的
LEFT JOIN LATERAL
类似,这个变体包括 * all * 用户,即使log
中没有条目,也可以得到combo1
的NULL
,如果需要,可以在外部查询中使用WHERE
子句轻松地过滤它。吹毛求疵:在外部查询中,你无法区分子查询是否没有找到行,或者所有列的值碰巧都是NULL--同样的结果。你需要在子查询中使用
NOT NULL
列来避免这种歧义。一个相关子查询只能返回一个单个值。你可以将多个列 Package 成一个复合类型。但是为了以后分解它,Postgres需要一个众所周知的复合类型。匿名记录只能在提供列定义列表的情况下分解。
使用已注册的类型,如现有表的行类型。或者使用
CREATE TYPE
显式(永久)注册复合类型。或者创建临时表(在会话结束时自动删除)以临时注册其行类型。转换语法:(log_date, payload)::combo
最后,我们不希望在同一查询级别上分解
combo1
。由于查询计划器中的一个弱点,这将为每列计算一次子查询(在Postgres 12中仍然如此)。相反,将其作为子查询并在外部查询中分解。相关:
使用100k个日志条目和1k个用户演示所有4个查询:
老SQLF
gblwokeq2#
这不是一个独立的答案,而是对@Erwin的answer的一个注解。对于2a,横向连接示例,可以通过对
users
表排序来利用log
上索引的局部性来改进查询。其基本原理是,如果
user_id
值是随机的,则索引查找的开销会很大。通过首先对user_id
进行排序,后续的横向联接就像对log
的索引进行简单扫描一样。尽管两个查询计划看起来很相似,但运行时间会有很大差异,尤其是对于大型表。排序的成本是最小的,特别是在
user_id
字段上有索引的情况下。c2e8gylq3#
也许表上的其他索引会有所帮助。请尝试以下索引:我不确定Postgres是否会最佳地使用
distinct on
。所以,我会坚持使用该索引,并尝试这个版本:
这将用索引查找代替排序/分组。这样可能会更快。