20 дек. 2025 г.·7 мин чтения

Настройка производительности Postgres перед тем, как пробовать шардинг

Настройка производительности Postgres обычно начинается с улучшения индексов, упрощения запросов и очистки таблиц. Узнайте, что исправить перед шардингом.

Настройка производительности Postgres перед тем, как пробовать шардинг

Почему желание шардить возникает слишком рано

Медленная страница не доказывает, что Postgres исчерпал ресурсы. Чаще это значит, что какая-то часть приложения тратит лишнюю работу. Здоровая база данных может выглядеть перегруженной, когда один запрос сканирует слишком много строк, сортирует огромный набор результатов или запускается сотни раз в одном запросе.

Это часто проявляется в растущих продуктах. Трафик растёт, дашборды тормозят, и кто-то говорит: "нужен шардинг." Иногда истинная проблема намного проще: один пропущенный индекс, один неаккуратный join или одна задача отчёта, которая запускается каждые 30 секунд. Эти проблемы вредят всей системе, но они не означают, что вся архитектура базы данных неправильная.

Шаблон знаком: одна конечная точка замедляется сразу после релиза фичи. CPU прыгает в одно и то же время каждый час. Чтения накапливаются за одним дорогим запросом. Команда видит проблему размера базы, когда на самом деле причина — потраченная впустую работа.

Реальные ограничения масштаба существуют. Некоторые системы перерастают одиночный узел записи. Некоторым нужна региональная или тенантная изоляция. Некоторые генерируют столько записей, что обычное обслуживание становится болезненным. Это реальные причины задуматься о шардинге.

Большинство команд ещё не дошли до этого.

Они имеют дело с дублирующимися индексами, раздутыми таблицами, ORM-запросами, которые запрашивают слишком много, и фоновыми заданиями, которые постоянно бьют по одним и тем же строкам. Очистка этого часто даёт месяцы запаса. Иногда годы.

Шардинг несёт реальные издержки. Добавляется логика маршрутизации, джоины становятся сложнее, миграции сложнее, и появляется больше способов, как всё может сломаться. Если один плохой запрос заставляет систему казаться слишком маленькой, разделение базы не решит привычку, которая это вызвала — оно просто распространит эту привычку на больше машин.

Что измерить в первую очередь

Команды обычно ошибаются, потому что смотрят на один график. Горячая диаграмма CPU не скажет вам, тратит ли Postgres циклы на плохие планы, ждёт ли он диск или сидит за блокировками.

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

  • Самые медленные запросы по суммарному времени, среднему времени и количеству вызовов
  • Пиковый трафик, скорость запросов и количество подключений
  • Рост таблиц и размер индексов по самым загруженным таблицам
  • CPU, давление по памяти и ожидания диска
  • Ожидания блокировок, дедлоки и длительность транзакций

Только время выполнения вводит в заблуждение. Запрос, который выполняется 200 мс один раз в час, часто менее приоритетен, чем тот, что идёт 40 мс и запускается пятьдесят тысяч раз в день. Смотрите вместе на прочитанные строки, возвращённые строки, сортировки, джоины и количество вызовов.

Рост таблиц тоже важен. Запрос, который сегодня кажется нормальным, может стать проблемой через месяц, если таблица удвоится в размере. Следите за самыми большими таблицами, самыми быстрорастущими и таблицами с постоянными обновлениями. Сравните это с пиком нагрузки. Многие системы выглядят здоровыми в 11 утра и разваливаются в 2 часа дня, когда отчёты, задания и API одновременно бьют по одним и тем же таблицам.

Чтение сигналов ресурсов вместе помогает. Высокий CPU часто означает плохие планы или слишком много сканирования строк. Низкий CPU с высокими ожиданиями диска указывает на проблемы I/O. Растущее число подключений при плоской пропускной способности обычно значит, что сессии ждут блокировок или борются за одну и ту же работу.

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

