Именно здесь SQL performance перестаёт быть узкой DBA-темой и становится частью профессиональной дисциплины аналитика. Плохо написанный запрос способен не только ждать двадцать минут. Он может замедлить соседние пайплайны, раздуть счёт за вычисления, создать ложное ощущение, что “данные тормозят”, хотя на самом деле тормозит сама логика запроса. Хороший аналитик думает не только о корректности, но и о стоимости пути к ответу.
SQL performance полезно рассматривать не как отдельный «урок про ускорение запросов», а как продолжение зрелого аналитического мышления. Как только запросы начинают жить в витринах, признаках и production-пайплайнах, цена их небрежности становится не теоретической, а вполне инженерной.
В 2026 году это особенно заметно, потому что аналитика живёт на больших колонночных хранилищах, lakehouse-движках, distributed SQL и очень длинных пайплайнах для BI и ML. В такой среде SQL performance — это уже не мелкая оптимизация. Это культура уважения к вычислительному контуру данных.
Главная интуиция: запрос платит за то, что заставляет систему сделать
Полезно мыслить о запросе как о плане работ, который мы отдаём хранилищу. Если мы попросили прочитать все колонки из гигантской таблицы, затем заджойнить её с другой гигантской таблицей, потом отсортировать всё целиком и только в самом конце отфильтровать одну неделю данных, система честно попытается выполнить именно этот план. И если план плохой, проблема не в “медленной базе”, а в том, что мы задали ей дорогую траекторию вычислений.
Поэтому SQL performance начинается не с магии индексов и не с заучивания редких оптимизаторных трюков. Она начинается с вопроса: какие реальные операции я заставляю систему выполнить и можно ли сделать тот же ответ дешевле?

Почему SELECT * почти никогда не безобиден
Одна из самых простых и важных привычек — не читать больше колонок, чем нужно. В колонночных хранилищах это особенно чувствительно. Когда вы пишете SELECT *, вы фактически говорите системе: прочитай весь набор признаков, даже если мне реально нужны три поля. Для широких таблиц это может увеличивать объём чтения в разы.
В аналитике и ML это встречается постоянно: сначала запрос писался как exploratory, потом попал в notebook, потом в отчёт, потом в feature pipeline — и везде продолжает тянуть лишние данные. Это не мелочь. Это накопленный технический шум, который со временем превращается в реальную цену.

Базовая идея стоимости сканирования
Самая грубая часть стоимости запроса часто связана с тем, сколько данных пришлось прочитать. Даже если дальше оптимизатор хорошо устроит join или aggregation, уже одно лишнее сканирование огромного объёма данных может сделать запрос дорогим.
(C_{scan}\approx N \cdot w)
Раздел математики: дискретная математика и асимптотический анализ.
Что означает каждый символ:
(C_{scan}) — условная стоимость чтения данных.
(N) — число строк, которые пришлось просканировать.
(w) — средняя ширина строки или эквивалентный объём читаемых признаков.
Формула не претендует на точную внутреннюю механику каждого движка. Она нужна как инженерная интуиция: чем больше строк и чем шире набор читаемых колонок, тем дороже сам факт чтения данных.
Численный пример: если таблица содержит 50 миллионов строк, а запрос тянет 20 колонок вместо реально нужных 4, то фактический скан становится примерно в пять раз шире по данным. Даже без сложных join и агрегаций это уже способно резко замедлить запрос.
Именно поэтому первая оптимизация в SQL часто даже не про хитрый синтаксис. Она про дисциплину чтения: выбрать только нужные колонки и как можно раньше сузить объём строк.
На практике это очень хорошо сочетается с логикой SQL CTE: как только запрос разбивается на осмысленные этапы, становится гораздо проще увидеть, где именно вы тянете лишние поля и на каком шаге теряете производительность.
Фильтрация должна происходить как можно раньше
Если аналитика нужна за последние семь дней, не надо сначала читать всю историю за три года и потом надеяться, что оптимизатор “сам разберётся”. Хороший запрос старается отрезать ненужные данные как можно раньше по логике плана. Особенно это важно в партиционированных таблицах, где ранний фильтр по дате может физически сократить объём чтения.
В прикладной логике это очень похоже на feature selection в ML. Мы не хотим таскать по пайплайну лишнюю информацию, если уже знаем, что она не пригодится на следующем шаге.

