Как сделать большую базу данных postgres быстрее?

У меня большая база данных Postgres (около 75 ГБ), и запросы выполняются очень медленно. Есть ли способ сделать их быстрее?

О базе данных:

                                             List of relations
 Schema |       Name        |   Type   |  Owner   | Persistence | Access method |    Size    | Description
--------+-------------------+----------+----------+-------------+---------------+------------+-------------
 public | fingerprints      | table    | postgres | permanent   | heap          | 35 GB      |
 public | songs             | table    | postgres | permanent   | heap          | 26 MB      |
 public | songs_song_id_seq | sequence | postgres | permanent   |               | 8192 bytes |
\d+ fingerprints
                                                    Table "public.fingerprints"
    Column     |            Type             | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
---------------+-----------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 hash          | bytea                       |           | not null |         | extended |             |              |
 song_id       | integer                     |           | not null |         | plain    |             |              |
 offset        | integer                     |           | not null |         | plain    |             |              |
 date_created  | timestamp without time zone |           | not null | now()   | plain    |             |              |
 date_modified | timestamp without time zone |           | not null | now()   | plain    |             |              |
Indexes:
    "ix_fingerprints_hash" hash (hash)
    "uq_fingerprints" UNIQUE CONSTRAINT, btree (song_id, "offset", hash)
Foreign-key constraints:
    "fk_fingerprints_song_id" FOREIGN KEY (song_id) REFERENCES songs(song_id) ON DELETE CASCADE
Access method: heap
\d+ songs
                                                                       Table "public.songs"
    Column     |            Type             | Collation | Nullable |                Default                 | Storage  | Compression | Stats target | Description
---------------+-----------------------------+-----------+----------+----------------------------------------+----------+-------------+--------------+-------------
 song_id       | integer                     |           | not null | nextval('songs_song_id_seq'::regclass) | plain    |             |              |
 song_name     | character varying(250)      |           | not null |                                        | extended |             |              |
 fingerprinted | smallint                    |           |          | 0                                      | plain    |             |              |
 file_sha1     | bytea                       |           |          |                                        | extended |             |              |
 total_hashes  | integer                     |           | not null | 0                                      | plain    |             |              |
 date_created  | timestamp without time zone |           | not null | now()                                  | plain    |             |              |
 date_modified | timestamp without time zone |           | not null | now()                                  | plain    |             |              |
Indexes:
    "pk_songs_song_id" PRIMARY KEY, btree (song_id)
Referenced by:
    TABLE "fingerprints" CONSTRAINT "fk_fingerprints_song_id" FOREIGN KEY (song_id) REFERENCES songs(song_id) ON DELETE CASCADE
Access method: heap

Схема БДСумма БД

Не нужно писать в базу данных, только читать. Все запросы очень просты:

SELECT song_id 
WHERE hash in fingerpints = X
 EXPLAIN(analyze, buffers, format text) SELECT "song_id", "offset" FROM "fingerprints" WHERE "hash" = decode('eeafdd7ce9130f9697','hex');
                                                                QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using ix_fingerprints_hash on fingerprints  (cost=0.00..288.28 rows=256 width=8) (actual time=0.553..234.257 rows=871 loops=1)
   Index Cond: (hash = '\xeeafdd7ce9130f9697'::bytea)
   Buffers: shared hit=118 read=749
 Planning Time: 0.225 ms
 Execution Time: 234.463 ms
(5 rows)

234 мс выглядит нормально, если это один запрос. Но на самом деле там 3000 запросов за раз, что занимает около 600 секунд. Это приложение для распознавания звука, поэтому алгоритм работает так.

Об индексах:

CREATE INDEX  "ix_fingerprints_hash" ON "fingerprints" USING hash ("hash");

Для пула использую Odyssey.

Немного информации из конфига:

shared_buffers = 4GB   
huge_pages = try
work_mem = 582kB
maintenance_work_mem = 2GB  
effective_io_concurrency = 200
max_worker_processes = 24
max_parallel_workers_per_gather = 12
max_parallel_maintenance_workers = 4
max_parallel_workers = 24
wal_buffers = 16MB
checkpoint_completion_target = 0.9
max_wal_size = 16GB
min_wal_size = 4GB
random_page_cost = 1.1
effective_cache_size = 12GB

Информация об оборудовании:

  • Xeon 12 ядер (24 потока)
  • Оперативная память DDR4 16 ГБ ECC
  • диск NVMe

Будет ли база данных ускорена за счет приобретения дополнительной оперативной памяти для обработки всей БД внутри (например, 128 ГБ)? И какие параметры я должен изменить, чтобы Postgres хранил БД в оперативной памяти?

Я читал несколько тем про pg_tune и т.д., но эксперименты не дают хороших результатов.

Спасибо. Я добавил дополнительную информацию о БД и запросе.

unbrokendub 23.12.2022 21:21

Как часто обновляются строки? Я спрашиваю об этом, потому что, если строки никогда не обновляются, вы можете начать кэшировать результаты в Redis или MemCached, таким образом, выбор займет менее 1 мс.

GabrielNexT 23.12.2022 21:42

