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 强制使用索引的情况,我认为使用它没有问题。对于某些特定情况,我自己会这样做,否则会造成巨大的性能下降,而且我知道对于那些特定的查询,强制使用索引会导致查询速度提高几个数量级。

尽管如此,有两点需要牢记:

  1. 您应该始终确保在查询后立即重新启用顺序扫描,否则它会在所有其他查询的其余连接中保留,这不太可能是您想要的。如果你的查询有点变化,或者如果 table 中的数据增长显着,那么做索引查询可能不再更快,尽管这肯定是一个 testable 的事情。

  2. 使用CTE会对查询产生重大影响 规划器有效优化查询的能力。我不 我认为这是本案的症结所在。

错误估计分析

这里问题的要点是 postgres 规划器不知道 generate_series 调用会产生什么值和多少行,但必须估计其中有多少会满足 JOIN条件对大entradastable。在你的情况下,它失败了很长时间。

现实中只有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 timestampwith time zone,同时你还有fecha datehora time [without time zone]?看起来应该是 fecha_hora timestamp 并删除多余的列?
这也可以避免 generate_series() 表达式的数据类型的细微差别 - 这通常不应该成为问题,但 timestamp 取决于会话的时区而不是 IMMUTABLEtimestamptz.

如果这还不够好,添加一个冗余的 WHERE 条件作为 来指示查询计划器。

针对糟糕计划的基本建议也适用:

  • Keep PostgreSQL from sometimes choosing a bad query plan