Оптимизируем аналитические SQL запросы
Привет! Если ты читаешь эту статью, значит явно интересуешься как писать оптимальные SQL запросы для чтения данных из баз данных или аналитических хранилищ.
Это важный навык для аналитиков, инженеров, разработчиков, тестировщиков и всех кто пишет на SQL.
Здесь рассмотрим кейсы как писать SELECT запросы эффективно, без создания индексов.
Песочница базы данных для твоей практики
Это не просто статья, а твоя возможность приобрести ценный навык писать комплексные запросы качественно. Внедри эти знания в свою работу чтобы твои запросы не ложили базу данных или не приходилось ждать результатов часами.
Ты можешь подключиться к базе данных, чтобы выполнять запросы из примеров ниже.
Хост: 94.26.239.237
Порт: 5432
База данных: sandbox
Логин: sandbox_user
Пароль: uth3u74g2
Почему важно оптимизировать SQL запросы
Давай подумаем на что влияет то, как написан твой запрос.
1. Скорость выполнения запроса
Чем лучше технически и логически написан запрос, тем быстрей его выполнит база данных. Скорость выполнения запросов может отличаться на минуты или даже десятки минут, хотя сами запросы могут возвращать одни и те же данные.
2. Затраты ресурсов базы
База данных как твой телефон имеет ограниченные ресурсы: память хранения, оперативная память, ядра процессора. Неоптимальные запросы задействуют больше ресурсов для их выполнения, что может привести к зависаниям или даже падениям базы данных, что очень критично для бизнес-процессов. Также от ресурсов напрямую зависят затраты денег на них, поэтому все стремятся к оптимизации.
3. Чистота кода
В нормальных компаниях принято писать запросы в едином стиле (style guide), чтобы дальше этот код можно было удобно поддерживать и развивать.
4. Качество данных
Если пишешь запросы некачественно или с неявными ошибками, это напрямую влияет на отдаваемые данные в результате запроса, а также на дальнейший анализ и зависимые от этого отчеты.
Правила оптимизации SQL
Давай разберём как писать оптимизированные запросы, а как делать не стоит.
Выбирай только нужные данные
Вместо привычной * стоит явно указывать только те столбцы, которые нужны из данных. Например, тебе для отчета необходимо выгружать заказы за сегодня, но при этом нужные только конкретный набор столбцов: id заказа, дата заказа, стоимость и кол-во продуктов.
Явное перечисление столбцов сократит затраченную память для считывания данных с жестких дисков и отображением этих данных тебе в пользовательском интерфейсе.
Ранняя фильтрация - фильтруй данные в начале запроса, а не в конце
Когда пишешь комплексный запрос, применяя табличные выражения (CTE), подзапросы или временные таблицы, важно применять фильтрацию в начале запроса для отсеивания ненужных данных сразу, чтобы в дальнейших шагах запроса работать уже с нужной выборкой.
Часто у аналитиков вижу ошибку, которую называю "фиктивной фильтрацией"
В чём она заключается: сначала обрабатывается весь объём данных, например добавляются расчетные поля, преобразования значений в полях, джойны с другими таблицами, добавление оконных функций.
И только в итоговом запросе применяется фильтрация в WHERE, которую нужно было применить сразу до всех преобразований и сократить объём обрабатываемых данных.
Такой запрос в любой ситуации будет более оптимален, чем первый, потому что фильрация в явном виде будет выполняться раньше чем JOIN и другие преобразования.
Избавляйся от лишних подзапросов и CTE
Подзапросы и CTE используются тогда, когда невозможно в рамках одного запроса написать всю логику обработки данных сразу и поэтому ее приходится делить на шаги. Например, сначала сделать JOIN и агрегацию, а потом этот результат сджойнить еще с одной таблицей или добавить оконную функцию.
НО часто вижу ситуации, когда пишут лишние подзапросы и CTE, в которых нет необходимости, т.к. их логику можно сразу учесть в рамках одного запроса.
Такой пример мы уже увидели в рамках рассмотрения предыдущего правила, но давай покажу еще пример. Напишем запрос, который собирает выборку клиентов с их контактными данными, у которых есть больше 1 заказа.
В данном случае 2 табличных выражения:
customer - в нем делается только преобразование полей
orders_agg - считаем кол-во заказов на клиента
Посмотрим как можно оптимизировать данный запрос и убрать лишний CTE.
Можно полностью убрать CTE customer и вынести его логику в основной запрос.
Обрати также внимание, что я сразу добавил фильтрацию на кол-во заказов по клиенту в HAVING, чтобы в CTE orders_agg оставить сразу только нужных нам клиентов с их кол-вом заказов. Дальше я использую INNER JOIN чтобы конечный запрос оставил только нужную выборку клиентов, которая есть в orders_agg.
Избавляйся от подзапросов в JOIN когда это возможно
Обычно такое встречается, когда хотят соединить таблицу только на определенную выборку данных из другой таблицы.
Например, давай соберем детали по первым и последним заказам каждого клиента.
Здесь видно, что в соединении прописаны подзапросы, которые выбирают первый заказ и последний заказ в разных выборках, чтобы соединить их с таблицей клиентов.
Давай избавимся от подзапросов и перепишем соединение просто с еще одним условием в блоке ON
Как можно увидеть мы убрали подзапросы и добавили дополнительно условие в соединение.
Применяй временные таблицы для промежуточного хранения результатов
Временная таблица - инструмент для сохранения результатов запроса и переиспользования этих данных дальше в рамках одной сессии подключения.
Когда следует применять временные таблицы:
- логика запроса слишком длинная и его нужно дробить на части
- одна и та же выборка данных должны обрабатываться повторно
- объемы данных большие, а ресурсы хранилища ограничены чтобы выполнять длинные запросы. могут возникать лимиты по памяти, блокировки запроса по продолжительности выполнения, лимит CPU
Например, мы хоти собрать отчет в таблице по кол-ву продаж с разной агрегацией (день/месяц/год) в разбивке по интерфейсу в рамках разных столбцов. В таком запросе придется обращаться к одним и тем же данным заказов несколько раз, чтобы агрегировать их на разном уровне и объединять через UNION.
В таком запросе данные из табличного выражения paid_orders применяются несколько раз.
Давай сохраним эти данные во временную таблицу один раз, чтобы обращение шло к ней.
Также временные таблицы удобно применять, когда например хочешь поисследовать данные. Сохрани выборку во временную таблицу и работай с ней, чем писать запросы на весь объём к исходной таблице.
Для тестов выборку данных можешь ограничивать через LIMIT
Часто в начале какой-либо аналитической задачи тебе сначала нужно "покрутить" данные.
- посмотреть их структуру
- оценить качество
- продумать логику их обработки и обогащения
- продумать методологию расчета метрики
И для этого проще и быстрей будет взять небольшую выборку нужных тебе данных, чтобы не обращаться сразу ко всему объему. Так получится и ускорить время выполнения запросов для ускорения работы, а также не нагружать базу данных.
Используй UNION ALL вместо UNION
UNION затрачивает ресурсы на поиск идентичных записей между соединяемыми выборками и удаляя их.
UNION ALL просто склеивает выборки данных друг с другом. Если у тебя соединяемые выборки заранее различны, то нет смысла применять более тяжелый UNION.
Используй DISTINCT выборочно
Уникализация через DISTINCT позволяет правильно посчитать кол-во уникальных значений или избавиться от дублирующихся значений в выборке. Однако часто можно обойтись без лишней перестраховки, например если заранее знаешь что значения в столбце точно уникальны и не могут дублироваться.
Выводы
Твои запросы в песочнице выполняются довольно быстро за миллисекунды. Но в данном случае у нас
- не так много преобразований данных, достаточно простые запросы
- небольшие объёмы данных
- база данных не загружена другими SELECT запросами и операциями INSERT/UPDATE/DELETE
На боевой базе с большими объёмами данных и комплексными SQL запросами на сотни и тысячи строк кода оптимизация является критически важным навыком.
Здесь мы разобрали правила оптимального формирования комплексных SELECT запросов, которые нужно применять каждому аналитику или специалисту, работающему с базами и хранилищами данных. Однако оптимизация на этом не заканчивается.
Что еще влияет на оптимизацию работы с данными в базе
- формат хранения данных. как нормализованы данные по таблицам, а также какие типы данных выбраны
- способ загрузки данных и формирование инкремента
- применение индексов, но они не так эффективны в аналитических хранилищах
- партиционирование и дистрибьюция данных в таблицах
- внутренние настройки базы по выделению ресурсов на выполнение запросов, авто-масштабирование и многое другое
Если ты хочешь научиться на практике писать комплексные оптимизированные SQL запросы, строить сложную логику преобразования данных и разрабатывать аналитические витрины, я приглашаю тебя на бесплатную консультацию, на которой
🔹 обсудим как усилить твой навык SQL
🔹 найдем шаги развития в профессии конкретно для тебя
🔹 покажу материалы обучения моего курса Продвинутый SQL и автоматизация витрин данных
🔹 отвечу на любые вопросы в сфере аналитики/инженерии, обучения и карьеры