Первичный проход прост и стоит того. Измерьте, что болит, назовите тип боли и исправьте наиболее узкую причину в первую очередь. Это спасёт команды от шардинга базы, которую на самом деле нужно лечить лучшими запросами, чистыми индексами или уменьшением числа блокирующих транзакций.

Исправьте запросы, которые делают слишком много

Многие медленные запросы несложны — они просто просят слишком много данных, слишком часто и в неправильной форме. Прежде чем думать о шардинге, найдите команды, которые читают 100000 строк и возвращают 40.

Несоответствие обычно видно в плане. Запустите EXPLAIN ANALYZE для важного запроса и сравните прочитанные строки с возвращёнными. Если Postgres сканирует большую часть таблицы, чтобы найти маленький набор результатов, у вас уже есть цель.

Начните с очевидных трат. Многие приложения всё ещё используют SELECT *, хотя странице нужно только три поля. Это тратит дисковые чтения, память и сеть зря. Выбирайте только те колонки, которые действительно нужны экрану, API или заданию.

Относитесь к джоинам с тем же подозрением. Join, который казался безобидным полгода назад, может стать дорогим, когда таблица истории или аудит вырастет. Если ответ не использует данные джоина, уберите его и загружайте эти данные только по запросу.

Повторяющиеся запросы делают тихий вред, но быстро суммируются. Один запрос загружает 50 пользователей, а затем запускает дополнительный запрос на каждого пользователя за одними и теми же связанными данными. Фоновые задания делают то же в циклах. Код работает, но база повторяет одну и ту же работу снова и снова.

Типичные шаблоны: цикл, который выполняет один запрос на строку; отчёт, который джоит большие таблицы до фильтрации; задание, которое загружает полные записи, когда нужны лишь ID; или страница, которая для каждого посетителя запрашивает опциональные данные. Это не повод для шардинга — это повод для меньшего, чище SQL.

Небольшие правки часто выигрывают у больших архитектурных изменений. Фильтруйте раньше. Агрегируйте один раз вместо многократного повторения. Иногда разбиение одного перегруженного запроса на два простых снижает общую работу.

Обычный пример — админская страница, показывающая 25 заказов, но джоинящая заметки клиента, историю отправок и полные позиции заказа для каждой строки. Если странице нужны только статус заказа, сумма и имя клиента, лишние джоины — чистый оверхед. Обрежьте их, и запрос может упасть с 900 мс до ощущения мгновенного отклика.

Измеряйте после каждого изменения. Новый план должен показывать меньшую работу, а не просто другую работу.

Приведите индексы в порядок с целью

Команды собирают индексы под давлением. Появился медленный запрос — кто‑то добавил индекс — и все двинулись дальше. Чтения могут ускориться на время, но каждый лишний индекс усложняет вставки, обновления, удаления и vacuum.

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

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

Одноколоночные индексы кажутся безопасными, но не всегда сильно помогают. Если запрос фильтрует по двум колонкам в фиксированном порядке, составной индекс может дать гораздо больше. Порядок имеет значение. Запрос с WHERE status = ? AND created_at > ? ORDER BY created_at DESC обычно получит больше пользы от (status, created_at), чем от (created_at, status).

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

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

Удалять индексы тоже важно. Дублирующие и неиспользуемые индексы замедляют записи и съедают диск без отдачи. Индекс на (email) и другой на (email, created_at) могут перекрываться настолько, что нужен только один — в зависимости от запросов. Проверяйте статистику использования перед удалением, но не держите индексы только потому, что они кажутся безопасными.

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

Приведите схему в порядок до того, как вокруг плохих решений вырастет всё остальное

Fix Slow Queries First
Работайте с SQL, который тратит время, и сократите ненужную работу до больших архитектурных изменений.

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

Обычная проблема — несовпадающие типы. Если одна таблица хранит user_id как UUID, а другая — как текст, Postgres будет делать касты при джоинах или фильтрах. Эта лишняя работа может блокировать использование индекса и замедлить запрос, который должен был быть дешёвым.

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

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

