PostgreSQL没有使用直接索引

时间:2019-03-06 17:50:09

标签: postgresql indexing amazon-rds postgresql-performance

我在Amazon RDS上有一个PostgreSQL 10.6数据库。我的桌子是这样的:

CREATE TABLE dfo_by_quarter (
    release_key int4 NOT NULL,
    country varchar(100) NOT NULL,
    product_group varchar(100) NOT NULL,
    distribution_type varchar(100) NOT NULL,
    "year" int2 NOT NULL,
    "date" date NULL,
    quarter int2 NOT NULL,
    category varchar(100) NOT NULL,
    units numeric(38,6) NOT NULL,
    sales_value_eur numeric(38,6) NOT NULL,
    sales_value_usd numeric(38,6) NOT NULL,
    sales_value_local numeric(38,6) NOT NULL,
    data_status bpchar(1) NOT NULL,
    panel_market_units numeric(38,6) NOT NULL,
    panel_market_sales_value_eur numeric(38,6) NOT NULL,
    panel_market_sales_value_usd numeric(38,6) NOT NULL,
    panel_market_sales_value_local numeric(38,6) NOT NULL,
    CONSTRAINT pk_dpretailer_dfo_by_quarter PRIMARY KEY (release_key, country, category, product_group, distribution_type, year, quarter),
    CONSTRAINT fk_dpretailer_dfo_by_quarter_release FOREIGN KEY (release_key) REFERENCES dpretailer.dfo_release(release_id)
);

我了解主键意味着唯一索引

如果我简单地询问对不存在的数据进行过滤时有多少行(release_key = 1则不返回任何内容),我可以看到它使用了索引

EXPLAIN
SELECT COUNT(*)
  FROM dpretailer.dfo_by_quarter
  WHERE release_key = 1

Aggregate  (cost=6.32..6.33 rows=1 width=8)
  ->  Index Only Scan using pk_dpretailer_dfo_by_quarter on dfo_by_quarter  (cost=0.55..6.32 rows=1 width=0)
        Index Cond: (release_key = 1)

但是,如果我对返回数据的值运行相同的查询,它将扫描该表,这势必会更加昂贵...

EXPLAIN
SELECT COUNT(*)
  FROM dpretailer.dfo_by_quarter
  WHERE release_key = 2

Finalize Aggregate  (cost=47611.07..47611.08 rows=1 width=8)
  ->  Gather  (cost=47610.86..47611.07 rows=2 width=8)
        Workers Planned: 2
        ->  Partial Aggregate  (cost=46610.86..46610.87 rows=1 width=8)
              ->  Parallel Seq Scan on dfo_by_quarter  (cost=0.00..46307.29 rows=121428 width=0)
                    Filter: (release_key = 2)

我认为在没有数据的情况下使用索引是有意义的,并且由表上的统计信息决定(我在测试之前就运行了ANALYZE)

但是如果有数据,为什么不使用我的索引呢?

当然,扫描索引的一部分(因为release_key是第一列)必须比扫描整个表更快一些?

我一定想念一些东西吗??

更新2019-03-07

谢谢您的评论,这非常有用。

这个简单的查询只是我试图理解为什么不使用索引的原因...

但是我应该了解得更多(我是Postgresql的新手,但是有很多SQL Server的工作经验),并且有道理,正如您所评论的那样,这是有道理的。

  • 选择性差,因为我的标准只能过滤大约20%的行
  • 错误的表格设计(太胖了,我们知道并且正在解决)
  • 索引未“覆盖”查询,等等...

如果可以的话,让我“稍微”改变我的问题...

我们的表格将按照事实/维度进行归一化处理(在错误的位置不再有varchars)。

我们只执行插入操作,绝不进行更新,删除操作很少,因此我们可以忽略它。

表的大小不会很大(几千万行的顺序)。

我们的查询将始终指定确切的release_key值。

我们新版的表格看起来像这样

