当索引扫描会更好时,为什么 PostgreSQL 选择慢速 Seq 扫描?

yan*_*kee 5 postgresql performance optimization postgresql-9.6 query-performance

我在 Postgres 9.6 DB 中有以下表格:

create table baseDimensions(
    id serial not null primary key,
    dimension1 date not null,
    dimension2 int not null,
    dimension3 text not null,
    -- ...
    dimension10 boolean not null,
    unique(dimension1, dimension2, ..., dimension10)
);

create table interestingData(
    baseDimensionId int not null references baseDimensions(id),
    subdimension1 int not null,
    subdimension2 boolean not null,
    -- ...
    value1 int not null,
    value2 bigint not null,
    -- ...
    primary key(baseDimensionId, subdimension1, subdimension2)
)
Run Code Online (Sandbox Code Playgroud)

所以在文本中:我有很多维度(例如,如果表格用于零售店的销售,则维度可以是销售日期、customerId、客户是否是重复客户、客户的夹克颜色穿着等)以及这些尺寸的一些值(例如,保持零售示例:值可以是客户支付的金额)。然而,有多个“interestingData”表共享十个基本维度,因此为了节省一些磁盘空间,我将这些基本维度提取到一个单独的表中。

我特意选择了唯一索引中列的顺序,以便典型的过滤条件在最左边。所以大部分时间我会按维度1过滤数据。

现在我想知道value1特定日期的平均值(dimension1每个dimension3)。这可以通过以下查询来回答:

select dimension3, avg(value1)
from baseDimensions b
join interestingData c on b.id = c.baseDimensionId
where b.dimension1 = '2016-08-20'::date
group by dimension3
Run Code Online (Sandbox Code Playgroud)

此查询的解释+分析如下所示:

HashAggregate  (cost=1141685.72..1141686.14 rows=28 width=41) (actual time=55824.222..55827.068 rows=3358 loops=1)
  Group Key: b.dimension3
  ->  Hash Join  (cost=63915.68..1139740.91 rows=259308 width=41) (actual time=9956.393..55684.492 rows=267004 loops=1)
        Hash Cond: (c.basedimensionid = b.id)
        ->  Seq Scan on interestingData c  (cost=0.00..628619.84 rows=28705684 width=20) (actual time=0.007..31909.112 rows=28705684 loops=1)
        ->  Hash  (cost=62303.57..62303.57 rows=83369 width=29) (actual time=93.587..93.587 rows=81101 loops=1)
              Buckets: 131072  Batches: 2  Memory Usage: 3565kB
              ->  Index Scan using baseDimensions_dimensions_key on baseDimensions b  (cost=0.56..62303.57 rows=83369 width=29) (actual time=0.021..59.422 rows=81101 loops=1)
                    Index Cond: (dimension1 = '2016-08-20'::date)
Planning time: 0.232 ms
Execution time: 55827.909 ms
Run Code Online (Sandbox Code Playgroud)

55 秒这个查询很慢,我的解释是原因是对表interestingData 的序列扫描。但是查询计划器就在这里吗?使用索引会更慢吗?我想知道,所以我试图用set enable_seqscan=false. 这是新的解释+分析:

HashAggregate  (cost=1790548.21..1790548.63 rows=28 width=41) (actual time=1023.655..1025.661 rows=3358 loops=1)
  Group Key: b.dimension3
  ->  Nested Loop  (cost=1.12..1788603.40 rows=259308 width=41) (actual time=0.034..848.152 rows=267004 loops=1)
        ->  Index Scan using baseDimensions_dimensions_key on baseDimensions b  (cost=0.56..62303.57 rows=83369 width=29) (actual time=0.019..76.750 rows=81101 loops=1)
              Index Cond: (dimension1 = '2016-08-20'::date)
        ->  Index Scan using interestingData_pkey on interestingData c  (cost=0.56..20.08 rows=63 width=20) (actual time=0.003..0.007 rows=3 loops=81101)
              Index Cond: (basedimensionid = b.id)
Planning time: 0.250 ms
Execution time: 1026.478 ms
Run Code Online (Sandbox Code Playgroud)

哇……查询速度突然提高了 50 倍以上!

但是我不应该在 production 中使用 set enable_seqscan=false那么为什么查询计划器的表现如此糟糕,我该怎么办呢?

统计数据

表格统计信息以更好地了解情况(表格将来会增长很多,因为每天都有新数据到达,但目前不打算删除旧数据):

