Як отримати навантаження БД на порожньому місці?

  • 27 ноября, 17:34
  • 2469
  • 0

Боретеся з навантаженням бази даних PostgreSQL? На прикладах розглянемо, звідки вони з'являються і як позбутися від них шляхом оптимізації запитів.

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

Подивіться на наступний план запиту, в якому знайдете і CTE, і APPEND, і GROUP BY, і ORDER BY. Уявіть, як це оптимізувати.

Як отримати навантаження БД на порожньому місці?

Обговоримо звичайні запити в спрощеному варіанті.

Перегляньте схему даних

У першому прикладі розробник зберігає JSON в словнику - в PostgreSQL це тип hstore. Далі розгортає значення в рядок всередині підзапиту, а в зовнішньому запиті робить пошук поля token і порівнює його з конкретним параметром.

Як отримати навантаження БД на порожньому місці?

Такі операції призводять до послідовного сканування (SeqScan) всієї таблиці. Яке виправдання можна знайти: через JSON всередині hstore індекс не зробиш, тобто не оптимізуєш.

Рішення виявилося простим. Аналіз показав, що JSON з таким токеном у нас менше 1%, тому колонку прибрали і винесли таблицю в окрему сутність.

Обережніше з CTE

Часто розробники помиляються щодо CTE-команди WITH: це не представлення (VIEW), яке підставляється в наступний запит. Спочатку виконується WITH, а тільки потім його результат передається в інший запит. Тому такий запит сканує всі документи на диску, щоб у зовнішньому запиті розробник отримав єдиний необхідний файл:

Як отримати навантаження БД на порожньому місці?

Як зменшити вибірку WITH з купи рядків до одного? Перенесіть туди перевірку умови. По цьому полю був маленький індекс B-Tree.

Як отримати навантаження БД на порожньому місці?

Результат такої оптимізації:

Як отримати навантаження БД на порожньому місці?

Позбавляйтеся від зайвих даних

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

Як отримати навантаження БД на порожньому місці?

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

Що не так з цим запитом? На перший погляд, ідеальне оновлення по первинному ключу:

Як отримати навантаження БД на порожньому місці?

Насправді, запит розраховував метрики на підставі дій користувача в особистому кабінеті. Але частіше результат розрахунку дорівнював значенню, збереженому в базі даних, а оновлення ставали марними. Додавання перевірки content != :new content на порядок скоротило кількість UPDATE:

Як отримати навантаження БД на порожньому місці?

Перевірте код на бекенді

Наступний приклад невинного запиту: установка нового значення і ключа в hstore. Проблема в тому, що розробник запускав цей запит при ітерації, додаючи туди купу нових ідентифікаторів. У масштабі 100 тисяч користувачів словник збільшувався і давав непомірне для конкретного сервісу навантаження:

Як отримати навантаження БД на порожньому місці?

Варіант оптимізації - сформувати на бекенді один запит і відправити відразу всі зміни в базу.

Слідкуйте за розмірами IN

Іноді розробник, здавалося б, виправдано робить гігантський запит, замість того щоб тисячу разів звертатися до бази даних в циклі за додатковою інформацією:

Як отримати навантаження БД на порожньому місці?

Але тут підвела купа індексів в одній таблиці. Між індексом на companyid і простим B-Tree на companyid + groupname PostgreSQL вибрав перше: прочитав мільйони операцій по companyid і відфільтрував результат по groupname. Хоча groupname містив всього тисячу елементів.

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

Як отримати навантаження БД на порожньому місці?

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

Виправляйте структуру запиту

Пам'ятайте приклад з системними новинами? Розглянемо схожий, тільки з прив'язкою до двох сутностей: користувачів і компаній.

Як отримати навантаження БД на порожньому місці?

Щоб користувач при вході в особистий кабінет бачив свої повідомлення, а потрапляючи в компанію - відповідне оточення, написали такий запит:

Як отримати навантаження БД на порожньому місці?

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

Як отримати навантаження БД на порожньому місці?

Оскільки GIN-індекс зберігає дані без певного порядку, довелося виправити структуру запиту: коли зробили первинним ключем userid+ companyid+ id, швидкість виконання виросла на очах.

Призначайте тайм-аути

Для деяких запитів повільна швидкість - нормальне явище. Уявіть: ваш сервіс робить розрахунки і збирає звіт по конкретним категоріям, а користувач сам вибирає період для вибірки.

Стояло обмеження часу виконання в 100 секунд на SQL Provider: через заданий час прилітала помилка сервера баз даних, на що користувач повторював запит. Але при цьому провайдер не закінчує перший запит, і на сервері виконувалося вже два однакових важких запити, які боролися за системні ресурси.

Як вдалося змусити провайдер завершувати запит після закінчення тайм-ауту? Встановили змінну сесії statement_timeout в 100 секунд.

