即使查询使用索引更快,Postgresql也会忽略timestamp列上的索引

时间:2015-01-09 19:17:44

标签: sql postgresql postgresql-9.3 postgresql-performance sql-execution-plan

在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?

4 个答案:

答案 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 datehora 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强制使用索引的情况,我不认为使用它有问题。我自己这样做是为了某些特定情况,否则将是一个巨大的性能接收器,我知道对于那些特定的查询,强制索引使用会导致查询速度提高几个数量级。

但有两件事要记住:

  1. 您应该始终确保在查询后立即重新启用顺序扫描,否则它将保留所有其他查询的其余连接,这可能不是您想要的。如果您的查询发生了一些变化,或者表中的数据显着增长,那么进行索引查询可能不再快,尽管这肯定是一个可以检验的事情。

  2. 使用 CTE 会对查询产生重大影响 规划人员有效优化查询的能力。我没有 在这种情况下认为这是问题的关键。