Повільний запит на великій таблиці з групою BY і ORDER BY


14

У мене є таблиця з 7,2 мільйона кортежів, яка виглядає приблизно так:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Тепер я хочу вибрати деякі значення, але запит неймовірно повільний:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

hashКолона є md5 хеш stringі має індекс. Тож я думаю, що моя проблема полягає в тому, що вся таблиця сортується за id, а не за хешем, тож спочатку потрібно сортувати її, а потім групувати?

У таблиці nostringміститься лише список хешей, які я не хочу мати. Але мені потрібні обидві таблиці, щоб мати усі значення. Тому видалити їх не є можливим.

додаткова інформація: жоден стовпець не може бути нульовим (фіксовано, що у визначенні таблиці), і я використовую postgresql 9.2.


1
Завжди надайте версію PostgreSQL, яку ви використовуєте. Який відсоток NULLзначень у стовпці method? Чи є дублікати string?
Ервін Брандстеттер

Відповіді:


18

Відповідь LEFT JOINу @ dezso має бути хорошою. Однак індекс навряд чи буде корисним (сам по собі), оскільки запит повинен так чи інакше прочитати всю таблицю - винятком є ​​сканування лише для індексів у Postgres 9.2+ та сприятливі умови, див. Нижче.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Виконати EXPLAIN ANALYZEзапит. Кілька разів для виключення грошових ефектів і шуму. Порівняйте найкращі результати.

Створіть індекс з декількома стовпцями, який відповідає вашому запиту:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Чекати? Після того, як я сказав, що індекс не допоможе? Ну, нам це потрібно до CLUSTERстолу:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Рерун EXPLAIN ANALYZE. Чи швидше? Вона повинна бути.

CLUSTER- це разова операція з перезапису всієї таблиці в порядку використаного індексу. Це також ефективно a VACUUM FULL. Якщо ви хочете бути впевнені, вам слід запустити попередній тест VACUUM FULLсамостійно, щоб побачити, що можна віднести до цього.

Якщо ваша таблиця бачить багато операцій запису, ефект з часом погіршиться. Складіть графік CLUSTERу неробочий час, щоб відновити ефект. Тонка настройка залежить від конкретного випадку використання. Посібник про CLUSTER.

CLUSTERє досить сирим інструментом, потребує ексклюзивного замка на столі. Якщо ви не можете собі цього дозволити, подумайте, pg_repackщо можна зробити так само без виключного блокування. Більше в цій пізнішій відповіді:


Якщо відсоток NULLзначень у стовпці methodвисокий (більше ~ 20 відсотків, залежно від фактичних розмірів рядків), частковий індекс повинен допомогти:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(У подальшому оновлення відображаються стовпці NOT NULL, тому не застосовується.)

Якщо ви використовуєте PostgreSQL 9.2 або пізнішої версії (як @deszo прокоментував ), представлені індекси можуть бути корисними без того, CLUSTERякщо планувальник може використовувати сканування, призначені лише для індексів . Застосовується лише за сприятливих умов: жодні операції запису, які впливали б на карту видимості з часу останнього VACUUMта всіх стовпців запиту, не повинні покриватися індексом. В основному таблиці, доступні лише для читання, можуть використовувати це будь-коли, тоді як сильно написані таблиці обмежені. Детальніше у Вікі Postgres.

Вищезазначений частковий індекс може бути ще кориснішим у цьому випадку.

Якщо , з іншого боку, у стовпці немає NULL значень method, вам слід
1.) визначити його NOT NULLта
2.) використовувати count(*)замість цього count(method), це трохи швидше і робить те ж саме за відсутності NULLзначень.

Якщо вам потрібно часто викликати цей запит, а таблиця доступна лише для читання, створіть MATERIALIZED VIEW.


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


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

@reox: Оскільки ви запускаєте v9.2: Ви протестували лише з індексом перед кластеризацією? Було б цікаво, якби ви побачили різницю. (Ви не можете відтворити різницю після кластеризації.) Також (і це було б дешево), чи EXPLAIN показує сканування індексу або сканування повної таблиці зараз?
Ервін Брандстеттер

5

Ласкаво просимо на DBA.SE!

Ви можете спробувати переформулювати запит так:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

або інша можливість:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN є типовою раковиною для продуктивності, оскільки з нею важко використовувати індекс.

Це може бути додатково покращено за допомогою індексів. Показник nostring.hashвиглядає корисним. Але спочатку: що ти зараз отримуєш? (Було б краще побачити результат, EXPLAIN ANALYZEоскільки самі витрати не вказують час проведення операцій.)


індекс створюється на nostring.hash вже, але я думаю, що postgres не використовує його через занадто багато кортежів ... коли я explcit відключає сканування послідовностей, він використовує індекс. якщо я використовую лівий приєднання, я отримую вартість 32 мільйони, тож його шлях краще ... але я намагаюся оптимізувати його більше ...
reox

3
Вартість полягає лише в тому, щоб той, хто планує, міг підготувати досить хороший план. Фактичні часи зазвичай співвідносяться з цим, але не обов'язково. Тож якщо ви хочете бути впевненими, використовуйте EXPLAIN ANALYZE.
dezso

1

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

Інші люди вже створили функцію pl / pgsql, яка перетворює (частина) значення md5 з тексту в рядок. Див. Https://stackoverflow.com/questions/9809381/hashing-a-string-to-a-numeric-value-in-postgressql для прикладу

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


1
Сумніваюся, що це перетворення пришвидшило б справи. Усі запити тут використовують рівність для порівняння. Обчислення чисельних уявлень, а потім перевірка рівності для мене не обіцяє великих вигод.
dezso

2
Я думаю, що я б зберігав md5 як bytea, а не як номер для просторової ефективності: sqlfiddle.com/#!12/d41d8/252
Джек каже спробувати topanswers.xyz

Також ласкаво просимо на dba.se!
Джек каже, спробуйте topanswers.xyz

@JackDouglas: Цікавий коментар! 16 байт на md5 замість 32 - це зовсім небагато для великих таблиць.
Ервін Брандстеттер

0

Я дуже часто стикався з цим питанням і виявив просту хитрість у двох частинах.

  1. Створіть індекс підрядків на хеш-значенні: (7 зазвичай є хорошою довжиною)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Нехай ваші пошукові запити / об’єднання включають відповідність підрядків, тому планувальник запитів натякає на використання індексу:

    старий: WHERE hash = :kwarg

    нове: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Ви також повинні мати індекс на сировину hash.

Результат (як правило) полягає в тому, що планувальник спочатку звернеться до індексу підрядки та відмелює більшість рядів. то він відповідає повному хешу 32 символів відповідному індексу (або таблиці). такий підхід знизив 800 м запитів до 4 для мене.

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