Медленный запрос на большой таблице с 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.


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


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

3
Стоимость только для планировщика, чтобы иметь возможность выбрать достаточно хороший план. Фактические времена обычно коррелируют с этим, но не обязательно. Так что, если вы хотите быть уверенным, используйте EXPLAIN ANALYZE.
— Дезсо

1

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

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

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


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 для меня.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.