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+, тут описано дуже швидке вирішення за допомогою рекурсивного запиту: Loose 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 для отримання останнього запису на користувача


6

Найкращий запит дуже залежить від розподілу даних .

У вас багато рядків на дату, це встановлено. Оскільки ваш випадок згоряє до лише 26 значень в результаті, всі наведені нижче рішення будуть надзвичайно швидко, як тільки індекс буде використаний.
(Для більш чітких значень справа стане цікавішою.)

Там немає необхідності залучати pageid на всіх (як ви прокоментували).

Покажчик

Все, що вам потрібно, це простий btree index on "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, щоб отримати останню запис на кожного користувача

Убік

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


-2

З документації postgresql:

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

Ваш покажчик на labelDate - це бат ..

Довідка:

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


Навіть при таких умовах, як "WHERE" labelDate "BETWEEN '2000-01-01" та "2020-01-01" все ще включає послідовне сканування.
Чарлі Кларк

На даний момент кластеризація (хоча дані були введені приблизно в такому порядку). Це все ще не пояснює рішення планувальника запитів не використовувати індекс навіть із пунктом WHERE.
Чарлі Кларк

Ви також намагалися відключити послідовне сканування сеансу? set enable_seqscan=offУ будь-якому випадку документація зрозуміла. Якщо ви кластеризуєте, він виконає послідовне сканування.
Фабріціо Маццоні

Так, я спробував відключити послідовне сканування, але це не мало особливого значення. Швидкість цього запиту насправді не є вирішальною, оскільки я використовую його для створення таблиці пошуку, яка потім може бути використана для JOINS в реальних запитах.
Чарлі Кларк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.