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

Стратегии индексирования

Индексирование часто запрашиваемых столбцов

Если вы часто фильтруете по определенному столбцу (например, WHERE status = 'active'), убедитесь, что этот столбец проиндексирован. Первичные ключи, как правило, должны иметь кластерные индексы для быстрого поиска.

-- MySQL / PostgreSQL
CREATE INDEX idx_status ON orders(status);

Использование составных индексов

Если в запросах часто используется несколько столбцов в предложении WHERE, составной индекс (composite index) может уменьшить необходимость в нескольких одностолбцовых индексах. Порядок столбцов в составном индексе должен соответствовать их использованию в запросах.

-- MySQL / PostgreSQL
CREATE INDEX idx_customer_date ON orders(customer_id, order_date);

Использование покрывающих индексов

Покрывающий индекс (covering index) включает все столбцы, на которые ссылается запрос (SELECT, JOIN, WHERE), что позволяет базе данных получать данные непосредственно из индекса, не обращаясь к таблице. Это снижает нагрузку операции ввода-вывода.

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

-- MySQL / PostgreSQL
CREATE INDEX idx_customer_status ON orders(customer_id, status);

Избегание чрезмерного индексирования

Индексы ускоряют чтение, но замедляют запись. Если вы наблюдаете снижение производительности при INSERT, UPDATE или DELETE, проверьте, нет ли у вас ненужных или избыточных индексов.

Рефакторинг запросов

Устранение ненужных столбцов в SELECT

Избегайте SELECT * — вместо этого указывайте только те столбцы, которые вам нужны. Это сокращает объем считываемых и передаваемых данных.

-- Вместо использования:
SELECT * FROM orders;

-- Будьте конкретнее:
SELECT order_id, customer_id, order_date, status FROM orders;

Корректный выбор джойнов и условий для них

Внутренние джойны (inner joins) часто работают быстрее при ранней фильтрации. Переместите наиболее ограничивающие условия как можно ближе к источнику данных. Иногда переписывание подзапросов в джойны (или наоборот) может улучшить план выполнения.

-- Исходный метод с использованием подзапроса:
SELECT o.order_id, o.customer_id
FROM orders o
WHERE o.customer_id IN (
    SELECT c.customer_id 
    FROM customers c 
    WHERE c.region = 'North'
);

-- Рефакторинг с помощью JOIN:
SELECT o.order_id, o.customer_id
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = 'North';

Избегание применения функций для индексированных столбцов в предложении WHERE

Например, WHERE LOWER(name) = 'john' может помешать использованию индекса. Вместо этого храните столбец в едином формате или используйте функциональные индексы, если они поддерживаются вашей СУБД.

Пример:

-- Вместо:
SELECT * FROM users WHERE LOWER(username) = 'jdoe';

-- Преобразуйте данные в нормализованную форму при вставке/обновлении или убедитесь, что хранящиеся данные уже имеют нижний регистр.
-- Если такая возможность поддерживается, создайте функциональный индекс (пример с использованием Postgres).:
CREATE INDEX idx_username_lower ON users((LOWER(username)));

-- Тогда:
SELECT * FROM users WHERE username = 'jdoe'; -- теперь можно использовать индекс

Разбиение сложных запросов

Вместо одного очень сложного SQL-запроса, используйте временную таблицу или CTE (Common Table Expression — обобщенное табличное выражение), чтобы изолировать сложную логику. Это поможет оптимизатору создавать лучшие планы.

WITH recent_orders AS (
    SELECT order_id, customer_id, order_date
    FROM orders
    WHERE order_date > NOW() - INTERVAL '30 days'
)
SELECT c.name, r.order_id, r.order_date
FROM recent_orders r
JOIN customers c ON r.customer_id = c.customer_id;

Проектирование схем и нормализация 

Корректная нормализация (до определенного момента)

Корректно нормализованная схема уменьшает избыточность и повышает эффективность запросов. Однако чрезмерная нормализация схемы может привести к слишком большому количеству джойнов. Стремитесь к сбалансированному подходу.

Денормализация для приложений с высокой интенсивностью чтения

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

Использование соответствующих типов данных

