Postgres выполняет последовательное сканирование вместо сканирования индекса

9

У меня есть таблица с около 10 миллионов строк и индекс в поле даты. Когда я пытаюсь извлечь уникальные значения из индексированного поля, Postgres выполняет последовательное сканирование, хотя в наборе результатов содержится только 26 элементов. Почему оптимизатор выбирает этот план? И что я могу сделать, чтобы избежать этого?

Из других ответов я подозреваю, что это связано как с запросом, так и с индексом.

explain select "labelDate" from pages group by "labelDate";
                              QUERY PLAN
-----------------------------------------------------------------------
 HashAggregate  (cost=524616.78..524617.04 rows=26 width=4)
   Group Key: "labelDate"
   ->  Seq Scan on pages  (cost=0.00..499082.42 rows=10213742 width=4)
(3 rows)

Структура таблицы:

http=# \d pages
                                       Table "public.pages"
     Column      |          Type          |        Modifiers
-----------------+------------------------+----------------------------------
 pageid          | integer                | not null default nextval('...
 createDate      | integer                | not null
 archive         | character varying(16)  | not null
 label           | character varying(32)  | not null
 wptid           | character varying(64)  | not null
 wptrun          | integer                | not null
 url             | text                   |
 urlShort        | character varying(255) |
 startedDateTime | integer                |
 renderStart     | integer                |
 onContentLoaded | integer                |
 onLoad          | integer                |
 PageSpeed       | integer                |
 rank            | integer                |
 reqTotal        | integer                | not null
 reqHTML         | integer                | not null
 reqJS           | integer                | not null
 reqCSS          | integer                | not null
 reqImg          | integer                | not null
 reqFlash        | integer                | not null
 reqJSON         | integer                | not null
 reqOther        | integer                | not null
 bytesTotal      | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesImg        | integer                | not null
 bytesFlash      | integer                | not null
 bytesJSON       | integer                | not null
 bytesOther      | integer                | not null
 numDomains      | integer                | not null
 labelDate       | date                   |
 TTFB            | integer                |
 reqGIF          | smallint               | not null
 reqJPG          | smallint               | not null
 reqPNG          | smallint               | not null
 reqFont         | smallint               | not null
 bytesGIF        | integer                | not null
 bytesJPG        | integer                | not null
 bytesPNG        | integer                | not null
 bytesFont       | integer                | not null
 maxageMore      | smallint               | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 fullyLoaded     | integer                |
 cdn             | character varying(64)  |
 SpeedIndex      | integer                |
 visualComplete  | integer                |
 gzipTotal       | integer                | not null
 gzipSavings     | integer                | not null
 siteid          | numeric                |
Indexes:
    "pages_pkey" PRIMARY KEY, btree (pageid)
    "pages_date_url" UNIQUE CONSTRAINT, btree ("urlShort", "labelDate")
    "idx_pages_cdn" btree (cdn)
    "idx_pages_labeldate" btree ("labelDate") CLUSTER
    "idx_pages_urlshort" btree ("urlShort")
Triggers:
    pages_label_date BEFORE INSERT OR UPDATE ON pages
      FOR EACH ROW EXECUTE PROCEDURE fix_label_date()
Чарли Кларк
источник

Ответы:

8

Это известная проблема, касающаяся оптимизации Postgres. Если различные значения мало - как в вашем случае - и вы в 8.4+ версии, очень быстро обойти , используя рекурсивный запрос описано здесь: дряблая Indexscan .

Ваш запрос может быть переписан ( LATERALнеобходима версия 9.3+):

WITH RECURSIVE pa AS 
( ( SELECT labelDate FROM pages ORDER BY labelDate LIMIT 1 ) 
  UNION ALL
    SELECT n.labelDate 
    FROM pa AS p
         , LATERAL 
              ( SELECT labelDate 
                FROM pages 
                WHERE labelDate > p.labelDate 
                ORDER BY labelDate 
                LIMIT 1
              ) AS n
) 
SELECT labelDate 
FROM pa ;

У Эрвина Брандстеттера есть подробное объяснение и несколько вариантов запроса в этом ответе (по связанной, но другой проблеме): Оптимизировать запрос GROUP BY для получения последней записи для пользователя

ypercubeᵀᴹ
источник
6

Лучший запрос очень сильно зависит от распределения данных .

У вас есть много строк на дату, это было установлено. Так как ваш случай сгорает до 26 значений в результате, все следующие решения будут невероятно быстрыми, как только индекс будет использован.
(Для более четких значений случай будет более интересным.)

Там нет необходимости привлекать pageid вообще (как вы прокомментировали).

Индекс

Все, что вам нужно, это простой индекс btree "labelDate".
С более чем несколькими значениями NULL в столбце, частичный индекс помогает немного больше (и меньше):

CREATE INDEX pages_labeldate_nonull_idx ON big ("labelDate")
WHERE  "labelDate" IS NOT NULL;

Вы позже уточнили:

0% NULL, но только после исправления ошибок при импорте.

Частичный индекс все еще может иметь смысл исключать промежуточные состояния строк со значениями NULL. Избегал бы ненужных обновлений индекса (с последующим раздутием).

запрос

На основании предварительного диапазона

Если ваши даты появляются в непрерывном диапазоне с не слишком большим количеством пробелов , мы можем использовать природу типа данных dateв наших интересах. Существует только конечное, счетное количество значений между двумя данными значениями. Если промежутков мало, это будет быстрее всего:

SELECT d."labelDate"
FROM  (
   SELECT generate_series(min("labelDate")::timestamp
                        , max("labelDate")::timestamp
                        , interval '1 day')::date AS "labelDate"
   FROM   pages
   ) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Почему бросок timestampв generate_series()? Видеть:

Мин и макс можно выбрать из индекса дешево. Если вы знаете минимальную и / или максимально возможную дату, она становится немного дешевле. Пример:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM   generate_series(0, now()::date - date '2011-01-01' - 1) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Или для неизменного интервала:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM generate_series(0, 363) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Слабое сканирование индекса

Это очень хорошо работает при любом распределении дат (если у нас много строк на дату). В основном то, что @ypercube уже предоставил . Но есть некоторые тонкости, и мы должны убедиться, что наш любимый индекс может использоваться везде.

WITH RECURSIVE p AS (
   ( -- parentheses required for LIMIT
   SELECT "labelDate"
   FROM   pages
   WHERE  "labelDate" IS NOT NULL
   ORDER  BY "labelDate"
   LIMIT  1
   ) 
   UNION ALL
   SELECT (SELECT "labelDate" 
           FROM   pages 
           WHERE  "labelDate" > p."labelDate" 
           ORDER  BY "labelDate" 
           LIMIT  1)
   FROM   p
   WHERE  "labelDate" IS NOT NULL
   ) 
SELECT "labelDate" 
FROM   p
WHERE  "labelDate" IS NOT NULL;
  • Первый CTE pфактически такой же, как

    SELECT min("labelDate") FROM pages

    Но подробная форма гарантирует, что наш частичный индекс используется. Плюс, эта форма, как правило, немного быстрее в моем опыте (и в моих тестах).

  • Только для одного столбца коррелированные подзапросы в рекурсивном члене rCTE должны быть немного быстрее. Это требует, чтобы исключить строки, приводящие к NULL для "labelDate". Видеть:

  • Оптимизировать запрос GROUP BY для получения последней записи для пользователя

Asides

Не цитируемые, легальные идентификаторы в нижнем регистре делают вашу жизнь проще.
Упорядочите столбцы в определении таблицы, чтобы сэкономить место на диске:

Эрвин Брандштеттер
источник
-2

Из документации postgresql:

CLUSTER может пересортировать таблицу, используя сканирование индекса по указанному индексу или (если индекс является b-деревом) последовательное сканирование с последующей сортировкой . Он попытается выбрать метод, который будет более быстрым, основываясь на параметрах стоимости планировщика и доступной статистической информации.

Ваш индекс на labelDate является btree ..

Ссылка:

http://www.postgresql.org/docs/9.1/static/sql-cluster.html

Фабрицио Маззони
источник
Даже с такими условиями, как «ГДЕ» labelDate «МЕЖДУ« 2000-01-01 »и« 2020-01-01 »все еще используется последовательное сканирование.
Чарли Кларк
Кластеризация в данный момент (хотя данные были введены примерно в этом порядке). Это все еще не объясняет решение планировщика запросов не использовать индекс даже с предложением WHERE.
Чарли Кларк
Вы пытались также отключить последовательное сканирование для сеанса? set enable_seqscan=offВ любом случае документация понятна. Если вы кластеризуете, он выполнит последовательное сканирование.
Фабрицио Маззони
Да, я попытался отключить последовательное сканирование, но это не имело большого значения. Скорость этого запроса на самом деле не имеет решающего значения, так как я использую его для создания таблицы поиска, которая затем может использоваться для JOINS в реальных запросах.
Чарли Кларк