PostgreSQL не использует прямой индекс - PullRequest
0 голосов
/ 06 марта 2019

У меня есть база данных PostgreSQL 10.6 на Amazon RDS. Мой стол такой:

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 я мог бы решить эту проблему с помощью первичного ключа «Clustered» (сортировка всей таблицы) или индекса на первичном ключе с параметром INCLUDE для других необходимых столбцов. для покрытия запросов (Единицы, Значения и т. д.).

Вопрос 1)

В postgresql есть ли эквивалент кластеризованного индекса SQL Server? Способ на самом деле сортировать всю таблицу? Я полагаю, это может быть сложно, потому что postgresql не делает обновления "на месте", следовательно, это может сделать сортировку дорогой ...

Или есть способ создать что-то вроде индекса SQL Server WITH INCLUDE (единицы, значения)?

обновление: я наткнулся на команду 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 ]

1 голос
/ 07 марта 2019

Поскольку release_key на самом деле не является уникальным столбцом, из предоставленной вами информации невозможно определить, следует ли использовать индекс. Если большой процент строк имеет release_key = 2 или даже меньший процент строк совпадает в большой таблице, использование индекса может быть неэффективным.

Частично это связано с тем, что индексы Postgres являются косвенными - то есть индекс фактически содержит указатель на место на диске в куче, где живет настоящий кортеж. Таким образом, цикл по индексу требует чтения записи из индекса, чтения кортежа из кучи и повторения. Для большого числа кортежей часто более полезно сканировать кучу напрямую и избегать штрафов за косвенный доступ к диску.

Edit: Как правило, вы не хотите использовать CLUSTER в PostgreSQL; это не то, как поддерживаются индексы, и по этой причине редко можно увидеть это в дикой природе.

Ваш обновленный запрос без данных дает план:

                                                                                  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 полностью способен использовать индекс для вашего запроса, но планировщик решает, что он того не стоит (т. Е. Стоимость непосредственного использования индекса выше, чем стоимость использования сканирования параллельной последовательности).

Если у вас set enable_indexscan = off; нет данных, вы получаете растровое сканирование индекса (как я и ожидал). Если у вас set enable_bitmapscan = off; нет данных, вы получаете (непараллельное) сканирование последовательности.

Вы должны увидеть изменение плана обратно (с большими объемами данных), если вы set max_parallel_workers = 0;.

Но, глядя на результаты объяснения вашего запроса, я бы очень ожидал, что использование индекса будет более дорогим и займет больше времени, чем сканирование параллельных последовательностей. В своем обновленном запросе вы по-прежнему сканируете очень высокий процент таблицы и большое количество строк, а также принудительно обращаетесь к куче, используя поля, отсутствующие в индексе. В Postgres 11 (я полагаю) добавлены закрывающие индексы, которые теоретически позволили бы вам управлять этим запросом только одним индексом, но я совсем не уверен, что в этом примере он действительно того стоит.

1 голос
/ 06 марта 2019

Обычно, когда это возможно, PK, охватывающий 7 столбцов, некоторые из которых varchar(100) не оптимизированы по производительности, если не сказать больше.

Такой индекс велик для начала и имеет тенденцию быстро увеличиваться, если у вас есть обновления для соответствующих столбцов.

Я бы работал с суррогатным ПК, serial (или bigserial, если у вас столько строк). Или IDENTITY. См:

И ограничение UNIQUE для всех 7 для обеспечения уникальности (все равно NOT NULL).

Если у вас много запросов на подсчет с единственным предикатом release_key, рассмотрите дополнительный простой индекс btree только для этого столбца.

Тип данных varchar(100) для такого количества столбцов может быть неоптимальным. Некоторая нормализация может помочь.

Дополнительные советы зависят от недостающей информации ...

0 голосов
/ 08 марта 2019

Ответ на мой первоначальный вопрос: почему postgresql не использует мой индекс для чего-то вроде SELECT (*) ... можно найти в документации ...

Введение в ВАКУУМ, АНАЛИЗ, ОБЪЯСНЕНИЕ и СЧЕТ

В частности: Это означает, что каждый раз, когда строка читается из индекса, движок должен также считывать фактическую строку в таблице, чтобы убедиться, что строка не была удалена.

Это многое объясняет, почему мне не удается заставить postgresql использовать мои индексы, когда, с точки зрения SQL Server, это явно "должно".

...