我需要选择一段时间内的记录:我的表
time_limits
具有两个timestamp
字段和一个integer
属性。我的实际表中还有此查询不涉及的其他列。以下代码花费了大量时间:create table (
start_date_time timestamp,
end_date_time timestamp,
id_phi integer,
primary key(start_date_time, end_date_time,id_phi);
所以我尝试添加另一个索引-PK的倒数:
select * from time_limits as t
where t.id_phi=0
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time >= timestamp'2010-08-08 00:05:00';
我感觉性能有所提高:访问表中间的记录的时间似乎更合理:大约40到90秒。
但是仍然要几十倍时间范围中间的值的秒数。并且针对表的末尾(按时间顺序)又增加了两次。 br />在depesz.com上查看结果。
我该怎么做才能优化搜索?一旦
explain analyze
设置为id_phi
,您将看到所有时间都花在扫描两个时间戳列上。而且我不了解时间戳上的大扫描(60K行!)。它们不是由主键索引并添加了吗?我应该从时间戳类型更改为其他类型吗?
我已经阅读了一些有关GIST和GIN索引。我收集到,在某些条件下,自定义类型可以提高效率。我的用例是否可行?
#1 楼
对于Postgres 9.1或更高版本:CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);
在大多数情况下,索引的排序顺序几乎不相关。 Postgres几乎可以向后扫描。但是,对于多列的范围查询,可能会产生巨大的不同。密切相关:
PostgreSQL索引不用于范围查询
考虑您的查询:
SELECT *
FROM time_limits
WHERE id_phi = 0
AND start_date_time <= '2010-08-08 00:00'
AND end_date_time >= '2010-08-08 00:05';
索引中第一列
id_phi
的排序是无关紧要的。由于已检查是否相等(=
),因此它应该排在第一位。你说对了。有关此问题的更多信息:多列索引和性能
Postgres可以立即跳到
id_phi = 0
并考虑以下两个匹配索引列。使用反向排序顺序的范围条件(<=
,>=
)查询它们。在我的索引中,符合条件的行排在第一位。应该是使用B树索引的最快方法1:您要
start_date_time <= something
:索引首先具有最早的时间戳。如果符合条件,还请检查列3。
递归直到第一行不符合条件(超快速)。
您希望
end_date_time >= something
:索引具有最新的时间戳。如果符合条件,请继续获取行,直到第一行不符合条件(超快速)。
继续第2列的下一个值。.
Postgres可以向前或向后扫描。使用索引的方式,它必须读取前两列中匹配的所有行,然后在第三列中进行过滤。请务必阅读手册中的“索引”和“
ORDER BY
”一章。 前两列匹配多少行?
只有很少几行的
start_date_time
接近表格时间范围的开始。但是几乎所有在表的时间顺序上带有id_phi = 0
的行!因此,性能会随着启动时间的延长而变差。规划师的估计值
规划人员为您的示例查询估算
rows=62682
。其中没有一个合格(rows=0
)。如果增加表的统计目标,可能会得到更好的估计。对于2.000.000行... ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time SET STATISTICS 1000;
...可能需要付款。甚至更高。有关此答案的更多信息:
检查PostgreSQL中的统计目标
我想您不需要
id_phi
(只有几个不同的值,均匀分布),但是时间戳(很多不同的值,分布不均匀)。我也不认为改进索引有什么意义。但是,您可以更快地简化表中行的物理顺序。如果您有能力单独锁定表(例如在非工作时间锁定表),请使用
CLUSTER
重写表并根据索引对行进行排序:CLUSTER time_limits USING idx_time_limits_inversed;
,或者考虑使用pg_repack或更高版本的pg_squeeze不论哪种方式,结果都是需要从表中读取更少的块,并且所有内容都进行了预排序。随着时间的推移,表上的写入会破坏物理排序顺序,这是一种一次性的影响。用于范围列。
CLUSTER
和timestamp
有内置的范围类型:timestamp with time zone
,tsrange
。对于其他tstzrange
列(例如integer
),btree索引通常更快。维护更小,更便宜。但是使用组合索引,整体查询可能仍然会更快。更改表定义或使用表达式索引。
对于您还需要安装多列GiST索引,您还需要安装附加模块
id_phi
(每个数据库一次),该模块为操作员类提供一个btree_gist
。三连胜!多列功能GiST索引:
CREATE EXTENSION IF NOT EXISTS btree_gist; -- if not installed, yet
CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));
现在在查询中使用“包含范围”运算符
integer
:SELECT *
FROM time_limits
WHERE id_phi = 0
AND tsrange(start_date_time, end_date_time, '[]')
@> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')
Postgres 9.3+中的SP-GiST索引SP-GiST索引对于这种查询可能甚至更快-除非引用手册,否则:
当前,仅B树,GiST,GIN和BRIN索引类型支持多列索引。 br />
在Postgres 12中仍然适用。
您必须将
@>
上的spgist
索引与(tsrange(...))
上的第二个btree
索引结合起来。随着额外的开销,我不确定这是否可以竞争。仅提供
(id_phi)
列的基准测试相关答案:在PostgreSQL中执行此小时的操作查询
评论
我至少应该告诉一次,您对SO和DBA的每个回答都具有很高的附加值/专业知识,并且大多数时候是最完整的。只需说一次:尊重!。
–斯蒂芬·罗兰(Stephane Rolland)
13-4-10在2:14
谢谢! :)那您得到更快的结果了吗?
–欧文·布兰德斯特
13年4月10日在2:34
我必须完成从我的笨拙的查询中生成的大批量副本,因此使该过程真的很慢,在问这个问题之前已经花了好几个小时了。但是我已经计算过了,我决定让它转动到明天早上,它将完成,新桌子也准备好明天装满。我试图在工作期间同时创建索引,但是由于访问过多(我认为),索引的创建应被锁定。我将重复同样的测试时间,以完善您的解决方案。我还研究了如何将Debian / ubuntu升级到9.2 ;-)。
–斯蒂芬·罗兰(Stephane Rolland)
13年4月10日在3:29
@StephaneRolland:为什么解释分析输出显示45毫秒,而您却看到查询花费了40秒钟,这仍然很有趣。
– a_horse_with_no_name
13年4月10日在7:19
@John:Postgres可以向前或向后遍历索引,但不能在同一扫描中改变方向。理想情况下,每个节点的第一个(或最后一个)都有所有符合条件的行,但是对于所有列,它必须具有相同的对齐方式(匹配查询谓词)才能获得最佳结果。
–欧文·布兰德斯特
17-12-29在22:31
#2 楼
但是,Erwin的答案已经很全面了:时间戳的范围类型可在PostgreSQL 9.1中获得,而Jeff Davis的Temporal扩展名是这样的: />
注意:功能有限(使用Timestamptz,并且您只能使'[)'样式重叠afaik)。另外,还有很多其他重要原因可以升级到PostgreSQL 9.2。
#3 楼
您可以尝试以不同的顺序创建多列索引:primary key(id_phi, start_date_time,end_date_time);
我曾经发布过一个类似的问题,该问题也与多列索引上的索引排序有关。关键是首先尝试使用限制性最强的条件来减少搜索空间。
编辑:我的错。现在我看到您已经定义了该索引。
评论
我已经有了两个索引。除了主键之外,其他主键都已存在,但您建议的索引已经存在,并且如果您查看以下说明,它也是使用的索引:位图索引在idx_time_limits_phi_start_end上扫描
–斯蒂芬·罗兰(Stephane Rolland)
13年4月9日在20:29
#4 楼
我设法快速增加(从1秒到70ms)我有一个表,其中包含许多测量值和许多级别的汇总(
l
列)(30s,1m,1h等),存在两个范围限制列:$s
表示开始,$e
表示结束。我创建了两个多列索引:一个表示开始,一个表示结束。
我调整了选择查询:选择范围从它们的开始位置界限在给定范围内。另外选择范围在其给定范围内的范围。
解释使用我们的索引有效地显示了两个行流。
索引:
选择查询:
drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");
drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");
说明:
select "$s", "$e", a, t, b, c from agg
where
measurement_id=0
and l = '30s'
and (
(
"$s" > '2013-05-01 02:05:05'
and "$s" < '2013-05-01 02:18:15'
)
or
(
"$e" > '2013-05-01 02:00:05'
and "$e" < '2013-05-01 02:18:05'
)
)
;
诀窍是您的计划节点仅包含所需的行。以前我们在计划节点中获得了数千行,因为它选择了
all points from some point in time to the very end
,然后下一个节点删除了不必要的行。
评论
好吧,那是45岁。我不知道为什么说45毫秒。我什至不会开始抱怨它是否快到45毫秒... :-)也许解释分析输出中的错误。或者也许是进行分析的时候了。不知道。但是我测量的是40/50秒。说明分析输出中报告的时间是服务器上查询所需的时间。如果您的查询花了45秒,那么将花费额外的时间将数据从数据库传输到运行查询的程序中。所有的内容都是62682行,并且如果每一行都很大(例如,具有长varchar或text列),这可能会影响传输时间大大缩短。
@a_horse_with_no_name:行= 62682行是计划者的估计。查询返回0行。 (实际时间= 44.446..44.446行= 0循环= 1)
@ErwinBrandstetter:嗯,对。我忽略了这一点。但是我仍然从未见过explain analysis的输出是关于执行时间的。