Почему join — это не просто “склеить две таблицы”
Многие запросы начинают тормозить именно на join. Причина в том, что соединение — это не декоративная операция. Система должна сопоставить записи по ключам, иногда перераспределить данные между узлами, иногда сортировать, иногда строить hash structure, а иногда неявно взрывать число строк из-за many-to-many соответствий.
Поэтому перед каждым join полезно задать себе три вопроса. Какова кардинальность соединения? Нужны ли обе таблицы в полном объёме? Не дублируем ли мы строки из-за неправильного ключа? Эти три вопроса часто приносят больше производительности, чем любой поздний тюнинг.
(C_{join}\approx |A|+|B|+M)
Раздел математики: теория множеств и асимптотический анализ.
Что означает каждый символ:
(C_{join}) — условная стоимость соединения.
(|A|) — размер первой таблицы или подзапроса.
(|B|) — размер второй таблицы или подзапроса.
(M) — размер промежуточного множества совпадений или результата соединения.
Формула выражает полезную интуицию: join дорог не только потому, что читает две таблицы, но и потому, что может породить большой промежуточный результат совпадений.
Численный пример: если у нас есть 10 миллионов заказов и 30 миллионов событий, то само чтение уже большое. Но если ключ соединения выбран плохо и даёт много повторов, результат (M) может оказаться ещё больше исходных таблиц. Именно в этот момент запрос часто “взрывается” по времени и памяти.
Для аналитики это особенно опасно, потому что запрос может вернуть правильный ответ после долгого времени, а пользователь даже не поймёт, что проблема была в кардинальности join. В ML-пайплайнах это ещё болезненнее: неудачное соединение может раздуть feature table, нарушить one-row-per-entity логику и параллельно убить производительность.
Именно в этом месте SQL performance естественно пересекается и с темой feature lineage: как только команда перестаёт видеть происхождение промежуточных таблиц и признаков, она одновременно теряет и прозрачность логики, и контроль над стоимостью вычислений.
GROUP BY и сортировки: где незаметно уходит производительность
Агрегации и сортировки кажутся естественными операциями, но они тоже дороги. GROUP BY требует собрать записи по ключам и удерживать структуру промежуточных агрегатов. ORDER BY на больших объёмах почти всегда тяжёлый, потому что данные нужно реально упорядочить. Особенно неприятна ситуация, когда сортировка стоит в середине запроса и не нужна для финального результата.
Одна из лучших привычек — сортировать только там, где сортировка имеет реальный смысл для конечного ответа. Не для красоты промежуточного CTE и не “на всякий случай”, а только когда это действительно требуется.
Почему CTE не всегда бесплатны
Многие любят Common Table Expressions за читаемость, и это правильно. Но ошибка начинается в тот момент, когда CTE начинают воспринимать как бесплатный способ структурировать всё подряд. В зависимости от движка CTE может инлайниться, а может материализоваться. Иногда это никак не вредит, а иногда заставляет систему вычислять тяжёлый промежуточный результат раньше, чем нужно.
Здесь важно не бороться с CTE как с явлением, а понимать их роль в конкретном движке. Хороший SQL — это читаемый SQL, но читаемость не должна автоматически означать потерю контроля над планом выполнения.
SQL performance и Data Science
Для ML-инженера и аналитика тема особенно важна потому, что почти все данные для моделей сначала проходят через SQL-слой. Если feature extraction дорогой и хрупкий, то и вся модельная система будет дорогой и хрупкой. Иногда команда неделями тюнит алгоритм, хотя реальная проблема сидит в том, что батч-признаки строятся неэффективным запросом и обновляются слишком медленно.
SQL performance напрямую влияет на скорость экспериментов, time-to-insight, стоимость feature pipelines и даже на качество моделей. Когда данные готовятся слишком долго, команда реже переобучает модели, позже замечает drift и начинает экономить на полезных пересчётах. То есть производительность SQL — это не только про хранилище. Это ещё и про скорость развития всей Data Science-системы.
Поэтому рядом с этой темой логично держать и разбор data contracts: если правила доступа к данным и структура источников постоянно плавают, то и хороший performance быстро становится нестабильным просто потому, что сама основа запроса перестаёт быть предсказуемой.
Геометрический смысл хорошего запроса
Если смотреть геометрически, хороший запрос сужает пространство данных постепенно и осмысленно. Он рано отрезает ненужные области, не раздувает промежуточные множества, не сортирует лишние точки и не таскает лишние координаты. Плохой запрос делает противоположное: сначала тащит огромное пространство целиком, потом создаёт на нём дорогое преобразование и только затем вспоминает, что итог нужен для маленького фрагмента.
Это удивительно похоже на общую логику оптимизации в ML. Мы стараемся двигаться в нужную область решений как можно раньше, а не блуждать по огромному пространству без необходимости. SQL performance — это в некотором смысле тоже задача хорошей траектории через вычислительное пространство.
Какие ошибки делают чаще всего
Первая ошибка — SELECT * в production-логике. Вторая — фильтрация слишком поздно. Третья — join без понимания кардинальности. Четвёртая — ненужные DISTINCT, которыми пытаются лечить последствия плохого join. Пятая — сортировка ради промежуточной красоты. Шестая — повторное вычисление тяжёлых подзапросов там, где можно один раз материализовать результат осознанно.
Есть и более тонкая ошибка: оптимизировать запросы вслепую, не читая plan execution и не понимая, где действительно сидит bottleneck. Иногда люди спорят о синтаксисе, а проблема на самом деле в том, что таблица не партиционирована по дате или что one-to-many join раздувает строк в десять раз.
Во временных задачах сюда ещё легко добавляется и логическая ошибка: запрос может быть не только медленным, но и причинно неверным. Поэтому тему производительности полезно держать рядом и с материалами про time-based validation и data leakage в фичах времени.
Python: как анализировать тяжёлый SQL-запрос через pandas и объяснять его стоимость
import pandas as pd # Подключаем pandas для анализа статистики по таблицам.
tables = pd.DataFrame({ # Создаем небольшую таблицу со статистикой по источникам данных.
"table": ["orders", "events", "users"], # Указываем названия таблиц.
"rows_mln": [12.0, 180.0, 3.5], # Оцениваем количество строк в миллионах.
"cols_used": [5, 7, 3], # Указываем, сколько колонок реально нужно запросу.
"cols_total": [24, 40, 18] # Указываем полную ширину таблицы.
})
tables["width_ratio"] = tables["cols_used"] / tables["cols_total"] # Считаем долю реально нужных колонок.
tables["scan_pressure"] = tables["rows_mln"] * tables["width_ratio"] # Строим простую эвристику стоимости чтения.
print(tables[["table", "scan_pressure"]]) # Показываем, какие таблицы дают основное давление на сканирование.
heavy_sources = tables.sort_values("scan_pressure", ascending=False) # Сортируем таблицы по убыванию нагрузки.
print(heavy_sources) # Печатаем приоритеты для оптимизации запроса.Этот код не заменяет EXPLAIN и не моделирует внутренности конкретного движка. Но он хорошо учит главной интуиции: запрос дорожает там, где мы читаем много строк и много лишних колонок. Даже такая простая эвристика помогает обсуждать производительность не абстрактно, а через конкретные источники нагрузки.
Что важно вынести из темы
SQL performance в Data Science — это способность думать не только о правильности ответа, но и о вычислительной траектории к этому ответу. Хороший запрос уважает ресурсы хранилища: рано фильтрует, читает только нужные колонки, осторожно соединяет таблицы и не создаёт лишних промежуточных результатов. Именно это делает аналитику устойчивой, а пайплайны — дешёвыми и воспроизводимыми.
Если сформулировать совсем коротко, быстрый SQL — это не про хитрость, а про ясность. Чем лучше вы понимаете, что именно заставляете систему читать, джойнить, агрегировать и сортировать, тем сильнее становится ваша аналитика. И для Data Science это важно не меньше, чем выбор хорошей модели.
А когда запросы уже становятся частью реального ML-контура, их цена проявляется ещё сильнее: медленный слой подготовки данных начинает тормозить не только аналитику, но и выпуск модели в прод. Поэтому SQL performance хорошо завершать переходом к теме CI/CD для ML-сервисов, где вычислительная дисциплина продолжается уже на уровне всего сервиса.
Kaggle notebook по теме:
https://www.kaggle.com/code/jas325/getting-started-with-sql-and-bigquery