优化大表查询执行 generate_series()

Posted

技术标签:

【中文标题】优化大表查询执行 generate_series()【英文标题】:Optimize query on big table executing generate_series() 【发布时间】:2019-06-12 00:47:34 【问题描述】:

以下查询在 PostgreSQL 11.1 中耗时超过 7 分钟:

SELECT 
    '2019-01-19' as date, 
    '2019-01-19'::date - generate_series(first_observed, last_observed, interval '1 day')::date as days_to_date, 
    ROUND(AVG(price)) as price,
    area_id
FROM 
    table_example
GROUP BY 
    days_to_date, area_id;

table_example 大约有 1500 万行。 有什么方法可以优化吗?我已经添加了以下索引:

CREATE INDEX ON table_example (first_observed, last_observed);
CREATE INDEX ON table_example (area_id);

这是EXPLAIN (ANALYZE,BUFFERS)的输出:

GroupAggregate  (cost=3235559683.68..3377398628.68 rows=1418000 width=72) (actual time=334933.966..440096.869 rows=21688 loops=1)
  Group Key: (('2019-01-19'::date - ((generate_series((first_observed)::timestamp with time zone, (last_observed)::timestamp with time zone, '1 day'::interval)))::date)), area_id
  Buffers: local read=118167 dirtied=118167 written=117143, temp read=1634631 written=1635058
  ->  Sort  (cost=3235559683.68..3271009671.18 rows=14179995000 width=40) (actual time=334923.933..391690.184 rows=380203171 loops=1)
        Sort Key: (('2019-01-19'::date - ((generate_series((first_observed)::timestamp with time zone, (last_observed)::timestamp with time zone, '1 day'::interval)))::date)), area_id
        Sort Method: external merge  Disk: 9187584kB
        Buffers: local read=118167 dirtied=118167 written=117143, temp read=1634631 written=1635058
        ->  Result  (cost=0.00..390387079.39 rows=14179995000 width=40) (actual time=214.798..171717.941 rows=380203171 loops=1)
              Buffers: local read=118167 dirtied=118167 written=117143
              ->  ProjectSet  (cost=0.00..71337191.89 rows=14179995000 width=44) (actual time=214.796..102823.749 rows=380203171 loops=1)
                    Buffers: local read=118167 dirtied=118167 written=117143
                    ->  Seq Scan on table_example  (cost=0.00..259966.95 rows=14179995 width=44) (actual time=0.031..2449.511 rows=14179995 loops=1)
                          Buffers: local read=118167 dirtied=118167 written=117143
Planning Time: 0.409 ms
JIT:
  Functions: 18
  Options: Inlining true, Optimization true, Expressions true, Deforming true
  Timing: Generation 5.034 ms, Inlining 13.010 ms, Optimization 121.440 ms, Emission 79.996 ms, Total 219.480 ms
Execution Time: 441133.410 ms

这就是 table_example 的样子:

column name        data type
'house_pk'         'integer'    
'date_in'          'date'   
'first_observed'   'date'   
'last_observed'    'date'   
'price'            'numeric'    
'area_id'          'integer'    

有 60 个不同的 area_id。

查询正在具有 128 GB 内存的多核机器(24 核)上运行。但是,设置可能不是最佳的。

【问题讨论】:

数据量很大——而且查询中没有过滤器。我怀疑你能否得到很大的改进。 表定义会有所帮助 - CREATE TABLE 带有数据类型和约束的语句。加上EXPLAIN (ANALYZE, BUFFERS) 的输出。请参阅:***.com/tags/postgresql-performance/info 您是否需要同时获得所有日期和所有区域的结果?您是否有一个 area 表,每个相关 area_id 有 1 行?有多少不同的area_idfirst_observedlast_observed - min/max/avg 之间有多少天? @ErwinBrandstetter 1) 我现在添加了EXPLAIN (ANALYZE, BUFFERS) 的信息。 2)我确实需要一次所有日子的结果。我可以对区域进行多次查询,但这需要进行 60 次查询而不是 1 次。 3)我不确定你的意思。我确实有一个包含 area_ids 的表,每个 area_id 一行。你是这个意思吗? 4) 60 area_ids, 5) min = 0, max=335, avg=25.8 完整的CREATE TABLE 声明是披露有关您的表的信息的方式。示例:***.com/questions/9789736/… 各种客户端(包括 pgAdmin3 和 pgAdmin4)显示反向工程语句。 【参考方案1】:

