PostgreSQL 9.6 在对时间戳列进行聚合期间选择了错误的计划

Posted

技术标签:

【中文标题】PostgreSQL 9.6 在对时间戳列进行聚合期间选择了错误的计划【英文标题】:PostgreSQL 9.6 selects a wrong plan during aggregation against timestamp columns 【发布时间】:2017-08-15 20:02:43 【问题描述】:

我有一个简单但相当大的表“日志”,它包含三列:user_id、day、hours。

user_id character varying(36) COLLATE pg_catalog."default" NOT NULL,
day timestamp without time zone,
hours double precision

所有列都有索引。

问题是针对“day”字段的聚合工作非常缓慢。例如,简单的查询需要很长时间才能完成。

select min(day) from log where user_id = 'ab056f5a-390b-41d7-ba56-897c14b679bf'

分析表明 Postgres 会进行全面扫描,过滤与 user_id = 'ab056f5a-390b-41d7-ba56-897c14b679bf' 无关的条目,这绝对是违反直觉的

[
  
    "Execution Time": 146502.05,
    "Planning Time": 0.893,
    "Plan": 
      "Startup Cost": 789.02,
      "Actual Rows": 1,
      "Plans": [
        
          "Startup Cost": 0.44,
          "Actual Rows": 1,
          "Plans": [
            
              "Index Cond": "(log.day IS NOT NULL)",
              "Startup Cost": 0.44,
              "Scan Direction": "Forward",
              "Plan Width": 8,
              "Rows Removed by Index Recheck": 0,
              "Actual Rows": 1,
              "Node Type": "Index Scan",
              "Total Cost": 1395792.54,
              "Plan Rows": 1770,
              "Relation Name": "log",
              "Alias": "log",
              "Parallel Aware": false,
              "Actual Total Time": 146502.015,
              "Output": [
                "log.day"
              ],
              "Parent Relationship": "Outer",
              "Actual Startup Time": 146502.015,
              "Schema": "public",
              "Filter": "((log.user_id)::text = 'ab056f5a-390b-41d7-ba56-897c14b679bf'::text)",
              "Actual Loops": 1,
              "Rows Removed by Filter": 12665610,
              "Index Name": "index_log_day"
            
          ],
          "Node Type": "Limit",
          "Plan Rows": 1,
          "Parallel Aware": false,
          "Actual Total Time": 146502.016,
          "Output": [
            "log.day"
          ],
          "Parent Relationship": "InitPlan",
          "Actual Startup Time": 146502.016,
          "Plan Width": 8,
          "Subplan Name": "InitPlan 1 (returns $0)",
          "Actual Loops": 1,
          "Total Cost": 789.02
        
      ],
      "Node Type": "Result",
      "Plan Rows": 1,
      "Parallel Aware": false,
      "Actual Total Time": 146502.019,
      "Output": [
        "$0"
      ],
      "Actual Startup Time": 146502.019,
      "Plan Width": 8,
      "Actual Loops": 1,
      "Total Cost": 789.03
    ,
    "Triggers": []
  
]

更奇怪的是,几乎相似的查询完美地工作。

select min(hours) from log where user_id = 'ab056f5a-390b-41d7-ba56-897c14b679bf'

Postgres 首先选择 user_id = 'ab056f5a-390b-41d7-ba56-897c14b679bf' 的条目,然后在其中汇总明显正确的条目。

[
  
    "Execution Time": 5.989,
    "Planning Time": 1.186,
    "Plan": 
      "Partial Mode": "Simple",
      "Startup Cost": 6842.66,
      "Actual Rows": 1,
      "Plans": [
        
          "Startup Cost": 66.28,
          "Plan Width": 8,
          "Rows Removed by Index Recheck": 0,
          "Actual Rows": 745,
          "Plans": [
            
              "Startup Cost": 0,
              "Plan Width": 0,
              "Actual Rows": 745,
              "Node Type": "Bitmap Index Scan",
              "Index Cond": "((log.user_id)::text = 'ab056f5a-390b-41d7-ba56-897c14b679bf'::text)",
              "Plan Rows": 1770,
              "Parallel Aware": false,
              "Actual Total Time": 0.25,
              "Parent Relationship": "Outer",
              "Actual Startup Time": 0.25,
              "Total Cost": 65.84,
              "Actual Loops": 1,
              "Index Name": "index_log_user_id"
            
          ],
          "Recheck Cond": "((log.user_id)::text = 'ab056f5a-390b-41d7-ba56-897c14b679bf'::text)",
          "Exact Heap Blocks": 742,
          "Node Type": "Bitmap Heap Scan",
          "Plan Rows": 1770,
          "Relation Name": "log",
          "Alias": "log",
          "Parallel Aware": false,
          "Actual Total Time": 5.793,
          "Output": [
            "day",
            "hours",
            "user_id"
          ],
          "Lossy Heap Blocks": 0,
          "Parent Relationship": "Outer",
          "Actual Startup Time": 0.357,
          "Total Cost": 6838.23,
          "Actual Loops": 1,
          "Schema": "public"
        
      ],
      "Node Type": "Aggregate",
      "Strategy": "Plain",
      "Plan Rows": 1,
      "Parallel Aware": false,
      "Actual Total Time": 5.946,
      "Output": [
        "min(hours)"
      ],
      "Actual Startup Time": 5.946,
      "Plan Width": 8,
      "Actual Loops": 1,
      "Total Cost": 6842.67
    ,
    "Triggers": []
  
]

