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

217

Я тестирую производительность вставки Postgres. У меня есть таблица с одним столбцом с номером в качестве типа данных. На это также есть индекс. Я заполнил базу данных с помощью этого запроса:

insert into aNumber (id) values (564),(43536),(34560) ...

Я вставил 4 миллиона строк очень быстро по 10000 за один раз с запросом выше. После того, как база данных достигла 6 миллионов строк, производительность резко снизилась до 1 миллиона строк каждые 15 минут. Есть ли хитрость для увеличения производительности вставки? Мне нужна оптимальная производительность вставки в этом проекте.

Использование Windows 7 Pro на машине с 5 ГБ ОЗУ.

Luke101
источник
5
Стоит упомянуть и вашу версию Pg в вопросах. В этом случае это не имеет большого значения, но для многих вопросов.
Крейг Рингер
1
сбросьте индексы в таблице и сработает, если таковые имеются, и запустите скрипт вставки. После завершения массовой загрузки вы можете воссоздать индексы.
Сандип

Ответы:

482

См. Заполнение базы данных в руководстве по PostgreSQL, отличную, как обычно, статью Depesz по этой теме, и этот ТАК вопрос .

(Обратите внимание, что этот ответ касается массовой загрузки данных в существующую БД или создания новой. Если вы заинтересованы в восстановлении производительности БД с помощью pg_restoreили psqlвыполнении pg_dumpвывода, большая часть этого не применяется с тех пор, pg_dumpи pg_restoreуже выполняет такие вещи, как создание триггеры и индексы после завершения схемы + восстановление данных) .

Есть много чего сделать. Идеальным решением было бы импортировать в UNLOGGEDтаблицу без индексов, а затем изменить его на logged и добавить индексы. К сожалению, в PostgreSQL 9.4 нет поддержки смены таблиц с UNLOGGEDзарегистрированных. 9,5 добавляет, ALTER TABLE ... SET LOGGEDчтобы позволить вам сделать это.

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

В противном случае:

  • Отключить любые триггеры на столе

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

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

  • Если возможно, используйте COPYвместо INSERTs

  • Если вы не можете использовать, COPYрассмотрите возможность использования многозначных INSERTs, если это возможно. Вы, кажется, уже делаете это. Не пытайтесь перечислить слишком много значений в одном VALUES; эти значения должны уместиться в памяти пару раз, так что держите их на несколько сотен на оператор.

  • Пакетные вставки в явные транзакции, делая сотни тысяч или миллионы вставок за транзакцию. Практического ограничения AFAIK нет, но пакетирование позволит вам восстановиться после ошибки, отметив начало каждого пакета в ваших входных данных. Опять же, вы, кажется, уже делаете это.

  • Используйте synchronous_commit=offи огромное, commit_delayчтобы уменьшить затраты fsync (). Это не сильно поможет, если вы сгруппировали свою работу в большие транзакции.

  • INSERTили COPYпараллельно из нескольких соединений. Сколько зависит от дисковой подсистемы вашего оборудования; как правило, вам нужно одно соединение на физический жесткий диск, если используется хранилище с прямым подключением.

  • Установите высокое checkpoint_segmentsзначение и включитеlog_checkpoints . Посмотрите журналы PostgreSQL и убедитесь, что они не жалуются на слишком часто возникающие контрольные точки.

  • Если и только если вы не против потерять весь ваш кластер PostgreSQL (вашу базу данных и все остальные в одном кластере) из-за катастрофического повреждения, если система потерпит крах во время импорта, вы можете остановить Pg, установить fsync=off, запустить Pg, выполнить импорт, затем (жизненно) остановите Pg и установите fsync=onснова. Смотрите конфигурацию WAL . Не делайте этого, если в какой-либо базе данных вашей установки PostgreSQL уже есть данные, которые вас интересуют. Если вы установите, fsync=offвы также можете установить full_page_writes=off; опять же, просто не забудьте включить его после импорта, чтобы предотвратить повреждение базы данных и потерю данных. Смотрите недолговечные настройки в руководстве по Pg.

Вы также должны посмотреть на настройку вашей системы:

  • Максимально используйте твердотельные накопители хорошего качества для хранения. Хорошие твердотельные накопители с надежным, защищенным питанием кэшем обратной записи делают скорость фиксации невероятно высокой. Они менее полезны, когда вы следуете совету, который приведен выше - что уменьшает количество дисковых сбросов / количество fsync()секунд - но все же может помочь. Не используйте дешевые SSD без надлежащей защиты от сбоя питания, если только вы не заботитесь о сохранности своих данных.

  • Если вы используете RAID 5 или RAID 6 для хранилища с прямым подключением, остановитесь сейчас. Создайте резервную копию данных, реструктурируйте массив RAID в RAID 10 и попробуйте снова. RAID 5/6 безнадежен для массовой записи - хотя хороший RAID-контроллер с большим кешем может помочь.

  • Если у вас есть возможность использовать аппаратный RAID-контроллер с большим резервным кэшем с резервным питанием от батареи, это действительно может улучшить производительность записи для рабочих нагрузок с большим количеством коммитов. Это не очень помогает, если вы используете async commit с commit_delay или если вы делаете меньше больших транзакций во время массовой загрузки.

  • Если возможно, сохраните WAL ( pg_xlog) на отдельном диске / массиве дисков. Нет смысла использовать отдельную файловую систему на одном диске. Люди часто предпочитают использовать пару RAID1 для WAL. Опять же, это больше влияет на системы с высокой частотой фиксации и мало влияет на то, что в качестве цели загрузки данных вы используете незафиксированную таблицу.

