Postgresql 忽略时间戳列上的索引,即使使用索引查询速度更快
Postgresql ignoring index on timestamp column even if query is faster using index
在 postgresql 9.3 上,我有一个 table 有超过一百万条记录,table 创建为:
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的输出可以看出没有使用timestamp索引:
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 的输出显示它正在使用时间戳上的索引专栏:
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
,除非我强制它使用它,即使使用它执行查询速度更快?
如何让 postgres 使用 entradas_date_idx
而不求助于 set enable_seqscan=false
?
如果你的 table 是新的并且行是最近添加的,postgres 可能没有收集足够的关于新数据的统计信息。如果是这种情况,您可以尝试分析 table.
PS:确保 table 上的统计目标未设置为零。
在索引使用方面,查询计划器尝试做出有根据的猜测(基于可用索引、table 统计信息和查询本身等)关于执行索引的最佳方式询问。在某些情况下,它总是会以顺序扫描结束,即使使用索引会快得多。只是查询规划器不知道在那些情况下(在许多情况下,特别是当查询要 return 很多行时,顺序扫描 是 比做一堆索引扫描更快)。
本质上,这是一个案例示例,您比查询规划器更了解这个非常具体案例的数据(查询规划器必须采用更通用、更广泛的外观,涵盖各种案例和可能的输入).
对于这种您知道通过 enable_seqscan=false
强制使用索引的情况,我认为使用它没有问题。对于某些特定情况,我自己会这样做,否则会造成巨大的性能下降,而且我知道对于那些特定的查询,强制使用索引会导致查询速度提高几个数量级。
尽管如此,有两点需要牢记:
您应该始终确保在查询后立即重新启用顺序扫描,否则它会在所有其他查询的其余连接中保留,这不太可能是您想要的。如果你的查询有点变化,或者如果 table 中的数据增长显着,那么做索引查询可能不再更快,尽管这肯定是一个 testable 的事情。
使用CTE会对查询产生重大影响
规划器有效优化查询的能力。我不
我认为这是本案的症结所在。
错误估计分析
这里问题的要点是 postgres 规划器不知道 generate_series
调用会产生什么值和多少行,但必须估计其中有多少会满足 JOIN条件对大entradas
table。在你的情况下,它失败了很长时间。
现实中只有table的一小部分会被join,但是估计在反面出现错误,如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
行,是big table!
行数的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范围包括上界或者一般大一点都无所谓,会被其他条件严格过滤掉)
然后规划者应该能够利用统计数据,认识到只有一小部分索引与这个值范围有关,并使用索引而不是顺序扫描整个大 table.
您可以大大简化您的查询:
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
与 window 函数的结合对于查询规划器来说通常要昂贵得多(也更难估计)。
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
条件作为 来指示查询计划器。
针对糟糕计划的基本建议也适用:
- Keep PostgreSQL from sometimes choosing a bad query plan
在 postgresql 9.3 上,我有一个 table 有超过一百万条记录,table 创建为:
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的输出可以看出没有使用timestamp索引:
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 的输出显示它正在使用时间戳上的索引专栏:
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
,除非我强制它使用它,即使使用它执行查询速度更快?
如何让 postgres 使用 entradas_date_idx
而不求助于 set enable_seqscan=false
?
如果你的 table 是新的并且行是最近添加的,postgres 可能没有收集足够的关于新数据的统计信息。如果是这种情况,您可以尝试分析 table.
PS:确保 table 上的统计目标未设置为零。
在索引使用方面,查询计划器尝试做出有根据的猜测(基于可用索引、table 统计信息和查询本身等)关于执行索引的最佳方式询问。在某些情况下,它总是会以顺序扫描结束,即使使用索引会快得多。只是查询规划器不知道在那些情况下(在许多情况下,特别是当查询要 return 很多行时,顺序扫描 是 比做一堆索引扫描更快)。
本质上,这是一个案例示例,您比查询规划器更了解这个非常具体案例的数据(查询规划器必须采用更通用、更广泛的外观,涵盖各种案例和可能的输入).
对于这种您知道通过 enable_seqscan=false
强制使用索引的情况,我认为使用它没有问题。对于某些特定情况,我自己会这样做,否则会造成巨大的性能下降,而且我知道对于那些特定的查询,强制使用索引会导致查询速度提高几个数量级。
尽管如此,有两点需要牢记:
您应该始终确保在查询后立即重新启用顺序扫描,否则它会在所有其他查询的其余连接中保留,这不太可能是您想要的。如果你的查询有点变化,或者如果 table 中的数据增长显着,那么做索引查询可能不再更快,尽管这肯定是一个 testable 的事情。
使用CTE会对查询产生重大影响 规划器有效优化查询的能力。我不 我认为这是本案的症结所在。
错误估计分析
这里问题的要点是 postgres 规划器不知道 generate_series
调用会产生什么值和多少行,但必须估计其中有多少会满足 JOIN条件对大entradas
table。在你的情况下,它失败了很长时间。
现实中只有table的一小部分会被join,但是估计在反面出现错误,如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
行,是big table!
欺骗计划者进行更好的估计
由于我们知道 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范围包括上界或者一般大一点都无所谓,会被其他条件严格过滤掉)
然后规划者应该能够利用统计数据,认识到只有一小部分索引与这个值范围有关,并使用索引而不是顺序扫描整个大 table.
您可以大大简化您的查询:
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
与 window 函数的结合对于查询规划器来说通常要昂贵得多(也更难估计)。
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
条件作为
针对糟糕计划的基本建议也适用:
- Keep PostgreSQL from sometimes choosing a bad query plan