【问题标题】:Postgresql ignoring index on timestamp column even if query is faster using indexPostgresql 忽略时间戳列上的索引,即使使用索引查询更快
【发布时间】:2015-03-08 04:35:36
【问题描述】:

在 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 的输出显示它正在使用时间戳列上的索引:

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

【问题讨论】:

  • CTE "x" 仅涵盖 "entradas" 的一小部分对于规划者来说可能并不明显,您也可以尝试将这些限制添加到 where 子句中。
  • 尝试不使用 CTE,这是查询规划器的已知障碍。

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


【解决方案1】:

如果您的表是新表并且行是最近添加的,则 postgres 可能没有收集到有关新数据的足够统计信息。如果是这种情况,您可以尝试分析表。

PS:确保表上的统计目标没有设置为零。

【讨论】:

  • 我不确定它是否有区别,但我记得如果类型不同,可以避免使用索引。
【解决方案2】:

在索引使用方面,查询规划器尝试对执行查询的最佳方式做出有根据的猜测(基于可用索引、表统计信息和查询本身等)。在某些情况下,即使使用索引会快得多,它也总是会执行顺序扫描。只是查询规划器不知道在这些情况下(在许多情况下,特别是当查询要返回很多行时,顺序扫描比做一堆索引要快扫描)。

本质上,这是一个示例,在这种情况下,您比查询计划器更了解这个非常具体的案例的数据(它必须采取更通用、更广泛的视角,涵盖各种案例和可能的输入)。

对于这样的情况,您知道通过enable_seqscan=false 强制使用索引,我认为使用它没有问题。对于某些特定情况,我自己会这样做,否则会导致巨大的性能下降,而且我知道对于那些特定的查询,强制使用索引会导致查询速度提高几个数量级。

不过,有两点需要牢记:

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

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

【讨论】:

    【解决方案3】:

    错误估计分析

    这里的问题的要点是 postgres 计划器不知道 generate_series 调用中有哪些值和多少行,但必须估计其中有多少将满足 JOIN 条件而不是大entradas 表。在你的情况下,它失败了。

    实际上,只有一小部分表会被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行,是大表行数的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范围包含上界或者一般大一点都没有关系,会被其他条件严格过滤掉)。

    然后计划者应该能够利用统计信息,认识到只有一小部分索引与此值范围有关,并使用索引而不是顺序扫描整个大表。

    【讨论】:

    • 嗯,这似乎确实解决了问题,但我喜欢@Erwin 提出的方法。它使查询方式更简单,速度更快
    • @plablo09:总结一下,你能用enable_seqscan 来显示Erwin 修改后的查询的执行计划吗?这无疑是对简单性的改进,但我看不出这会如何改变 JOIN 估计。
    • 我使用两种变体进行了测试:一种带有您提出的文件管理器条件,另一种没有。事实证明,您的过滤器确实提高了 JOIN 估计。此外,我以两种不同的方式重新创建了表:使用 csv 文件中的 COPY 和使用 SELECT INTO 语句(在这两种情况下我都重新创建了索引),在这两种情况下,没有过滤器的查询大约需要 80 秒,而使用过滤器的查询需要不到2秒。我已将 EXPLAIN here 的结果放入。问题是在原始数据库中,两个版本几乎都花费了相同的时间
    【解决方案4】:

    您可以大大简化您的查询:

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

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

    对于不良计划的基本建议也适用:

    【讨论】:

    • 好吧,fechahora 列仅用于遗留问题,不再真正使用。是的,我错过了最后 15 分钟,谢谢!总的来说,这是一个优雅的解决方案,表明我必须从简单的角度考虑
    猜你喜欢
    • 1970-01-01
    • 2021-05-04
    • 2016-09-16
    • 1970-01-01
    • 2012-05-23
    • 2014-09-05
    • 2012-05-21
    • 2019-10-27
    相关资源
    最近更新 更多