CREATE TABLE dfo_by_quarter (
    release_key int4 NOT NULL,
    country_key int2 NOT NULL,
    product_group_key int2 NOT NULL,
    distribution_type_key int2 NOT NULL,
    category_key int2 NOT NULL,
    "year" int2 NOT NULL,
    "date" date NULL,
    quarter int2 NOT NULL,
    units numeric(38,6) NOT NULL,
    sales_value_eur numeric(38,6) NOT NULL,
    sales_value_usd numeric(38,6) NOT NULL,
    sales_value_local numeric(38,6) NOT NULL,
    CONSTRAINT pk_milly_dfo_by_quarter PRIMARY KEY (release_key, country_key, category_key, product_group_key, distribution_type_key, year, quarter),
    CONSTRAINT fk_milly_dfo_by_quarter_release FOREIGN KEY (release_key) REFERENCES dpretailer.dfo_release(release_id),
    CONSTRAINT fk_milly_dim_dfo_category FOREIGN KEY (category_key) REFERENCES milly.dim_dfo_category(category_key),
    CONSTRAINT fk_milly_dim_dfo_country FOREIGN KEY (country_key) REFERENCES milly.dim_dfo_country(country_key),
    CONSTRAINT fk_milly_dim_dfo_distribution_type FOREIGN KEY (distribution_type_key) REFERENCES milly.dim_dfo_distribution_type(distribution_type_key),
    CONSTRAINT fk_milly_dim_dfo_product_group FOREIGN KEY (product_group_key) REFERENCES milly.dim_dfo_product_group(product_group_key)
);

考虑到这一点,在SQL Server环境中,我可以通过使用“集群”主键(对整个表进行排序)来解决此问题,或者在主键上使用带有INCLUDE选项的索引来处理所需的其他列涵盖查询(单位,值等)。

问题1)

在postgresql中,是否有与SQL Server群集索引等效的符号?一种对整个表格进行实际排序的方法?我想这可能很困难,因为postgresql不会“就地”进行更新,因此可能会使排序变得昂贵...

或者,是否有一种方法可以创建带有INCLUDE(单位,值)的SQL Server索引?

更新:我遇到了SQL CLUSTER命令,这是我认为最接近的命令。 这对我们来说很合适

问题2

通过以下查询

EXPLAIN (ANALYZE, BUFFERS)
WITH "rank_query" AS
(
  SELECT
    ROW_NUMBER() OVER(PARTITION BY "year" ORDER BY SUM("main"."units") DESC) AS "rank_by",
    "year",
    "main"."product_group_key" AS "productgroupkey",
    SUM("main"."units") AS "salesunits",
    SUM("main"."sales_value_eur") AS "salesvalue",
    SUM("sales_value_eur")/SUM("units") AS "asp"
  FROM "milly"."dfo_by_quarter" AS "main"

  WHERE
    "release_key" = 17 AND
    "main"."year" >= 2010
  GROUP BY
    "year",
    "main"."product_group_key"
)
,BeforeLookup
AS (
SELECT
  "year" AS date,
  SUM("salesunits") AS "salesunits",
  SUM("salesvalue") AS "salesvalue",
  SUM("salesvalue")/SUM("salesunits") AS "asp",
  CASE WHEN "rank_by" <= 50 THEN "productgroupkey" ELSE -1 END AS "productgroupkey"
FROM
  "rank_query"
GROUP BY
  "year",
  CASE WHEN "rank_by" <= 50 THEN "productgroupkey" ELSE -1 END
)
SELECT BL.date, BL.salesunits, BL.salesvalue, BL.asp
  FROM BeforeLookup AS BL
  INNER JOIN milly.dim_dfo_product_group PG ON PG.product_group_key = BL.productgroupkey;

我明白了