Большие колонки требуют внимания. Таблица может содержать огромный JSON‑блок, длинные заметки или сырые полезные нагрузки API, которые использует однажды поддержка. Если самый загруженный запрос нуждается только в id, status и created_at, эти большие поля не должны лежать на горячем пути.

Простое разделение часто помогает. Держите колонки, которые используются постоянно, в основной таблице, а редкие тяжёлые поля — во второй таблице с тем же первичным ключом. Джойните их только при необходимости.

Мёртвые колонки создают другую проблему: они сбивают с толку разработчиков, загромождают ORM‑модели и побуждают выбирать больше данных, чем нужно. Старые флаги, заброшенные поля статуса и наполовину завершённые миграции могут жить годами, если кто‑то намеренно их не удалит.

По умолчанию и ограничения тоже со временем умирают. Значение по умолчанию, имевшее смысл два года назад, может сейчас писать бесполезные данные при каждой вставке. Правило уникальности может больше не соответствовать продукту. Колонки могут по‑прежнему допускать null, хотя приложение уже зависит от них. Очистка этого уменьшает шум и облегчает рассуждение о будущих запросах.

Быстрый обзор схемы должен ответить на простые вопросы. Совпадают ли типы смежных колонок? Можно ли вынести старые строки из самой загруженной таблицы? Какие большие поля почти никогда не используются в общих чтениях? Какие колонки больше никто не использует? Соответствуют ли текущие ограничения приложению?

Эта работа не гламурна, но часто даёт больше времени, чем команды ожидают. Она также облегчает все последующие шаги по тюнингу, потому что вам больше не придётся спорить со старыми проектными решениями при каждом запросе.

Используйте простой процесс тюнинга

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

Выберите одну медленную конечную точку, отчёт или задачу. Запишите эталон до изменений. Будьте конкретны: текущее время отклика или время выполнения, насколько часто оно запускается и какую нагрузку даёт на базу. Даже небольшая заметка до/после сэкономит много угадываний.

Полезная базовая линия включает имя эндпойнта или задания, среднее и p95 время выполнения, точный текст запроса или fingerprint, прочитанные строки против возвращённых и объём записей в ту же таблицу.

Затем найдите точные запросы, стоящие за замедлением. Используйте логи запросов, трассировки приложения или pg_stat_statements. Не настраивайте страницу или воркер в абстракте — настраивайте SQL, который реально съедает время.

Запустите EXPLAIN ANALYZE для худшего запроса и ищите трату. Вы можете найти пропущенный индекс, сортировку на слишком многих строках, джоин, который взрывает набор результатов, или фильтр, который выполняется после полного сканирования. Исправьте одну проблему, протестируйте на том же рабочем наборе и только потом переходите к следующему изменению.

Эта сдержанность важна. Если вы добавили индекс и переписали три запроса за один раз, вы теряете способность оценить компромисс. Чтение может упасть с 1.9 секунды до 140 мс, а записи замедлиться на 10%. Иногда это допустимо. Иногда нет.

Держите заметки там, где команда уже ведёт изменения кода. Короткая запись с запросом, изменением, результатом и шагом отката — достаточно. Когда исправление даёт неожиданный эффект, команда сможет быстро откатить, вместо того чтобы гадать, что изменили в прошлый вторник.

Реальный пример

Cut Write Overhead
Проверьте дублирующиеся индексы и тяжёлые операции записи до того, как они замедлят всю систему.

Небольшой B2B‑продукт ежедневно опирался на три таблицы: customers, orders и activity_logs. Первые две росли стабильно. Таблица логов взорвалась. Она выросла с нескольких миллионов строк до более чем 180 миллионов менее чем за год, потому что продукт записывал просмотры страниц, поиски, входы и активность задач.

Проблема проявилась на странице поиска клиентов. Саппорт искал по имени клиента, затем открывал список результатов, который также показывал дату последнего заказа и недавнюю активность. Раньше страница грузилась менее 300 мс. После роста таблицы логов она часто занимала 2–4 секунды.