Используйте типы данных, соответствующие характеру и размеру данных. Меньшие типы данных приводят к уменьшению индексов и ускорению ввода-вывода. Например, используйте INT вместо BIGINT, если большие значения не нужны.

-- Используйте INT вместо BIGINT, если диапазон подходит:
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    region VARCHAR(50)
);

Применение планов выполнения запросов и средств профилирования

Регулярная проверка планов выполнения

Используйте EXPLAIN или EXPLAIN ANALYZE, чтобы увидеть, как оптимизатор базы данных выполняет запросы. Это позволит определить, используются ли индексы и нет ли неэффективного полного сканирования таблицы или сортировки.

-- MySQL / PostgreSQL
EXPLAIN SELECT customer_id, status FROM orders WHERE status = 'active';
EXPLAIN ANALYZE SELECT customer_id, status FROM orders WHERE status = 'active';

Использование специфических инструментов профилирования для баз данных

Такие инструменты, как performance_schema от MySQL, pg_stat_statements от PostgreSQL или Query Store от SQL Server, помогают определить медленные запросы и получить представление о тенденциях производительности с течением времени.

-- Включите pg_stat_statements в файле postgresql.conf, затем:
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC LIMIT 10;

Кэширование результатов

Кэширование на уровне приложения

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

# Псевдокод: кэширование результатов запросов
result = cache.get("recent_orders")
if not result:
    result = db.execute("SELECT * FROM orders ORDER BY order_date DESC LIMIT 100")
    cache.set("recent_orders", result, expire=300)  # кэширование на 5 минут
return result

Использование и обновление материализованных представлений

Материализованные представления в базах данных, которые их поддерживают (например, PostgreSQL или Oracle), могут предварительно вычислять дорогостоящие джойны или агрегации. Периодически обновляйте их, чтобы поддерживать данные в актуальном состоянии.

-- Периодически обновляйте сводную таблицу:
TRUNCATE TABLE daily_order_totals;
INSERT INTO daily_order_totals (order_date, total_revenue)
SELECT CAST(order_date AS DATE), SUM(total_amount)
FROM orders
GROUP BY CAST(order_date AS DATE);

Рекомендации по транзакциям и блокировкам

Делайте транзакции короткими

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

BEGIN;
UPDATE orders SET status = 'shipped' WHERE order_id = 1234;
COMMIT;

Используйте подходящие уровни изоляции

Высокие уровни изоляции могут снизить параллелизм и вызвать проблемы с блокировкой. Использование более низкого уровня изоляции (если это допустимо с точки зрения согласованности) может повысить пропускную способность.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- Делайте запросы здесь
COMMIT;

Настройка аппаратного обеспечения 

Оптимизация параметров памяти

Увеличьте буферные пулы или размер кэша, чтобы часто используемые данные дольше оставались в памяти, сокращая дисковые операции ввода-вывода.

Использование твердотельных накопителей и правильная компоновка хранилища

Размещайте файлы данных, журналы и индексы на более быстрых накопителях. Отделите журналы, требующие больших объемов записи, от файлов данных, чтобы избежать конфликтов при вводе-выводе.

Управление пулом соединений

Эффективное управление соединениями снижает накладные расходы на создание и разрыв соединений. Пул соединений может привести к более стабильной пропускной способности.

# postgresql.conf snippet
shared_buffers = '2GB'
work_mem = '64MB'
maintenance_work_mem = '512MB'

Регулярное обслуживание

Анализ и очистка (PostgreSQL)/оптимизация таблиц (MySQL)

Регулярное выполнение операций ANALYZE и VACUUM (PostgreSQL), а также OPTIMIZE TABLE (MySQL) обеспечивает оптимизатору актуальную статистику и минимизацию фрагментации данных и индексов.

VACUUM ANALYZE orders;
OPTIMIZE TABLE orders;

Обслуживание индексов

Со временем индексы могут стать фрагментированными. Периодическое пересоздание или реорганизация индексов позволяет сохранить их эффективность.

REINDEX INDEX idx_customer_date;

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Skilled Coder: Must know SQL database optimisation techniques

Предыдущая статьяТоп-10 самых используемых SaaS-продуктов с открытым исходным кодом 
Следующая статьяОт биологии к биоинформатике: практическое руководство для новичков