我在Postgres 9.2中有以下用户消息(简化形式)的日志表:
CREATE TABLE log (
log_date DATE,
user_id INTEGER,
payload INTEGER
);
每个用户和每天最多包含一条记录。每天将有大约500,000条记录,为期300天。每个用户的有效负载都在不断增加(如果重要的话)。
我希望在特定日期之前有效地检索每个用户的最新记录。我的疑问是:
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
)的结果。
我是否应该使用其他任何索引加快速度,或以其他任何方式实现我想要的目标?
答案 0 :(得分:92)
为获得最佳阅读效果,您需要multicolumn index:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST)
要使 index only scans 成为可能,请添加其他不需要的列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 11 - work is ongoing for Postgres 12。但有一些方法可以有效地模仿它。
Common Table Expressions要求Postgres 8.4 +
LATERAL
要求Postgres 9.3 +
以下解决方案超出了Postgres Wiki中涵盖的范围。
使用单独的users
表格,下面 2。中的解决方案通常更简单,更快捷。向前跳。
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。章节中有更多解释。
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章中有关此查询的更多说明。下方。
相关:
users
表只要每个相关user_id
只有一行得到保证,表格布局就不重要了。例如:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
理想情况下,表与物理排序与log
表同步。参见:
或者它足够小(低基数),这几乎不重要。否则,在查询中对行进行排序有助于进一步优化性能。 See Gang Liang's addition.如果users
表的物理排序顺序与log
上的索引匹配,则可能无关紧要。
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
在逗号之前绑定。对于更多的连接表,这种微妙的差异可能很重要。参见:
从单行检索单列的不错选择。代码示例:
多列也是如此,但您需要更多智能:
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 9.6 - 计划为Postgres 10进行改进)。相反,将它作为子查询并在外部查询中进行分解。
相关:
使用100k日志条目和1k用户演示所有4个查询:
SQL Fiddle - 第9.6页
db&lt;&gt;小提琴here - 第11页
答案 1 :(得分:4)
也许表上的不同索引会有所帮助。试试这个:log(user_id, log_date)
。我并不认为Postgres会使用distinct on
进行最佳使用。
所以,我会坚持使用该索引并尝试此版本:
select *
from log l
where not exists (select 1
from log l2
where l2.user_id = l.user_id and
l2.log_date <= :mydate and
l2.log_date > l.log_date
);
这应该用索引查找替换排序/分组。它可能会更快。
答案 2 :(得分:4)
这不是一个独立的答案,而是对@Erwin answer的评论。对于2a,横向连接示例,可以通过对users
表进行排序以利用log
上索引的位置来改进查询。
SELECT u.user_id, l.log_date, l.payload
FROM (SELECT user_id FROM users ORDER BY user_id) u,
LATERAL (SELECT log_date, payload
FROM log
WHERE user_id = u.user_id -- lateral reference
AND log_date <= :mydate
ORDER BY log_date DESC NULLS LAST
LIMIT 1) l;
理由是,如果user_id
值是随机的,则索引查找会很昂贵。通过先排序user_id
,后续的横向连接就像对log
索引的简单扫描。尽管两个查询计划看起来都很相似,但运行时间会有很大不同,特别是对于大型表。
如果user_id
字段上有索引,则排序成本很低。