Проектируем от реальных выборок, а не «на будущее»
Проверенное восстановление
Бэкап считается рабочим после теста на восстановление
План запроса до и после
Оптимизация подтверждается EXPLAIN, а не на глаз
Схема и миграции в репозитории
Изменения структуры проходят ревью и откат
pg_stat_activity — мониторинг нагрузки
Активных запросов
18
Долгий запрос
0.4 s
Отставание реплики
0.2 s
Нагрузка на чтение
последние 12 часов
Тяжёлые запросы
SELECT … FROM orders WHERE …0.038 ms
SELECT … FROM order_items JOIN …0.121 ms
SELECT … FROM audit_log GROUP BY …0.640 ms
Резервная копия проверена восстановлением
сегодня, 04:12
Ключевые моменты
Проектируем схему под реальные запросы, а не «на будущее»: нормализация там, где она помогает
Индексы и планы запросов: находим и убираем медленные выборки до того, как они станут проблемой
Репликация для чтения и разделение нагрузки между мастером и репликами
Redis для кэша, очередей и сессий — снижаем давление на основную базу
Партиционирование и архивация больших таблиц вместо бесконечного роста
Резервное копирование с проверкой восстановления, а не «бэкап, который никто не тестировал»
Задачи
Что происходит с базой, когда проект растёт
Почти все проблемы с базами данных проявляются одинаково: сначала «стало
медленнее», потом «непонятно, что с этим делать». Ниже — типовые ситуации,
с которыми приходят на аудит, и к чему они приводят.
База выросла — списки начали тормозить
На тестовых данных всё летало, а в продакшене появились сотни тысяч строк, и отчёты стали занимать секунды. Часто причина не в «слабом сервере», а в выборке, которая читает таблицу целиком, и в отсутствии нужного индекса.
Сначала это замечают пользователи, потом — руководство
Индексы наставлены наугад
Индекс добавляют под каждый медленный запрос, и со временем их становится больше, чем нужно. Каждый лишний индекс замедляет вставку и обновление и занимает место, а нужного составного индекса всё равно нет.
Запись медленнее, база больше, эффекта нет
Резервная копия есть, но не проверялась
Дамп снимается по расписанию, а восстановление никто не пробовал. Обнаруживается это в момент аварии, когда выясняется, что копия битая, неполная или восстанавливается дольше, чем бизнес готов ждать.
Риск реализуется ровно тогда, когда дороже всего
Изменения схемы «на живую»
Структуру правят прямо в продакшене, потому что «надо быстро». Никто не помнит, какие изменения уже применены на каждом стенде, локальная база разработчика расходится с боевой, а откатить релиз нечем.
Разные окружения живут по разным правилам
Схема не защищает данные
Дубли строк, отрицательные остатки, ссылки на несуществующие записи — всё это ловится уже в отчётах или у клиента. Ограничения на уровне базы не заведены, поэтому приложение остаётся единственным «контролёром», а он не всегда прав.
Данные, которым нельзя доверять
Непонятно, что происходит с базой
Нет ответа на простые вопросы: сколько живёт самый долгий запрос, растут ли таблицы, успевает ли реплика, когда последний раз обновлялась статистика. Без метрик любое решение о масштабировании принимается наугад.
Реагируем на аварию вместо того, чтобы её видеть
Выбор СУБД
PostgreSQL или MySQL: решение под задачу, а не по привычке
Обе системы зрелые и справятся с большинством проектов. Разница проявляется
на сложной доменной логике, аналитике и требованиях к целостности данных.
Ниже — честные развилки, по которым мы выбираем.
Сравнение PostgreSQL и MySQL по ключевым критериям
Критерий
PostgreSQL
MySQL
Тип нагрузки
Сложные аналитические запросы, отчёты, агрегации рядом с транзакционной нагрузкой
Преимущественно короткие транзакционные запросы, простые выборки по ключу
Целостность данных
Строгая типизация, богатый набор ограничений, отложенные проверки внешних ключей
Ограничения есть, но исторически часть проверок выполнялась неявно — зависит от режима SQL
Транзакции и блокировки
MVCC: читатели не блокируют писателей; уровень изоляции настраивается под задачу
В InnoDB чтение тоже не блокирует запись, но поведение и уровни изоляции отличаются от PostgreSQL
Расширения и типы
Расширения (например, для индексов по JSONB или геоданных), массивы, диапазоны, собственные типы
Ядро проще, расширяемость ограничена; для сложных типов чаще выносят логику в приложение
Изменение схемы
DDL транзакционен: изменение структуры можно откатить вместе с транзакцией
DDL не транзакционен так же, как в PostgreSQL: часть операций применяется необратимо
Команда и хостинг
Часто уже есть у команды; поддержка во всех крупных облаках и managed-сервисах
Огромная база специалистов, самый распространённый вариант в веб-хостинге и легаси-проектах
Когда выбираем
Сложная доменная логика, отчётность и аналитика, требования к целостности и типам
Простая модель данных, важна привычная инфраструктура и максимальная распространённость стека
Если у вас уже есть база и команда, которая её знает, — это весомый аргумент
остаться на ней. Смена СУБД сама по себе редко решает проблему производительности.
Индексы и планы
Что именно меняется, когда индекс подобран верно
Разница между «тормозит» и «работает быстро» видна в плане выполнения запроса.
Переключите режим, чтобы сравнить чтение таблицы целиком и поиск по составному
индексу.
СУБД читает таблицу целиком и отбрасывает почти всё. Время растёт линейно вместе с объёмом таблицы — на тестовой базе этого не видно.
Seq Scan
Полный перебор таблицы вместо поиска по индексу
rows
Оценка числа строк расходится с фактом — статистика устарела
142 ms
При 10-кратном росте данных запрос станет пропорционально дольше
Составной индекс покрывает условие фильтрации целиком. Планировщик перестал читать лишние строки, и время перестало зависеть от размера таблицы.
Index Scan
Поиск по индексу вместо полного перебора
rows=12
Оценка совпала с фактом — статистика актуальна
0.038 ms
Время определяется глубиной индекса, а не объёмом таблицы
Index Scan using orders_customer_status_created_idx on orders (cost≈0.29..8.31 rows=12 width=64) (actual time=0.021..0.038 rows=12 loops=1)
Составной индекс: (customer_id, status, created_at)
customer_id
Первым — столбец равенства: по нему идёт точное совпадение
status
Вторым — следующий по селективности фильтр из запроса
created_at
Последним — сортировка и диапазон, чтобы не строить отдельную сортировку
Тот же индекс с обратным порядком столбцов для этого запроса уже не
подойдёт: СУБД сможет использовать только его начало.
Разделы работ
Что входит в работу с базой данных
Объём подбирается под ситуацию: для нового проекта это проектирование схемы,
для работающей системы чаще начинается с аудита и оптимизации запросов.
Схема, в которой нельзя записать противоречивые данные
Структуру проектируем от запросов, которые приложение действительно выполняет: где нужна нормализация, где оправдана денормализация, какие ограничения должна держать сама база, а не только код приложения.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
Убираем дублирование там, где оно порождает расхождения, и не дробим структуру там, где это только усложняет запросы.
Ограничения в базе
Внешние ключи, уникальность, проверки значений: база не даёт записать то, что противоречит модели, даже при ошибке в коде.
Типы и точность
Деньги — точный числовой тип, не float. Даты — с часовым поясом. Строки — с явной длиной и кодировкой.
Миграции в репозитории
Каждое изменение структуры — файл миграции под контролем версий, который проходит ревью и может быть откачен.
Оптимизация, подтверждённая планом запроса
Сначала измеряем: находим тяжёлые запросы, читаем их план выполнения, и только потом меняем индексы или сам запрос. Иначе оптимизация превращается в угадывание.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
Порядок столбцов определяется тем, как фильтрует и сортирует конкретный запрос, а не алфавитом.
Устранение N+1
Жадная загрузка связей в ORM вместо сотни отдельных запросов из цикла — типовая причина «медленной страницы».
EXPLAIN до и после
Любое изменение подкреплено планом: видно, что перебор таблицы превратился в поиск по индексу.
Актуальная статистика
Без своевременного обновления статистики планировщик выбирает плохой план даже при верных индексах.
Транзакции, блокировки и целостность
Конкурентный доступ — источник самых неприятных дефектов: списание дважды, потерянное обновление, зависшая блокировка. Эти сценарии разбираются на этапе проектирования.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
Операция, меняющая несколько таблиц, выполняется целиком или не выполняется вовсе.
Осторожно с блокировками
Читаем без блокировки там, где это допустимо; держим транзакции короткими, чтобы не собирать очередь ожиданий.
Идемпотентность
Повторная обработка платежа или вебхука не создаёт вторую операцию — защита на уровне ограничений базы.
Уровни изоляции
Выбираем осознанно: где нужен строгий уровень, а где достаточно чтения зафиксированных данных.
Порядок действий, когда данных становится больше
Масштабирование начинается не с покупки более мощного сервера, а с вопроса «что именно упирается в предел». Кэш, реплики и партиционирование решают разные задачи.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
Повторяющиеся выборки и счётчики уходят из базы в кэш — основной источник снижения нагрузки на чтение.
Репликация для чтения
Отчёты и тяжёлые выборки переносятся на реплику, чтобы не мешать транзакционной нагрузке.
Партиционирование
Большие таблицы делятся по периоду, и запросы читают нужную часть вместо всей истории.
Архивация
Устаревшие данные переносятся в архив по регламенту, а не удаляются навсегда «чтобы стало быстрее».
Бэкапы, которые действительно восстанавливаются
Резервная копия имеет ценность только тогда, когда из неё восстановились на реальном стенде и уложились в допустимое время. Это проверяется, а не предполагается.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
Полные и инкрементальные копии по расписанию, с хранением в отдельном месте от самой базы.
Проверка восстановления
Регулярный тест: развернуть копию на отдельном стенде и убедиться, что она читается и полна.
Замер времени восстановления
Заранее известно, сколько занимает возврат в работу, — иначе в аварии решения принимаются вслепую.
Мониторинг и алерты
Долгие запросы, блокировки, рост таблиц, отставание реплики — с оповещением до того, как это заметит клиент.
Доступ по минимуму и защита на уровне строк
Учётная запись приложения не должна уметь удалять таблицы, а сотрудник не должен видеть чужие данные. Это настраивается в базе, а не только в приложении.
Что дальше
Покажем на вашей базе, что именно упирается в предел, и
предложим порядок изменений с оценкой риска.
У приложения, у миграций и у разработчика — разные учётные записи с разным объёмом прав.
Доступ по минимуму
Приложению выдаются только те операции, которые ему действительно нужны.
Персональные данные
Чувствительные поля шифруются, доступ к ним фиксируется в журнале, выгрузки ограничены.
Сетевая изоляция
База не смотрит в интернет: доступ только из защищённого контура или через туннель.
Этапы
Как проходит работа
Начинаем с замеров, а не с изменений: пока не видно планов запросов и размеров
таблиц, любая оптимизация — предположение.
01
Аудит и замеры
Снимаем реальную картину: размеры таблиц, самые тяжёлые запросы, их планы, состояние индексов, статистика, настройки бэкапов. Смотрим код доступа к данным в приложении, включая связи ORM.
Отчёт с узкими местами и приоритетами
02
Проектирование схемы
Описываем структуру, ограничения, индексы и связи под фактические сценарии работы. Отдельно фиксируем, какие данные растут быстро и что с ними будет через год.
Схема, миграции, состав индексов
03
Внедрение и проверка
Применяем изменения через миграции, переносим данные без остановки работы, сравниваем планы запросов до и после. Каждое изменение обратимо.
Изменения в продакшене с подтверждением
04
Эксплуатация
Настраиваем мониторинг, бэкапы и проверку восстановления. Передаём документацию по схеме и регламент действий при аварии.
Мониторинг, регламент, обученная команда
Защита от двойного списания на уровне схемы
Вместо проверки «а не обработали ли мы уже этот платёж» в коде — уникальное
ограничение в базе. Повторный запрос с тем же ключом просто не сможет создать
вторую операцию, даже если придёт дважды одновременно.
ALTER TABLE payments
ADD CONSTRAINT payments_external_id_unique
UNIQUE (external_id);
Блокировка против гонки при записи
Когда два процесса меняют одну строку, второй должен дождаться первого, а не
перезаписать результат. Advisory-блокировка берётся на время операции и
снимается автоматически вместе с транзакцией.
-- PostgreSQL
SELECT pg_advisory_xact_lock(hashtext('order:42'));
-- MySQL: именованная блокировка на время транзакции
SELECT GET_LOCK('order:42', 5);
Результат
Что вы получаете
Состав работ фиксируется до начала. Ниже — типовой набор; для действующей
системы он сужается до того, что действительно даёт эффект.
Как начинается работа
Аудит базы — самостоятельный результат, а не «первый этап продажи
разработки». Отчёт можно передать своей команде или другому подрядчику:
мы не привязываем смету аудита к последующим работам.
Схема данных с ограничениями целостности и внешними ключами
Набор индексов, обоснованный планами конкретных запросов
Миграции под контролем версий и порядок их отката
Оптимизированные тяжёлые запросы с планами «до и после»
Кэширование на Redis и перенос отчётности на реплику, где нужно
Партиционирование и архивация растущих таблиц
Резервное копирование с проверенным восстановлением
Мониторинг, алерты и регламент действий при аварии
Коротко о терминах
Чтобы разговор о базе не превращался в обмен непонятными словами.
Индекс
Структура, позволяющая найти строки без чтения всей таблицы. Ускоряет чтение, но замедляет запись и занимает место.
План запроса
Порядок действий, который СУБД выбрала для выполнения запроса. Основной источник фактов при оптимизации.
Транзакция
Набор изменений, который применяется целиком или не применяется вовсе.
MVCC
Механизм многоверсионности: читатели видят согласованный снимок данных и не блокируют писателей.
Реплика
Копия базы, которую можно использовать для чтения, чтобы разгрузить основной сервер.
Партиционирование
Разделение большой таблицы на части, чтобы запрос читал только нужный фрагмент.
Вопросы
Частые вопросы о базах данных
PostgreSQL или MySQL — что выбрать для нового проекта?
По умолчанию для новой разработки мы чаще предлагаем PostgreSQL: строгая типизация, транзакционный DDL и расширения упрощают жизнь на сложной доменной логике. MySQL остаётся разумным выбором, когда модель данных простая, команда уже работает с ним, а инфраструктура и хостинг заточены под него. Решение принимается по вашей нагрузке и опыту команды, а не по общему правилу.
Можно ли ускорить существующую базу, не переписывая приложение?
Часто да. Индексы, устранение запросов из циклов (N+1), актуализация статистики и кэширование повторяющихся выборок дают эффект без изменения бизнес-логики. Если узкое место в самой модели данных — это выяснится на аудите, и тогда мы покажем, какие изменения нужны и чем они обойдутся.
Чем опасна денормализация?
Тем, что одни и те же данные начинают храниться в нескольких местах и рано или поздно расходятся. Денормализация оправдана, когда она решает конкретную проблему производительности, а согласованность копий поддерживается осознанно — например, триггером или фоновой задачей. Мы не используем её «для скорости» без такой поддержки.
Что делать, если база уже большая и тормозит?
Начинаем с замеров, а не с изменений. Смотрим планы тяжёлых запросов и размеры таблиц, находим, что именно упирается в предел: чтение с диска, блокировки, нехватка памяти под кэш страниц или неудачные запросы. Дальше порядок зависит от причины: индексы и запросы, затем кэш и реплики, затем партиционирование и архивация.
Как часто нужно проверять резервные копии?
Регулярно и по регламенту, а не «когда будет время». Настроенный бэкап сам по себе ничего не гарантирует: важно хотя бы периодически разворачивать копию на отдельном стенде, проверять полноту данных и замерять время восстановления. Мы передаём этот регламент вместе с проектом.
Вы работаете с нашей существующей базой или создаёте новую?
И с тем, и с другим. Для действующей системы начинаем с аудита и точечных изменений, чтобы не останавливать работу. Для нового проекта проектируем схему до первой строки кода: переделывать структуру, когда в ней уже есть данные, заметно дороже.
Кто отвечает за базу после сдачи проекта?
Вы. Передаём схему с описанием, миграции, регламент бэкапов и мониторинг, объясняем, почему приняты те или иные решения. Можем остаться на сопровождении, но не строим систему так, чтобы без нас её нельзя было эксплуатировать.
Пришлите доступ к тестовой копии или опишите, что именно тормозит. На аудите покажем планы тяжёлых запросов, состояние индексов и бэкапов — и назовём порядок изменений с оценкой риска.
Мы используем необходимые cookie для работы сайта и форм. Аналитические cookie
(Яндекс.Метрика, включая запись сессий) подключаем только с вашего согласия —
от него можно отказаться. Подробнее:
Политика использования cookie
и
обработка персональных данных.
Настройки cookie
Выберите, какие категории cookie можно использовать. Необходимые отключить нельзя:
без них не работают формы и сессия.