Медленный запрос на большой таблице с GROUP 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и имеет индекс. Поэтому я думаю, что моя проблема в том, что вся таблица сортируется по идентификатору, а не по хешу, поэтому сначала нужно отсортировать ее, а затем сгруппировать?

Таблица nostringсодержит только список хэшей, которые я не хочу иметь. Но мне нужно, чтобы обе таблицы имели все значения. Так что это не вариант, чтобы удалить их.

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

reox
источник
1
Всегда предоставляйте версию PostgreSQL, которую вы используете. Какой процент NULLзначений в столбце method? Есть ли дубликаты на string?
Эрвин Брандштеттер

Ответы:

18

LEFT JOINВ @ Dezső Ответим должно быть хорошо. Индекс, однако, вряд ли будет полезен (сам по себе), потому что в любом случае запрос должен прочитать всю таблицу - исключение составляет сканирование только по индексу в 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;

Rerun EXPLAIN ANALYZE. Еще быстрее? Так должно быть.

CLUSTERэто однократная операция по перезаписи всей таблицы в порядке используемого индекса. Это также эффективно 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 Wiki.

Вышеупомянутый частичный индекс может быть еще более полезным в этом случае.

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

Dezso
источник
индекс уже создан для nostring.hash, но я думаю, что postgres не использует его из-за слишком большого числа кортежей ... когда я отключаю сканирование последовательности, он использует индекс. если я использую левое соединение, то получаю 32 миллиона, так что лучше ... но я пытаюсь оптимизировать его больше ...
reox
3
Стоимость только для планировщика, чтобы иметь возможность выбрать достаточно хороший план. Фактические времена обычно коррелируют с этим, но не обязательно. Так что, если вы хотите быть уверенным, используйте EXPLAIN ANALYZE.
Дезсо
1

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

Другие люди уже создали функцию pl / pgsql, которая преобразовывает (часть) значение md5 из текста в строку. Смотрите пример /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql

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

eppesuig
источник
1
Я сомневаюсь, что это преобразование ускорит процесс. Все запросы здесь используют равенство для сравнения. Вычисление числовых представлений, а затем проверка на равенство не сулит мне больших успехов.
Дезсо
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 для меня.

Джонатан Ванаско
источник