在处理整个表时,索引通常是无用的(如果表行比索引宽得多,则仅索引扫描可能例外)。

在处理整个表时,我认为查询本身的性能优化空间不大。一件小事:

SELECT d.the_date
     , generate_series(d.the_date - last_observed
                     , d.the_date - first_observed) AS days_to_date
     , round(avg(price)) AS price
     , area_id
FROM   table_example
     , (SELECT date '2019-01-19') AS d(the_date)
GROUP  BY days_to_date, area_id;

假设first_observedlast_observeddate NOT NULL 并且总是< date '2019-01-19'。否则你需要施放/做更多。

这样,你只有两个减法,然后generate_series() 处理整数(最快)。

添加的迷你子查询只是为了方便,只提供一次日期。在准备好的语句或函数中,您可以使用参数而不需要这个:

     , (SELECT date '2019-01-19') AS d(the_date)

除此之外,如果EXPLAIN (ANALYZE, BUFFERS) 提到“磁盘”(例如:Sort Method: external merge Disk: 3240kB),那么work_mem 的(临时)更高设置应该会有所帮助。见:

Configuration parameter work_mem in PostgreSQL on Linux Optimize simple query using ORDER BY date and text

如果您买不起更多 RAM,并且聚合和/或排序步骤仍会溢出到磁盘,则使用 LATERAL 连接之类的查询可能有助于分而治之:

SELECT d.the_date, f.*, a.area_id
FROM   area a
     , (SELECT date '2019-01-19') AS d(the_date)
     , LATERAL (
   SELECT generate_series(d.the_date - last_observed
                        , d.the_date - first_observed) AS days_to_date
        , round(avg(price)) AS price
   FROM   table_example
   WHERE  area_id = a.area_id
   GROUP  BY 1
   ) f;

很明显,假设有一张桌子area

【讨论】:

惊人的差异。从 6 分钟到 8 秒。对于其他阅读此答案,请注意使用 generate_series 的两种方式的结果不相同。快速方法仅创建价格发生变化的行。对于我的用例,这实际上更好,但在某些情况下,您可能希望每天连续排列一行。 再看一遍,我意识到first_observedlast_observed 需要在我的两个查询中切换,如果last_observed 预计晚于first_observed .所以我很困惑为什么这对你有用。这里有 3 个查询(我的回答中有 2 个,问题中有 1 个),哪个需要 6 分钟,哪个需要 8 秒? (我在你的问题中看到 7.4 分钟。)并且:where there is a change in price - 这是什么意思? 你是对的。它适用于一种特定的边缘情况,但通常不起作用。它们需要翻转,大部分性能增益都消失了。我的 cmets 与使用 LATERAL 的查询无关。 所以更像是 7.4 到 4.9 分钟,这仍然是一个很大的收获。 @Wessi:我翻转了日期以相应地修复查询。是的,对于一点优化魔法来说仍然是一个惊人的收获。然而,为了让它更快,请考虑一下我写的关于 work_mem 的内容 - 可能不会有太大变化(取决于您的硬件),但避免溢出到磁盘通常要快得多。

以上是关于优化大表查询执行 generate_series()的主要内容,如果未能解决你的问题,请参考以下文章

求助Oracle大表查询优化

MySQL 对于千万级的大表要怎么优化

大表的 MySQL 查询优化

大表连接的mysql查询优化

hive大表和小表MapJoin关联查询优化

如何优化这个大表SQL查询的响应时间?