Перевіряйте на існування правильно

Коли розробникам кажуть перевірити існування хоч одного запису в базі даних, деякі обчислюють загальну кількість рядків з перевіркою купи умов і пишуть return count > 0:

Як отримати навантаження БД на порожньому місці?

Замість цього використовуйте EXISTS з константою, оскільки оператор завершує запит, як тільки зустріне підходящий запис:

Використовуйте BRIN-індекс

Іноді від замовника прилітає завдання порахувати загальну кількість чогось. Ви хоч раз цікавилися, скільки отримали всього повідомлень на пошті або в месенджері? А комусь подібне знадобилося, і запит виглядав моторошно:

Як отримати навантаження БД на порожньому місці?

А якщо потрібна підсумкова кількість? Наприклад, в адмінці сайту людина аналізує відвідуваність в певні періоди. Такий запит виконується повільно без індексу за датою:

Але чи доцільно навішувати величезний індекс B-Tree на маленьку табличку? Послідовне сканування займало тут 5-6 хвилин. На допомогу прийшов Block Range Index (BRIN). BRIN-індекс повертає номери сторінок з відповідними даними і виконує перевірку всієї сторінки на відповідність умовам.

Оскільки він не зберігає ідентифікатори рядків і значення, його розмір значно менше: у BRIN-індексу - 72 KB, а для порівняння, у B-Tree - 216 Mb.

Ось результат оптимізації за допомогою BRIN-індексу:

Такий маленький індекс легко підтримувати і зручно використовувати, коли значення колонок і фізичне розташування на диску корелюються, наприклад, для журналу. Документація рекомендує створювати цей індекс на багато колонок, якщо фільтруєте по ним. І тоді в зв'язці з операцією AND в умовах ви отримаєте чудову швидкість виконання.

Скорочуйте кількість інформації, що зберігається

Зменшуйте не тільки індекси, а й таблиці. Припустимо, в одній колонці у вас JSON зі старими даними, а в іншій - з новими.

Замість того, щоб зберігати кортеж повністю, позбудьтеся від дефолтних значень типу NULL і порожнього рядка. У другу колонку записуйте тільки змінені значення. Це заощадить не тільки місце на диску, але і час на резервне копіювання.

Як ще зменшити час на бекапи? Замініть тип timestamp на date, якщо час в таблиці нульовий. Так, розробники отримали економію 1,3 гігабайт на порожньому місці і скоротили час на щоденне резервне копіювання.

Оптимізуйте LIKE

Деякі вважають, що для запиту з LIKE не можна створити індекс. Думка помилкова, і тут вам стане в нагоді індекс SP-GIST, який розділить значення на окремі групи. Для рядків він створить окремі гілки на кожну букву:

SP-GIST підійде для списків товарів, послуг, міст або інших рядків. При автодоповненні на сайті ви передаєте в запит перед знаком % те, що ввів користувач, а планувальнику навіть не доведеться лізти в купу, адже всі дані вже в індексі.

А що, якщо потрібне в довіднику слово йде не з початку рядка? Використовуйте GIN-індекс, схожий на алфавітний покажчик в книзі: він вказує номери сторінок, де ви вручну шукаєте потрібне слово.

В результаті підете від послідовних сканів і отримаєте прискорення запиту в кілька разів:

Застосовуйте Partial index

Partial index - індекси c обмеженням за умовою. Уявіть, що потрібна перевірка операцій компанії за останній рік за двома постійним умовам:

Замість написання попереднього обробника з чергою і перевірками, створіть маленький індекс з відібраними за умовою значеннями:

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

Розберіться зі складним сортуванням

У продуктів зустрічаються дуже складні правила сортування:

І в першу чергу розробники намагаються оптимізувати фільтрацію WHERE. У такій ситуації розгляньте створення індексу для складного сортування. Для прикладу вище така оптимізація скоротила час виконання і кількість сканів:

У прагненні написати все зараз не забувайте, що підзапити і UNION обходяться недешево. Подивіться на запит:

Коли в таблиці більше сотень тисяч відповідних записів, на етапі сортування диск впаде. А потрібно було 20 елементів.

Для вирішення питання створюють окрему таблицю або призначають єдину з необхідних майстер-таблицю. Переносите в неї дані з інших таблиць під іншими категоріями і отримуєте звичайний select * from mastertable замість об'єднань.

Аналізуйте код і плани запитів

Коли ви приходите до необхідності створення hints для планувальника запитів або костилів, щоб пояснити PostgreSQL, що робити, то перегляньте власну схему і код. Можливо, там доведеться все міняти.

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

Для отримання більш детальної інформації розробникам потрібні логи з продакшена. Щоб розібратися, згодуйте логи утиліті pgbadger і побачите звіт.

Висновок

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


0 комментариев
Сортировка:
Добавить комментарий