Hash Join  (cost=40883.82..40896.46 rows=558 width=98) (actual time=676.565..678.308 rows=663 loops=1)
  Hash Cond: (bl.productgroupkey = pg.product_group_key)
  Buffers: shared hit=483 read=22719
  CTE rank_query
    ->  WindowAgg  (cost=40507.15..40632.63 rows=5577 width=108) (actual time=660.076..668.272 rows=5418 loops=1)
          Buffers: shared hit=480 read=22719
          ->  Sort  (cost=40507.15..40521.09 rows=5577 width=68) (actual time=660.062..661.226 rows=5418 loops=1)
                Sort Key: main.year, (sum(main.units)) DESC
                Sort Method: quicksort  Memory: 616kB
                Buffers: shared hit=480 read=22719
                ->  Finalize HashAggregate  (cost=40076.46..40160.11 rows=5577 width=68) (actual time=648.762..653.227 rows=5418 loops=1)
                      Group Key: main.year, main.product_group_key
                      Buffers: shared hit=480 read=22719
                      ->  Gather  (cost=38710.09..39909.15 rows=11154 width=68) (actual time=597.878..622.379 rows=11938 loops=1)
                            Workers Planned: 2
                            Workers Launched: 2
                            Buffers: shared hit=480 read=22719
                            ->  Partial HashAggregate  (cost=37710.09..37793.75 rows=5577 width=68) (actual time=594.044..600.494 rows=3979 loops=3)
                                  Group Key: main.year, main.product_group_key
                                  Buffers: shared hit=480 read=22719
                                  ->  Parallel Seq Scan on dfo_by_quarter main  (cost=0.00..36019.74 rows=169035 width=22) (actual time=106.916..357.071 rows=137171 loops=3)
                                        Filter: ((year >= 2010) AND (release_key = 17))
                                        Rows Removed by Filter: 546602
                                        Buffers: shared hit=480 read=22719
  CTE beforelookup
    ->  HashAggregate  (cost=223.08..238.43 rows=558 width=102) (actual time=676.293..677.167 rows=663 loops=1)
          Group Key: rank_query.year, CASE WHEN (rank_query.rank_by <= 50) THEN (rank_query.productgroupkey)::integer ELSE '-1'::integer END
          Buffers: shared hit=480 read=22719
          ->  CTE Scan on rank_query  (cost=0.00..139.43 rows=5577 width=70) (actual time=660.079..672.978 rows=5418 loops=1)
                Buffers: shared hit=480 read=22719
  ->  CTE Scan on beforelookup bl  (cost=0.00..11.16 rows=558 width=102) (actual time=676.296..677.665 rows=663 loops=1)
        Buffers: shared hit=480 read=22719
  ->  Hash  (cost=7.34..7.34 rows=434 width=4) (actual time=0.253..0.253 rows=435 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 24kB
        Buffers: shared hit=3
        ->  Seq Scan on dim_dfo_product_group pg  (cost=0.00..7.34 rows=434 width=4) (actual time=0.017..0.121 rows=435 loops=1)
              Buffers: shared hit=3
Planning time: 0.319 ms
Execution time: 678.714 ms

有什么想法吗?

如果我没看错的话,这意味着到目前为止我最大的花费是对表的初始扫描...但是我无法使其使用索引...

我创建了一个索引,希望对您有所帮助,但是它被忽略了...

CREATE INDEX eric_silly_index ON milly.dfo_by_quarter(release_key, YEAR, date, product_group_key, units, sales_value_eur);

ANALYZE milly.dfo_by_quarter;

我也尝试将表聚类,但也没有可见效果

CLUSTER milly.dfo_by_quarter USING pk_milly_dfo_by_quarter; -- took 30 seconds (uidev)

ANALYZE milly.dfo_by_quarter;

非常感谢

埃里克

3 个答案:

答案 0 :(得分:1)

通常,在可能的情况下,跨越 7 列的PK(至少可以说varchar(100)并未针对性能进行优化)。

如果您在相关列上进行了更新,那么这样的索引一开始就很大,并且往往会迅速膨胀。

我将使用代理PK,serial(如果您有那么多行,则使用bigserial)。或IDENTITY。参见:

在所有7个上施加UNIQUE约束以强制唯一性(无论如何都为NOT NULL)。

如果您使用release_key上的唯一谓词进行大量查询,请考虑在该列上增加一个纯btree索引。

这么多列的数据类型varchar(100)可能不是最佳的。一些规范化可能会有所帮助。

更多建议取决于缺少的信息...

答案 1 :(得分:1)

由于release_key实际上不是唯一列,因此根据您提供的信息无法知道是否应使用索引。如果在大型表上有较高比例的行具有release_key = 2或什至较小比例的行,则使用索引可能无效。

部分原因是因为Postgres索引是间接索引-索引实际上包含一个指向实际元组所在的堆中磁盘位置的指针。因此,遍历索引需要​​从索引中读取一个条目,从堆中读取元组,然后重复。对于大量的元组,直接扫描堆并避免间接的磁盘访问代价通常更有价值。

编辑: 通常,您不想在PostgreSQL中使用CLUSTER;这不是维护索引的方式,因此很少有机会在野外看到它。

您没有数据的更新查询给出了此计划:

                                                                                  QUERY PLAN                                                                                  
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 CTE Scan on beforelookup bl  (cost=8.33..8.35 rows=1 width=98) (actual time=0.143..0.143 rows=0 loops=1)
   Buffers: shared hit=4
   CTE rank_query
     ->  WindowAgg  (cost=8.24..8.26 rows=1 width=108) (actual time=0.126..0.126 rows=0 loops=1)
           Buffers: shared hit=4
           ->  Sort  (cost=8.24..8.24 rows=1 width=68) (actual time=0.060..0.061 rows=0 loops=1)
                 Sort Key: main.year, (sum(main.units)) DESC
                 Sort Method: quicksort  Memory: 25kB
                 Buffers: shared hit=4
                 ->  GroupAggregate  (cost=8.19..8.23 rows=1 width=68) (actual time=0.011..0.011 rows=0 loops=1)
                       Group Key: main.year, main.product_group_key
                       Buffers: shared hit=1
                       ->  Sort  (cost=8.19..8.19 rows=1 width=64) (actual time=0.011..0.011 rows=0 loops=1)
                             Sort Key: main.year, main.product_group_key
                             Sort Method: quicksort  Memory: 25kB
                             Buffers: shared hit=1
                             ->  Index Scan using pk_milly_dfo_by_quarter on dfo_by_quarter main  (cost=0.15..8.18 rows=1 width=64) (actual time=0.003..0.003 rows=0 loops=1)
                                   Index Cond: ((release_key = 17) AND (year >= 2010))
                                   Buffers: shared hit=1
   CTE beforelookup
     ->  HashAggregate  (cost=0.04..0.07 rows=1 width=102) (actual time=0.128..0.128 rows=0 loops=1)
           Group Key: rank_query.year, CASE WHEN (rank_query.rank_by <= 50) THEN (rank_query.productgroupkey)::integer ELSE '-1'::integer END
           Buffers: shared hit=4
           ->  CTE Scan on rank_query  (cost=0.00..0.03 rows=1 width=70) (actual time=0.127..0.127 rows=0 loops=1)
                 Buffers: shared hit=4
 Planning Time: 0.723 ms
 Execution Time: 0.485 ms
(27 rows)

因此PostgreSQL完全有能力在查询中使用索引,但是计划者认为它不值得(即,直接使用索引的成本高于使用并行序列扫描的成本)。 / p>

如果set enable_indexscan = off;没有数据,则会得到位图索引扫描(正如我期望的那样)。如果set enable_bitmapscan = off;没有数据,则将进行(非并行)序列扫描。

如果set max_parallel_workers = 0;,您应该看到计划变回了(包含大量数据)。

但是,从查询的解释结果来看,我非常希望使用索引比使用并行序列扫描更昂贵并且花费更长的时间。在更新的查询中,您仍在扫描很高比例的表和大量行,并且还通过访问索引中未包含的字段来强制访问堆。 Postgres 11(我相信)增加了覆盖索引,从理论上讲,它使您可以使查询仅由索引驱动,但是在此示例中,我一点也不确信这实际上是值得的。

答案 2 :(得分:0)

我最初的问题的答案:为什么PostgreSQL不对诸如SELECT(*)之类的东西使用我的索引...可以在文档中找到...

Introduction to VACUUM, ANALYZE, EXPLAIN, and COUNT

尤其是:这意味着每次从索引中读取一行时,引擎也必须读取表中的实际行,以确保未删除该行。 < / p>

这从很多方面解释了为什么从SQL Server的角度来看,显然“应该”时,我无法设法使PostgreSQL使用我的索引。