xpa*_*pad 45 sql postgresql indexing greatest-n-per-group postgresql-performance
我在Postgres 9.2中有下表(简化形式)
CREATE TABLE log (
log_date DATE,
user_id INTEGER,
payload INTEGER
);
Run Code Online (Sandbox Code Playgroud)
它每个用户和每天最多包含一条记录.每天将有大约500,000条记录,为期300天.每个用户的running_total总是在增加.
我想在特定日期之前有效地检索每个用户的最新记录.我的查询是:
SELECT user_id, max(log_date), max(payload)
FROM log
WHERE log_date <= :mydate
GROUP BY user_id
Run Code Online (Sandbox Code Playgroud)
这非常慢.我也尝试过:
SELECT DISTINCT ON(user_id), log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC;
Run Code Online (Sandbox Code Playgroud)
具有相同的计划,同样缓慢.
到目前为止,我在user_msg_log(aggr_date)上有一个索引,但没有多大帮助.我应该用什么其他索引来加快速度,还是以任何其他方式实现我的目标?
Erw*_*ter 106
为获得最佳读取性能,您需要一个多列索引:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
Run Code Online (Sandbox Code Playgroud)
要使索引仅扫描成为可能,请添加其他不需要的列payload
:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Run Code Online (Sandbox Code Playgroud)
为什么INCLUDE
?
对于每个或每个小表的几行,DESC NULLS LAST
简单user_id
是最快和最简单的解决方案之一:
对于许多每行DISTINCT ON
一个松散索引扫描将是(多)更有效.这在Postgres中没有实现(至少在Postgres 10中实现),但有一些方法可以模仿它:
以下解决方案超出了Postgres Wiki所涵盖的范围.
随着一个单独的user_id
表,在方案2,下面是通常更加简单和快速.
LATERAL
连接的递归CTE常用表格表达式要求Postgres 8.4+.
users
需要Postgres 9.3+.
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Run Code Online (Sandbox Code Playgroud)
这在Postgres的当前版本中是优选的,并且检索任意列很简单.第2a章有更多解释.下面.
方便检索单个列或整行.该示例使用表的整行类型.其他变体是可能的.
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;
Run Code Online (Sandbox Code Playgroud)
用于测试行值可能会产生误导LATERAL
.这仅users
在测试行的每一列都返回时返回,如果包含user_id
单个log
值则会失败.(我的答案中有一段时间我有这个错误.)相反,在前一次迭代中找到了一个行,测试了一行定义的行users
(如主键).更多:
第2b章中对此查询的更多解释.下面.
相关答案:
log
桌子只要每个相关的行只有一行,表格布局就不重要了LATERAL
.例:
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;
Run Code Online (Sandbox Code Playgroud)
理想情况下,表是物理排序的.看到:
或者它足够小(低基数),这几乎不重要.
否则,在查询中对行进行排序有助于进一步优化性能.见Gang Liang的补充.
JOIN LATERAL
加入CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
FROM
允许users
在同一查询级别引用前面的项目.每个用户只能获得一个索引(-only)查找.
通过在另一个答案中对Gang Liang建议的log
表格进行排序来考虑可能的改进.如果表的物理排序顺序与索引匹配,则不需要此操作.LEFT JOIN LATERAL ... ON true
CROSS JOIN LATERAL
LIMIT n
即使您有条目,也无法获得表中缺少用户的结果LIMIT 1
.通常,您将具有强制引用完整性的外键约束来规则.
您也没有为任何没有匹配条目的用户获取行JOIN
.这符合你原来的问题.如果需要在结果使用中包含这些行LEFT JOIN LATERAL
而不是log
:
此表单最适合检索每个用户的多行(但不是全部).只需使用NULL
而不是combo1
.
实际上,所有这些都会做同样的事情:
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;
Run Code Online (Sandbox Code Playgroud)
但后者的优先级较低.WHERE
在逗号之前显式绑定.
从单行检索单个列的好选择.代码示例:
这同样是可能的多列,但你需要更多的智慧:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
Run Code Online (Sandbox Code Playgroud)
像NOT NULL
上面这样的变体包括所有用户,即使没有条目CREATE TYPE
.你得到(log_date, payload)::combo
的combo1
,payload
如果需要的话,可以使用外部查询中的子句轻松过滤.
Nitpick:在外部查询中,您无法区分子查询是否找不到行,或者返回的所有值都是NULL - 结果相同.您必须INCLUDE
在子查询中包含一列才能确定.
相关子查询只能返回单个值.您可以将多个列包装为复合类型.但是为了稍后分解它,Postgres需要一种众所周知的复合类型.匿名记录只能在提供列定义列表的情况下进行分解.
使用已注册类型(如现有表的行类型)或创建类型.显式(和永久)注册复合类型DESC NULLS LAST
,或创建临时表(在会话结束时自动删除)以临时提供行类型.转换为该类型:user_id
最后,我们不希望DISTINCT ON
在同一查询级别上进行分解.由于查询规划器的弱点,这将为每列评估子查询一次(直到Postgres 9.6 - 计划为Postgres 10进行改进).相反,将其作为子查询并在外部查询中进行分解.
有关:
使用100k日志条目和1k用户演示所有4个查询:
SQL Fiddle - pg 9.6
db <> fiddle here - pg 10
也许表上的不同索引会有所帮助。试试这个: 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
);
Run Code Online (Sandbox Code Playgroud)
这应该用索引查找代替排序/分组。它可能会更快。
这不是一个独立的答案,而是对@Erwin的答案的评论。对于横向连接示例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;
Run Code Online (Sandbox Code Playgroud)
理由是如果user_id
值是随机的,则索引查找会很昂贵。通过user_id
首先进行排序,随后的横向联接就像对索引的简单扫描log
。即使两个查询计划看起来都一样,但是运行时间还是会有很大差异的,尤其是对于大型表。
排序的成本是最小的,特别是如果user_id
现场有索引的话。
归档时间: |
|
查看次数: |
32346 次 |
最近记录: |