HashAggregate  (cost=1141685.72..1141686.14 rows=28 width=41) (actual time=55824.222..55827.068 rows=3358 loops=1)
  Group Key: b.dimension3
  ->  Hash Join  (cost=63915.68..1139740.91 rows=259308 width=41) (actual time=9956.393..55684.492 rows=267004 loops=1)
        Hash Cond: (c.basedimensionid = b.id)
        ->  Seq Scan on interestingData c  (cost=0.00..628619.84 rows=28705684 width=20) (actual time=0.007..31909.112 rows=28705684 loops=1)
        ->  Hash  (cost=62303.57..62303.57 rows=83369 width=29) (actual time=93.587..93.587 rows=81101 loops=1)
              Buckets: 131072  Batches: 2  Memory Usage: 3565kB
              ->  Index Scan using baseDimensions_dimensions_key on baseDimensions b  (cost=0.56..62303.57 rows=83369 width=29) (actual time=0.021..59.422 rows=81101 loops=1)
                    Index Cond: (dimension1 = '2016-08-20'::date)
Planning time: 0.232 ms
Execution time: 55827.909 ms
Run Code Online (Sandbox Code Playgroud)
HashAggregate  (cost=1790548.21..1790548.63 rows=28 width=41) (actual time=1023.655..1025.661 rows=3358 loops=1)
  Group Key: b.dimension3
  ->  Nested Loop  (cost=1.12..1788603.40 rows=259308 width=41) (actual time=0.034..848.152 rows=267004 loops=1)
        ->  Index Scan using baseDimensions_dimensions_key on baseDimensions b  (cost=0.56..62303.57 rows=83369 width=29) (actual time=0.019..76.750 rows=81101 loops=1)
              Index Cond: (dimension1 = '2016-08-20'::date)
        ->  Index Scan using interestingData_pkey on interestingData c  (cost=0.56..20.08 rows=63 width=20) (actual time=0.003..0.007 rows=3 loops=81101)
              Index Cond: (basedimensionid = b.id)
Planning time: 0.250 ms
Execution time: 1026.478 ms
Run Code Online (Sandbox Code Playgroud)

变体

应 ypercube 的要求(在评论中),一个带有(dimension1、id、dimension3)索引的变体:

HashAggregate  (cost=1141043.72..1141044.14 rows=28 width=41) (actual time=45213.910..45217.416 rows=3358 loops=1)
  Group Key: b.dimension3
  ->  Hash Join  (cost=63273.68..1139098.91 rows=259308 width=41) (actual time=6871.808..45082.855 rows=267004 loops=1)
        Hash Cond: (combined.basedimensionid = b.id)
        ->  Seq Scan on interestingData c  (cost=0.00..628619.84 rows=28705684 width=20) (actual time=0.007..22862.174 rows=28705684 loops=1)
        ->  Hash  (cost=61661.57..61661.57 rows=83369 width=29) (actual time=67.638..67.638 rows=81101 loops=1)
              Buckets: 131072  Batches: 2  Memory Usage: 3565kB
              ->  Index Only Scan using dim1_id_dim3_idx on baseDimensions b  (cost=0.56..61661.57 rows=83369 width=29) (actual time=0.060..36.704 rows=81101 loops=1)
                    Index Cond: (dimension1 = '2016-08-20'::date)
                    Heap Fetches: 81101
Planning time: 0.265 ms
Execution time: 45218.174 ms
Run Code Online (Sandbox Code Playgroud)

我也试过用index(dimension1,dimension3,id),但是解释结果完全一样。

jja*_*nes 5

看起来通过嵌套循环解决此查询所需的所有数据都已缓存在 RAM 中。PostgreSQL 的规划器不会意识到这一点,它假设必须访问磁盘才能获取大量数据。

如果所有内容都被缓存,那么这可能会公平或不公平地发生。

很可能您有大量 RAM,并且大多数数据大部分时间都在缓存中。在这种情况下,降低random_page_cost到相同或几乎相同才是seq_page_cost正确的响应。但请注意,如果您重新启动服务器,当缓存从磁盘重新加载时,您可能会经历一段痛苦的预热期。

另一个“相当在缓存中”是,您实际上经常在生产中使用完全相同的参数(2016-08-20)运行完全相同的查询,因此即使大多数数据确实如此,该特定数据集仍保留在内存中不是。在这种情况下,降低random_page_cost可以修复这个特定的查询,但会使其他查询变得更糟。一种解决方案是仅针对此查询降低random_page_cost或设置并在之后重置它(如果您的框架允许您这样做)。enable_seqscan=off

“缓存中不公平”是指您继续使用参数 2016-08-20 测试此查询,而您的实际查询每次都会使用不同的参数。这意味着您的性能测试服务器可以一遍又一遍地重复使用相同的数据,而无需从磁盘读取数据,而您的生产服务器则无法这样做。在这种情况下,您需要改进您的测试/基准测试方法,使其更加现实。