Команда сначала обвинила Postgres и заговорила о шардинге. Реальная проблема была уже: один запрос джоинял customers, orders и activity_logs при каждом поиске, затем сортировал огромный рабочий набор, чтобы показать 25 строк.

Они сделали несколько целевых изменений. Добавили индекс activity_logs (customer_id, created_at desc), потому что странице нужен был только последний ивент на клиента. Добавили индекс orders (customer_id, created_at desc), потому что UI показывал только последний заказ. Затем переместили старые строки логов в архивную таблицу и держали в «горячей» только недавнюю активность.

Во втором месте они нашли проблему в еженедельном отчёте: он джоинял customers, orders, order_items и activity_logs в один большой запрос, хотя отчёт нуждался только в суммах оплаченных заказов по клиентам. Они переписали его так, чтобы сначала агрегировать заказы, затем джоинить к customers, и полностью убрали джоин логов.

Этого оказалось достаточно. Страница поиска упала до ~180 мс на 95‑ом процентиле. Еженедельный отчёт сократился с нескольких минут до менее чем минуты. Нагрузка записи на основные таблицы осталась ровной, а чтения стали намного дешевле.

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

Ошибки, которые тратят время

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

Одна частая ошибка — добавлять индексы при каждом медленном запросе. Лишние индексы помогают чтениям, но делают вставки, обновления и удаления тяжелее. Они занимают место, добавляют работу vacuum и превращают простые записи в набор дополнительного обслуживания. На таблице с частыми изменениями ещё один индекс может стоить дороже, чем сэкономит.

Другая ошибка — рассматривать шардинг как короткий путь. Команды иногда разбивают данные между базами, потому что один отчёт, одна админская страница или один экспорт тормозят. Это распространяет сложность по всей системе, чтобы скрыть узкую проблему. Часто отчёту просто нужен лучший SQL, сводная таблица или другой график запуска.

Ожидания блокировок тратят время, потому что они прячутся за другими симптомами. Страница загружается медленно — люди винят CPU, память или недостающие индексы. Между тем одна долгая транзакция блокирует строку или таблицу, и всё становится в очередь за ней. Если смотреть только графики ресурсов, истинную причину пропустите.

Очистка схемы откладывается, потому что кажется рискованной. Старые колонки остаются. Неиспользуемые таблицы остаются. Дублирующиеся поля остаются. Разработчики продолжают строить вокруг этого наследия, и мусор растёт. Запрос, который задевает три устаревших джоина и две наполовину мёртвые колонки, гораздо сложнее исправить, чем чистый.

Последняя ошибка — менять продакшн без базовой линии. Если вы не знаете время запроса, скорость записей, время ожидания блокировок и уровень ошибок до изменений, нельзя судить результат после. Люди начинают спорить по ощущениям.

С этим проще, когда ведёте счёт. Сначала измерьте, потом поменяйте одну вещь, сравните числа и оставьте только те исправления, которые улучшают показатели.

Быстрая проверка перед шардингом

Review Postgres Bottlenecks
Практический анализ медленных запросов, индексов, блокировок и схемы до шардинга.

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

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

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

Старые данные — ещё одна экономия. Многие команды хранят каждую строку в горячих таблицах навсегда, а потом удивляются замедлению рутинных запросов. Если большинство запросов обращаются к недавним записям, вынесите старые строки, партиционируйте по дате или удалите неиспользуемые данные. Это само по себе может уменьшить рабочий набор настолько, что индексы и кэш начнут работать гораздо лучше.

Проверьте также блокировки и долгие транзакции. Один отчёт, который работает 10 минут, или задание, которое держит строки открытыми слишком долго, может сделать нормальный трафик медленным, даже если CPU и память в порядке. Шардинг этого не уберёт — он лишь распространит проблему.

Короткий чеклист:

  • Поясните самые медленные запросы и убедитесь, что реальный план соответствует ожиданиям
  • Сравнивайте латентность в час пик до и после каждого исправления, по одному изменению за раз
  • Убирайте холодные данные из горячих путей, когда большинство чтений ориентировано на недавние записи
  • Находите блокирующие транзакции, прежде чем винить размер базы
  • Спросите, действительно ли одна таблица превысила то, что способен обработать одиночный узел Postgres

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