有两种可能的解决方法:

1) 将查询重写为:

select user_id, min(day) from log where user_id = 'ac43a155-4fbb-49eb-a670-02c307eb3d4f' group by user_id

2) 像 finding MAX(db_timestamp) query 中建议的那样引入对索引

它们可能看起来不错,但我认为这两种方式都可以解决问题(第一个甚至是 hack)。从逻辑上讲,如果 Postgres 可以为“小时”选择合适的计划,它必须为“天”选择合适的计划,但事实并非如此。所以它看起来像是在时间戳字段聚合期间发生的 Postgres 错误,但我承认我可能会错过一些东西。有人可以告诉我是否可以在不使用 WA 的情况下在这里完成某些事情,或者这确实是 Postgres 错误,我必须报告它?

UPD:我已将此作为一个错误报告给 PostgreSQL 错误邮件列表。如果被接受,我会通知大家。

【问题讨论】:

您是否收集了表格的统计数据? 我有统计收集的默认设置,相信应该自动收集。所以我需要对统计做一些明确的事情吗? 顺便说一句:user_id,day 对我来说似乎是候选键。 【参考方案1】:

Min 是聚合函数,而不是运算符。必须对所有匹配的记录执行函数。 选择部分中的字段不影响计划。 From ... join ... where ... group by ... order by - 所有这些都在计划中考虑。 试试:

select day from log where user_id = 'ab056f5a-390b-41d7-ba56-897c14b679bf'
order by user_id, day
limit 1

【讨论】:

感谢您的建议!我试过了,但不幸的是得到了同样糟糕的计划。但是我之前发现的 WA 有效: select user_id, min(day) from log where user_id = 'ac43a155-4fbb-49eb-a670-02c307eb3d4f' group by user_id 所以我将它用于我的解决方案。我理解聚合函数的局限性,这没关系,但问题是为什么 Postrgres 没有像我通过双字段聚合时那样首先通过 user_id 限制行?我已经向 Postgres 团队 (#14780) 提交了一个错误,它现在处于预审阶段。让我们看看他们说了什么。【参考方案2】:

我收到了 PostgreSQL 的回复。他们不认为这是一个错误。在这种情况下可能存在 WA,其中许多在原始帖子中以及后来的 cmets 中都有提及。我个人的选择是最初提到的第一个选项,因为它不需要索引操作(远不总是可能的)。所以解决方法是将查询重写为:

select user_id, min(day) from log where user_id = 'ac43a155-4fbb-49eb-a670-02c307eb3d4f' group by user_id

【讨论】:

【参考方案3】:

看到这篇文章有一些玩索引的顺序 - PostgreSQL index not used for query on range

https://dba.stackexchange.com/questions/39589/optimizing-queries-on-a-range-of-timestamps-two-columns

还有一个想法是

select min(day) from (
   select day from log 
      where user_id = 'ac43a155-4fbb-49eb-a670-02c307eb3d4f'
) q

附言另外你能确认autovacuum (verbose, analyze) 是为表执行的吗?

【讨论】:

谢谢!我不执行 autovacuum 。数据库很新,所以应该没问题,但还是请让我试试吧。 抽真空了。没有效果。它甚至变得更糟了:) 尝试按顺序创建索引...不确定是否有帮助 CREATE INDEX my_idx ON my_table (day ASC); 然后使用分析进行自动清空 我试过: select min(day) from ( select day from log where user_id = 'ac43a155-4fbb-49eb-a670-02c307eb3d4f' ) q 还是有同样的计划。

以上是关于PostgreSQL 9.6 在对时间戳列进行聚合期间选择了错误的计划的主要内容,如果未能解决你的问题,请参考以下文章

odoo 的postgresql数据库可以用9.6吗

无法将 PostgreSQL10 转储导入 9.6 数据库

解决重置PostgreSQL 9.6密码的问题

postgresql9.6安装

如何在 Mac OS 上使用自制软件将 postgresql 从 10.1 降级到 9.6 [关闭]

postgresql 9.6 rpm包安装 CentOS 7.2 X64