Вы также можете быть заинтересованы в Оптимизации PostgreSQL для быстрого тестирования .

Крейг Рингер
источник
1
Согласитесь ли вы, что штраф за запись в RAID 5/6 несколько снизится, если использовать твердотельные накопители хорошего качества? Очевидно, что все еще есть штраф, но я думаю, что разница гораздо менее болезненна, чем с жесткими дисками.
1
Я не проверял это. Я бы сказал, что это, вероятно, не так уж и плохо - неприятные эффекты усиления записи и (для небольших записей) потребность в цикле чтения-изменения-записи все еще существуют, но серьезное наказание за чрезмерный поиск не должно возникать.
Крейг Рингер
Можем ли мы просто отключить индексы, а не отбрасывать их, например, установив indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) значение false, затем загрузить данные и затем перевести индексы в оперативный режим REINDEX?
Владислав Раструсный
1
@CraigRinger Я протестировал RAID-5 против RAID-10 с твердотельными накопителями на Perc H730. RAID-5 на самом деле быстрее. Также стоит отметить, что операции вставки / транзакции в сочетании с большими байтами выглядят быстрее копирования. В целом хороший совет, хотя.
Атлас
2
Кто-нибудь видит какие-либо серьезные улучшения скорости с UNLOGGED? Быстрый тест показывает примерно 10-20% улучшение.
17
15

Использование COPY table TO ... WITH BINARYв соответствии с документацией « несколько быстрее, чем формат текста и CSV ». Делайте это только в том случае, если у вас есть миллионы строк для вставки и если вы знакомы с двоичными данными.

Вот пример рецепта в Python, использующего psycopg2 с двоичным вводом .

Майк Т
источник
1
Бинарный режим может сэкономить много времени на некоторых входах, таких как временные метки, где их анализ нетривиален. Для многих типов данных это не дает большой выгоды или может даже быть немного медленнее из-за увеличенной пропускной способности (например, маленькие целые числа). Хороший вопрос, поднимающий это.
Крейг Рингер
11

В дополнение к превосходной публикации Крейга Рингера и публикации в блоге depesz, если вы хотите ускорить вставку через интерфейс ODBC ( psqlodbc ) с помощью вставок подготовленных операторов внутри транзакции, вам нужно сделать несколько дополнительных вещей, чтобы сделать ее работать быстро:

  1. Установите для уровня уровня отката при ошибках значение «Транзакция», указав Protocol=-1в строке подключения. По умолчанию psqlodbc использует уровень «Statement», который создает SAVEPOINT для каждого оператора, а не всей транзакции, делая вставки медленнее.
  2. Используйте подготовленные операторы на стороне сервера, указав UseServerSidePrepare=1в строке подключения. Без этой опции клиент отправляет весь оператор вставки вместе с каждой вставляемой строкой.
  3. Отключить автоматическую фиксацию для каждого оператора, используя SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Как только все строки были вставлены, подтвердите транзакцию, используя SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. Нет необходимости явно открывать транзакцию.

К сожалению, psqlodbc «реализует» SQLBulkOperations, выпуская серию неподготовленных операторов вставки, так что для достижения самой быстрой вставки необходимо вручную кодировать вышеописанные шаги.

Максим Егорушкин
источник
Большой размер буфера сокета A8=30000000в строке подключения также должен использоваться для ускорения вставок.
Андрус
10

Я потратил около 6 часов на тот же вопрос сегодня. Вставки идут с «обычной» скоростью (менее 3 секунд на 100 КБ) вплоть до 5-ти строк (из 30-ти общих), а затем производительность резко падает (вплоть до 1 минуты на 100 КБ).

Я не буду перечислять все вещи, которые не сработали, и вырезать прямо на мясо.

Я сбросил первичный ключ на целевой таблице (который был GUID), и мои 30MI или строки благополучно перенаправились к месту назначения с постоянной скоростью менее 3 секунд на 100 КБ.

Деннис
источник
7

Если вам нужно вставить столбцы с UUID (что не совсем так) и добавить к ответу @Dennis (я пока не могу комментировать), посоветуйте, чем использовать gen_random_uuid () (требуется PG 9.4 и модуль pgcrypto) (a много) быстрее, чем uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

против


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

Кроме того, это рекомендуемый официальный способ сделать это

Заметка

Если вам нужны только случайно сгенерированные (версия 4) UUID, рассмотрите возможность использования функции gen_random_uuid () из модуля pgcrypto.

Это время вставки пропущено от ~ 2 часов до ~ 10 минут для 3,7M строк.

Франсиско Рейносо
источник
1

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

Икар
источник
-1

Я также столкнулся с этой проблемой производительности вставки. Мое решение - порождение некоторых процедур go, чтобы закончить вставку. В то же время, SetMaxOpenConnsдолжен быть указан правильный номер, в противном случае слишком много ошибок открытого соединения будут предупреждены.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

Скорость загрузки намного выше для моего проекта. Этот фрагмент кода только что дал представление о том, как он работает. Читатели должны иметь возможность легко изменить его.

Патрик
источник
Ну, вы можете сказать это. Но в моем случае это сокращает время работы с нескольких часов до нескольких минут для миллионов строк. :)
Патрик