Что делать дальше

Большинству команд не нужен огромный рефактор базы. Им нужен короткий спринт по очистке с понятными владельцами и финишной линией. Хороший тюнинг часто даёт месяцы запаса, если сосредоточиться на запросах и таблицах, которые мешают каждый день.

Начните с небольшой двухнедельной задачи, которую команда может закончить, не замораживая обычную работу:

  • Выберите топ‑5 медленных или дорогих запросов из продакшн‑трафика
  • Удалите или объедините индексы, которые вредят записям и не помогают чтениям
  • Исправьте одну‑две проблемы схемы, которые создают повторяющуюся работу
  • Запишите числа до и после по латентности, CPU, I/O и времени записи
  • Назначьте одного владельца, чтобы закрыть цикл и обновить заметки

Задайте порог шардинга до следующего спора. Если вы не назовёте линию сейчас, команда будет её постоянно передвигать. Сделайте её конкретной. Например: "Мы рассматриваем шардинг только если p95 латентность остаётся выше цельной в течение двух подряд недель после очистки запросов, индексов и схемы, или если один узел всё ещё не справляется с нормальным объёмом записей без постоянного давления."

Запишите, что помогло. Короткая внутренняя страница — достаточно. Перечислите изменения в запросах, индексы, которые добавили или удалили, редактирование схемы и числа, которые улучшились. Через шесть месяцев эта заметка сэкономит гораздо больше времени, чем ожидается.

Если нужен второй взгляд перед крупным шагом, Oleg Sotnikov на oleg.is проводит обзоры настроек Postgres в рамках более широкой работы по архитектуре и инфраструктуре. Такой обзор часто обходится дешевле, чем начать проект шардинга и потом обнаружить, что настоящая проблема была на уровне запросов или развёртывания.

Часто задаваемые вопросы

How do I know if I actually need sharding?

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

What should I measure first in Postgres?

Соберите базовую линию для обычной недели и для самых загруженных часов. Отслеживайте медленные запросы по суммарному времени, среднему времени и количеству вызовов, затем сравнивайте это с CPU, ожиданиями диска, количеством подключений, ожиданиями блокировок и ростом таблиц. Одна графика редко даёт полную картину.

How can I spot a query that does too much?

Запустите EXPLAIN ANALYZE на проблемном запросе. Сравните прочитанные строки с возвращёнными, ищите большие сканирования, поздние фильтры, тяжёлые сортировки и джоины, которые подтягивают данные, не использующиеся в ответе. Если запрос читает 100000 строк, чтобы вернуть 40, вы нашли лишнюю работу.

Should I add more indexes every time a page gets slow?

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

When do composite indexes help more than single-column ones?

Когда запрос фильтрует и сортирует по тем же столбцам в стабильном порядке, составной индекс часто полезнее отдельных одноколоночных индексов. Например, запрос с WHERE status = ? AND created_at > ? ORDER BY created_at DESC обычно получает больше выгоды от (status, created_at), чем от двух отдельных индексов. Порядок колонок важен — делайте индекс под форму запроса.

Can schema problems really slow Postgres down?

Да. Несовпадающие типы колонок, большие JSON-поля в «горячих» таблицах, мёртвые колонки и старые значения по умолчанию добавляют лишнюю работу. Чистая схема делает джоины дешевле, индексы меньше, и уменьшает соблазн запрашивать лишние данные из кода.

What usually causes lock-related slowdowns?

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

Is archiving old data worth doing before sharding?

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

How should I tune Postgres without guessing?

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

What is a reasonable threshold for deciding to shard?

Определите линию заранее. Практичное правило: рассматривать шардинг только если p95-latency остаётся выше целевой в течение двух подряд недель после очистки запросов, индексов и схемы, или если один узел всё ещё не справляется со стандартным объёмом записей и давлением на хранилище. Так команда получит явный критерий вместо спора.