在postgresql 9.3上,我有一个包含超过一百万条记录的表,该表创建为:
CREATE TABLE entradas
(
id serial NOT NULL,
uname text,
contenido text,
fecha date,
hora time without time zone,
fecha_hora timestamp with time zone,
geom geometry(Point,4326),
CONSTRAINT entradas_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
ALTER TABLE entradas
OWNER TO postgres;
CREATE INDEX entradas_date_idx
ON entradas
USING btree
(fecha_hora);
CREATE INDEX entradas_gix
ON entradas
USING gist
(geom);
我正在执行查询以按时间间隔聚合行:
WITH x AS (
SELECT t1, t1 + interval '15min' AS t2
FROM generate_series('2014-12-02 0:0' ::timestamp
,'2014-12-02 23:45' ::timestamp, '15min') AS t1
)
select distinct
x.t1,
count(t.id) over w
from x
left join entradas t on t.fecha_hora >= x.t1
AND t.fecha_hora < x.t2
window w as (partition by x.t1)
order by x.t1
此查询大约需要50秒。从explain的输出中,您可以看到未使用时间戳索引:
Unique (cost=86569161.81..87553155.15 rows=131199111 width=12)
CTE x
-> Function Scan on generate_series t1 (cost=0.00..12.50 rows=1000 width=8)
-> Sort (cost=86569149.31..86897147.09 rows=131199111 width=12)
Sort Key: x.t1, (count(t.id) OVER (?))
-> WindowAgg (cost=55371945.38..57667929.83 rows=131199111 width=12)
-> Sort (cost=55371945.38..55699943.16 rows=131199111 width=12)
Sort Key: x.t1
-> Nested Loop Left Join (cost=0.00..26470725.90 rows=131199111 width=12)
Join Filter: ((t.fecha_hora >= x.t1) AND (t.fecha_hora < x.t2))
-> CTE Scan on x (cost=0.00..20.00 rows=1000 width=16)
-> Materialize (cost=0.00..49563.88 rows=1180792 width=12)
-> Seq Scan on entradas t (cost=0.00..37893.92 rows=1180792 width=12)
但是,如果我set enable_seqscan=false
(我知道,人们永远不应该这样做),那么查询会在不到一秒的时间内执行,而explain的输出显示它正在使用timestamp列上的索引:
Unique (cost=91449584.16..92433577.50 rows=131199111 width=12)
CTE x
-> Function Scan on generate_series t1 (cost=0.00..12.50 rows=1000 width=8)
-> Sort (cost=91449571.66..91777569.44 rows=131199111 width=12)
Sort Key: x.t1, (count(t.id) OVER (?))
-> WindowAgg (cost=60252367.73..62548352.18 rows=131199111 width=12)
-> Sort (cost=60252367.73..60580365.51 rows=131199111 width=12)
Sort Key: x.t1
-> Nested Loop Left Join (cost=1985.15..31351148.25 rows=131199111 width=12)
-> CTE Scan on x (cost=0.00..20.00 rows=1000 width=16)
-> Bitmap Heap Scan on entradas t (cost=1985.15..30039.14 rows=131199 width=12)
Recheck Cond: ((fecha_hora >= x.t1) AND (fecha_hora < x.t2))
-> Bitmap Index Scan on entradas_date_idx (cost=0.00..1952.35 rows=131199 width=0)
Index Cond: ((fecha_hora >= x.t1) AND (fecha_hora < x.t2))
为什么postgres不使用entradas_date_idx
,除非我强制它,即使执行查询使用它更快?
如何在不诉诸entradas_date_idx
的情况下使用set enable_seqscan=false
制作postgres?
答案 0 :(得分:3)
错误估算分析
问题的关键在于postgres规划器不知道generate_series
调用中出现了什么值和多少行,但是必须估计它们中有多少行满足JOIN条件大entradas
表。在你的情况下,它失败了很长时间。
实际上,只有一小部分表会被连接,但估计错误在另一侧,如EXPLAIN的这一部分所示:
-> Nested Loop Left Join (cost=0.00..26470725.90 rows=131199111 width=12)
Join Filter: ((t.fecha_hora >= x.t1) AND (t.fecha_hora < x.t2))
-> CTE Scan on x (cost=0.00..20.00 rows=1000 width=16)
-> Materialize (cost=0.00..49563.88 rows=1180792 width=12)
-> Seq Scan on entradas t (cost=0.00..37893.92 rows=1180792 width=12)
entradas
估计为1180792
行,x
估计为1000
行,我认为这只是任何SRF呼叫的默认值。 JOIN的结果估计为131199111
行,超过大表行数的100倍!
将计划员变成更好的估算
由于我们知道x
中的时间戳属于一个狭窄的范围(一天),我们可能会以额外的JOIN条件的形式帮助规划人员获取该信息:
left join entradas t
ON t.fecha_hora >= x.t1
AND t.fecha_hora < x.t2
AND (t.fecha_hora BETWEEN '2014-12-02'::timestamp
AND '2014-12-03'::timestamp)
(BETWEEN范围包括上限或通常稍微大一点并不重要,它将严格按其他条件过滤掉。)
然后规划人员应该能够利用统计数据,认识到只有一小部分索引涉及这个值范围,并使用索引而不是顺序扫描整个大表。
答案 1 :(得分:2)
您可以简单地简化查询:
SELECT x.t1, count(*) AS ct
FROM generate_series('2014-12-02'::timestamp
, '2014-12-03'::timestamp
, '15 min'::interval) x(t1)
LEFT JOIN entradas t ON t.fecha_hora >= x.t1
AND t.fecha_hora < x.t1 + interval '15 min'
GROUP BY 1
ORDER BY 1;
DISTINCT
与窗口函数结合使用对于查询规划器来说通常要贵得多(并且难以估计)。
CTE不是必需的,通常比子查询更昂贵。由于CTE是优化障碍,因此对查询计划者也更难估计。
看起来你想要整整一天,但你错过了最后15分钟。使用更简单的generate_series()
表达式来覆盖整天(仍然不会与相邻日期重叠)。
接下来,为什么您还有fecha_hora timestamp
with time zone
,而您还有fecha date
和hora time [without time zone]
?看起来它应该是fecha_hora timestamp
并删除多余的列?
这也可以避免与generate_series()
表达式的数据类型产生细微差别 - 这通常不应该是一个问题,但timestamp
取决于会话的时区而不是IMMUTABLE
比如timestamptz
。
如果该方法不够好,请添加一个冗余的WHERE
条件advised by @Daniel来指示查询计划程序。
错误计划的基本建议也适用:
答案 2 :(得分:1)
如果您的表是新的并且最近添加了行,则postgres可能没有收集到有关新数据的足够统计信息。如果是这种情况,您可以尝试分析表格。
PS:确保表格上的统计目标未设置为零。
答案 3 :(得分:1)
在索引使用方面,查询规划器尝试对有关执行查询的最佳方法进行有根据的猜测(基于可用索引,表统计信息和查询本身等)。有些情况下,即使使用索引的速度要快得多,它也总是会进行顺序扫描。只是在这些情况下,查询规划人员并不知道(在很多情况下,特别是当查询要返回大量行时,顺序扫描 更快)而不是做一堆索引扫描。)
基本上,这是一个案例的例子,在这种情况下,您比查询计划器更了解这个特定案例的数据(必须采用更通用,更广泛的外观,涵盖各种情况和可能的输入)。
对于您知道通过enable_seqscan=false
强制使用索引的情况,我不认为使用它有问题。我自己这样做是为了某些特定情况,否则将是一个巨大的性能接收器,我知道对于那些特定的查询,强制索引使用会导致查询速度提高几个数量级。
但有两件事要记住:
您应该始终确保在查询后立即重新启用顺序扫描,否则它将保留所有其他查询的其余连接,这可能不是您想要的。如果您的查询发生了一些变化,或者表中的数据显着增长,那么进行索引查询可能不再快,尽管这肯定是一个可以检验的事情。
使用 CTE 会对查询产生重大影响 规划人员有效优化查询的能力。我没有 在这种情况下认为这是问题的关键。