Вы храните файлы в своей базе данных (хэш столбца)? какой размер файла? это может замедлить работу базы данных

Edouard 23.12.2022 21:57

@GabrielNexT Обычно обновления строк отсутствуют. Таким образом, база данных является статической и доступна только для чтения.

unbrokendub 23.12.2022 21:58

@Edouard В БД нет файлов. хэш 9 байт.

unbrokendub 23.12.2022 21:59

Ваш диск кажется очень медленным, всего несколько МБ в секунду

Frank Heikens 23.12.2022 22:09

@FrankHeikens hdparm показывает, что это не так уж и плохо. sudo hdparm -Tt /dev/nvme0n1p2 /dev/nvme0n1p2: Время чтения в кэше: 19950 МБ за 1,99 секунды = 10027,52 МБ/сек Время чтения с буферизованного диска: 1146 МБ за 3,00 секунды = 381,46 МБ/сек

unbrokendub 23.12.2022 22:22

Я бы сказал, что улучшение вашего сервера будет иметь значение, но я не знаю, решит ли это проблему, но вы могли бы подумать о создании долгосрочного кеша, чтобы уменьшить эти 3000 запросов. Это моделирование прекрасно работает с базой данных noSQL, но миграция 75 ГБ потребует много работы.

GabrielNexT 23.12.2022 22:25

@unbrokendub, если вы запустите этот запрос дважды подряд, что произойдет с таймингами при втором запуске? Потому что все блоки, к которым осуществляется доступ, должны поступать из кеша для этого запуска.

Richard Huxton 23.12.2022 23:18

@a_horse_with_no_name я просто беру это значение из рекомендации pg_tune. какой размер буфера вы рекомендуете для этого типа запросов?

unbrokendub 23.12.2022 23:38

Диски NVME считаются очень быстрыми, но здесь они не кажутся такими уж быстрыми. Есть ли у вас спецификации того, что должны обеспечивать диски (с точки зрения IOPS, а не пропускной способности при последовательном чтении), и можете ли вы проверить, что они обеспечивают это, с помощью какого-либо инструмента для тестирования производительности вне PostgreSQL?

jjanes 23.12.2022 23:40

Какая версия PostgreSQL используется?

jjanes 23.12.2022 23:47

@RichardHuxton Первый запуск: Buffers: shared hit=4 read=48 Planning Time: 1.814 ms Execution Time: 8.816 ms; Второй запуск: Buffers: shared hit=52 Planning Time: 0.085 ms Execution Time: 0.129 ms

unbrokendub 23.12.2022 23:48

@jjanes 14 версия. У меня нет нужных мне спецификаций, потому что я начал этот проект в качестве эксперимента, и БД отлично работала с небольшим объемом данных (несколько ГБ), но после того, как я заполнил базу данных, она начала замедляться. Так что теперь ищем решения

unbrokendub 23.12.2022 23:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
15
141
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Не нужно писать в базу данных, только читать.

Таким образом, БД доступна только для чтения. И в комментариях:

db отлично работал с небольшим объемом данных (несколько ГБ), но после того, как я заполнил базу данных, она начала замедляться.

Таким образом, индексы были построены постепенно.

Индексы

UNIQUE CONSTRAINT на (song_id, "offset", hash)

Я бы заменил это на:

ALTER TABLE fingerprints
  DROP CONSTRAINT uq_fingerprints
, ADD CONSTRAINT uq_fingerprints UNIQUE(hash, song_id, "offset") WITH (FILLFACTOR = 100)

Это накладывает то же ограничение, но ведущий столбец hash в базовом индексе B-дерева теперь поддерживает фильтр по hash в отображаемом запросе. А тот факт, что все необходимые столбцы включены в индекс, позволяет намного быстрее сканировать только индекс. (Меньший) индекс также должен легче кэшироваться, чем (большая) таблица (плюс индекс).

Видеть:

Также перезаписывает индекс в исходном состоянии и с FILLFACTOR 100 для БД только для чтения. (Вместо 90 по умолчанию для индекса B-дерева.)

Хэш-индекс на (hash) и CLUSTER

Имя столбца «хэш» не имеет ничего общего с именем типа индекса, который также является «хэшем». (Сначала столбец, вероятно, не должен называться «хеш».)

Если (и только если) у вас также есть другие запросы, сосредоточенные вокруг одного из нескольких значений hash, которые не могут использовать сканирование только по индексу (и вы действительно видите более быстрые запросы, чем без них), дополнительно сохраните хеш-индекс. И оптимизировать его. (Иначе бросай!)

ALTER INDEX ix_fingerprints_hash SET (FILLFACTOR = 100);

Постепенно увеличивающийся индекс может привести к раздуванию или несбалансированному переполнению страниц в случае хэш-индекса. REINDEX должны позаботиться об этом. При этом увеличьте FILLFACTER до 100 (с 75 по умолчанию для хеш-индекса) для вашей БД, доступной только для чтения (!) Вы можете REINDEX сделать изменение эффективным.

REINDEX INDEX ix_fingerprints_hash;

Или вы можете CLUSTER (как уже предложил jjanes) в перестроенном индексе B-дерева сверху:

CLUSTER fingerprints USING uq_fingerprints;

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


После завершения оптимизации запустите один раз:

VACUUM ANALYZE fingerprints;

work_mem

Крошечная настройка для work_mem выделяется:

work_mem = 582kB

Даже (очень консервативно!) значение по умолчанию составляет 4 МБ. Но после повторного прочтения вашего вопроса может показаться, что у вас есть только крошечные запросы. Так что, может быть, это нормально в конце концов.

В противном случае с 16 ГБ ОЗУ вы обычно можете позволить себе в 100 раз больше. Зависит от вашей загруженности конечно.

  • Много маленьких запросов, много параллельных воркеров --> сохраняйте маленький размер work_mem (например, 4 МБ?)
  • Несколько больших запросов, несколько параллельных рабочих операций --> увеличение (например, 256 МБ? или больше)

Большое количество временных файлов, записанных в вашу базу данных с течением времени, и упоминание «диска» в выводе EXPLAIN ANALYZE указывает на необходимость большего количества work_mem.

Дополнительный вопрос

Будет ли база данных ускорена за счет приобретения дополнительной оперативной памяти для обработки всей БД внутри (например, 128 ГБ)?

Больше оперативной памяти почти всегда помогает, пока вся БД не будет кэширована в оперативной памяти, и все процессы не смогут позволить себе все work_mem, что они желают.

И какие параметры я должен изменить, чтобы Postgres хранил БД в оперативной памяти?

Все, что считывается из базы данных, автоматически кэшируется в системном кеше и кеше Postgres вплоть до предела доступной оперативной памяти. (Слишком высокое значение work_mem конкурирует за тот же ресурс.)

Ух ты. После того, как база данных CLUSTER работает как шарм, выглядит в 10 раз быстрее.

unbrokendub 25.12.2022 13:13

@unbrokendub Я полагаю, вы сначала изменили уникальный состав? Видите ли вы сканирование только индекса для примера qiery сейчас?

Erwin Brandstetter 25.12.2022 14:08

Да, я сначала переставил констрейнты, а уже потом делал кластер. Где я могу увидеть, что там индексируются только сканы?

unbrokendub 25.12.2022 16:26

@unbrokendub: Вы видите это в выводе EXPLAIN. Или подробнее с EXPLAIN (ANALYZE, BUFFERS). Запуск VACUUM ANALYZE fingerprints один раз после CLUSTER должен помочь еще немного.

Erwin Brandstetter 25.12.2022 19:39

Это вывод объяснения: `Сканирование только индексом с использованием uq_fingerprints по отпечаткам пальцев (стоимость=0,57..61,71 строк=1665 ширина=8) (фактическое время=0,312..0,323 строк=33 цикла=1) Index Cond: (hash = ' \x5cfb91912e6cc544ab'::bytea) Выборки кучи: 0 Буферы: общее попадание=4 чтение=1 Время планирования: 0,121 мс Время выполнения: 0,350 мс`. И хочу сказать спасибо за отличное объяснение, что делать с базой данных, чтобы ускорить ее. Прочитав некоторую документацию, теперь я понимаю, почему CLUSTER делает БД так быстро.

unbrokendub 26.12.2022 00:33

Для данного запроса сканирование только по индексу после изменения ограничения уникальности имеет большое значение. CLUSTER не имеет отношения. Я уточнил свое объяснение в ответе.

Erwin Brandstetter 26.12.2022 04:19

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

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

Если вы КЛАСТЕРИЗУЕТЕ таблицу в индексе по столбцу с именем «хэш» (что вы можете сделать, только если это индекс btree), тогда строки с одинаковым хэш-кодом должны в основном использовать одну и ту же страницу таблицы, что значительно сократит количество различных чтений буфера, необходимых для получения их всех.

Если бы вы могли заставить его выполнять сканирование кучи растровых изображений вместо сканирования индекса, то он должен иметь возможность одновременно обрабатывать большое количество запросов на чтение из-за Effective_io_concurrency. Но планировщик не учитывает Effective_io_concurrency при планировании, что означает, что он не будет выбирать сканирование кучи растровых изображений специально, чтобы получить это преимущество. Обычно чтение индекса, находящее сотни строк на разных страницах, автоматически выбирает метод сканирования кучи растрового изображения, но в вашем случае, вероятно, низкая настройка random_page_cost препятствует этому. Низкое значение random_page_cost, вероятно, само по себе разумно, но у него есть неприятный побочный эффект. Проблема с этой стратегией заключается в том, что она не уменьшает общее количество необходимых операций ввода-вывода, она просто позволяет им перекрываться и, таким образом, лучше использовать несколько каналов ввода-вывода. Но если во многих сеансах выполняется несколько экземпляров этого запроса одновременно, они начнут заполнять эти каналы и, таким образом, начнут конкурировать друг с другом. Таким образом, метод CLUSTER, вероятно, лучше, поскольку он дает тот же ответ с меньшим количеством операций ввода-вывода. Если вы хотите поиграть со сканированием растровых изображений, вы можете временно увеличить значение random_page_cost или временно отключить параметр enable_indexscan.

Другие вопросы по теме