8 мин

Теория баз данных Джеффри Ульмана: основы быстрых и масштабируемых запросов

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

Теория баз данных Джеффри Ульмана: основы быстрых и масштабируемых запросов

Почему Ульман важен для современной работы с данными

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

Тихое влияние за привычными инструментами

Когда движок базы данных превращает ваш SQL в то, что он может быстро выполнить, он опирается на идеи, которые должны быть одновременно точными и адаптируемыми. Ульман помог формализовать смысл запросов (чтобы систему можно было безопасно переписывать) и связать мышление о базах данных с мышлением о компиляторах (чтобы запрос можно было распарсить, оптимизировать и перевести в исполняемые шаги).

Это влияние нельзя увидеть как кнопку в вашем BI‑инструменте или явную функцию в облачной панели — оно проявляется в:

  • запросах, которые ускоряются после добавления индекса или переписывания JOIN
  • оптимизаторах, которые выбирают разные планы по мере роста данных
  • системах, которые масштабируются, не меняя результата вашего запроса

Что вы узнаете из этой статьи (без перегруза математикой)

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

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

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

Основы баз данных, которые закрепил Ульман

Если вы когда‑то писали SQL и думали, что он «просто означает одно и то же», вы опирались на идеи, которые Джеффри Ульман помог популяризовать и формализовать: понятную модель данных плюс точные способы описания того, что запрашивается.

Реляционная модель простыми словами

В основе реляционной модели данные рассматриваются как таблицы (отношения). Каждая таблица имеет строки (кортежи) и столбцы (атрибуты). Это сейчас кажется очевидным, но важна дисциплина, которую это создаёт:

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

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

Реляционная алгебра: калькулятор для запросов

В обучении Ульмана реляционная алгебра часто выступает как своего рода калькулятор запросов: небольшой набор операций (select, project, join, union, difference), которые можно комбинировать, чтобы выразить требуемое.

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

Алгебра vs. исчисление (на высоком уровне)

  • Реляционная алгебра отвечает на «как»: последовательность операций для вычисления результата.
  • Реляционное исчисление больше про «что»: описание результата, который вы хотите.

SQL в основном формулирует «что», но движки часто оптимизируют, опираясь на алгебраический «как».

Основа важнее знания диалекта

Диалекты SQL различаются (Postgres vs Snowflake vs MySQL), но фундамент остаётся. Понимание ключей, связей и алгебраической эквивалентности помогает заметить, когда запрос логически неверен, когда он просто медленный и какие изменения сохранят смысл на разных платформах.

Реляционная алгебра: скрытый язык под SQL

Реляционная алгебра — это «математика под» SQL: небольшой набор операторов, которые описывают нужный результат. Работа Ульмана помогла сделать этот взгляд на операторы ёмким и удобным для обучения — и это до сих пор ментальная модель, которую используют большинство оптимизаторов.

Основные операторы (и что они значат)

Запрос можно представить как конвейер из нескольких строительных блоков:

  • Select (σ): фильтрация строк (аналог WHERE в SQL)
  • Project (π): оставить только нужные столбцы (аналог SELECT col1, col2)
  • Join (⋈): объединить таблицы по условию (JOIN ... ON ...)
  • Union (∪): сложить результаты одинаковой формы (UNION)
  • Difference (−): строки в A, которых нет в B (аналог EXCEPT)

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

Как SQL отображается в алгебру (концептуально)

Возьмём знакомый запрос:

SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;

Концептуально это:

  1. начать с join customers и orders: customers ⋈ orders

  2. select только заказы больше 100: σ(o.total > 100)(...)

  3. project нужный столбец: π(c.name)(...)

Это не точная внутренняя нотация каждой СУБД, но идея верна: SQL становится деревом операторов.

Эквивалентность: дверь в оптимизацию

Много разных деревьев могут означать одинаковый результат. Например, фильтры часто можно выносить раньше (применить σ перед большим join), а проекции — отбрасывать ненужные столбцы пораньше (применить π раньше).

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

От SQL к планам запросов: переписывания, сохраняющие смысл

Когда вы пишете SQL, база данных не выполняет его «как написано». Она переводит ваше выражение в план запроса — структурированное представление работы, которую нужно выполнить.

Полезная модель — это дерево операторов. Листья читают таблицы или индексы; внутренние узлы трансформируют и комбинируют строки. Обычные операторы: scan, filter (selection), project, join, group/aggregate, sort.

Логический план vs физический план (что vs как)

Обычно планирование разделяется на два уровня:

  • Логический план: что вычислять, выражено абстрактными операторами (фильтр, join, агрегат) и связями между ними.
  • Физический план: как выполнить это на реальном хранении и железе (индексный скан vs полный скан, hash join vs nested-loop, параллельный vs однопоточный).

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

Правила переписывания, которые экономят работу

Перед выбором финального подхода оптимизаторы применяют алгебраические «очистки». Эти переписывания не меняют результат; они сокращают лишнюю работу.

Примеры:

  • Selection pushdown: применять фильтры как можно раньше, чтобы в последующие шаги шло меньше строк.
  • Projection pruning: сохранять только нужные столбцы, уменьшая I/O и память.
  • Join reordering: сначала соединять более селективные/мелкие результаты, если это безопасно.

Простой пример переписывания

Предположим, нужно получить заказы пользователей из одной страны:

SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';

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

  • отфильтровать пользователей по country = 'CA'
  • затем соединить этих пользователей с orders
  • затем проектировать order_id и total

В терминах плана оптимизатор пытается превратить:

Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)

в нечто вроде:

Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)

Тот же ответ. Меньше работы.

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

Оптимизация на основе затрат без жаргона

Когда вы запускаете SQL, СУБД рассматривает несколько корректных способов получить тот же ответ и выбирает тот, который, по её оценке, будет самым дешёвым. Этот процесс называется cost-based optimization — и это одно из самых прикладных мест, где теоретические идеи Ульмана проявляются в повседневной производительности.

Что такое «модель затрат» на самом деле

Модель затрат — это система оценки, которой оптимизатор пользуется для сравнения планов. Большинство движков оценивают стоимость по нескольким ресурсам:

  • Обрабатываемые строки (работа часто масштабируется с объёмом данных в каждом шаге)
  • I/O (чтение страниц с диска или SSD, плюс эффекты кэша)
  • CPU (фильтрация, хеширование, сортировка, агрегация)
  • Память (влезет ли операция в RAM или будет сливать на диск)

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

Оценка кардинальности простыми словами

Перед тем как оценить план, оптимизатор задаёт вопрос на каждом шаге: сколько строк получится? Это и есть оценка кардинальности.

Если вы фильтруете WHERE country = 'CA', движок оценивает, какая доля таблицы подходит под условие. Если вы соединяете customers и orders, он оценивает, сколько пар совпадений получится по ключу. Эти предположения о количестве строк определяют, предпочтёт ли движок индексный поиск или полный скан, hash join или nested loop, и будет ли сортировка маленькой или огромной.

Почему статистика важна (и что идёт не так без неё)

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

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

Неизбежный компромисс: точность vs время планирования

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

Поэтому оптимизаторы балансируют две цели:

  • Планировать достаточно быстро для интерактивных сценариев
  • Планировать достаточно умно, чтобы избежать катастрофических ошибок

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

Алгоритмы соединений и сердце производительности запросов

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

Ульман помог популяризовать простую, но мощную идею: SQL не столько «выполняется», сколько переводится в план выполнения. Особенно это заметно на соединениях. Два эквивалентных по результату запроса могут резко отличаться по времени выполнения в зависимости от выбранного алгоритма соединения и порядка их применения.

Nested loop, hash join, merge join — когда какой подходит

Nested loop join прост по концепции: для каждой строки слева ищем подходящие строки справа. Он может быть быстрым, когда левая сторона мала, а правая сторона имеет полезный индекс.

Hash join строит хеш‑таблицу из одного входа (обычно меньшего) и пробует другие строки по ней. Он хорошо работает для больших неотсортированных входов при равенствах (например, A.id = B.id), но требует памяти; при сбросе на диск преимущество теряется.

Merge join одновременно проходит по двум входам в отсортированном порядке. Подходит, когда обе стороны уже упорядочены (или их можно дешёво отсортировать), например когда индексы выдают строки в порядке ключа.

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

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

Индексы меняют набор хороших планов

Индексы не просто ускоряют поиски — они делают некоторые стратегии соединений жизнеспособными. Индекс по ключу соединения может превратить дорогой вложенный цикл в быстрый «seek per row». И наоборот, отсутствие индекса может подтолкнуть движок к hash join или большим сортировкам для merge join.

Практический чеклист: симптомы плохого плана соединения

  • Время выполнения резко растёт при небольшом увеличении данных (возможно, порядок соединений создаёт огромные промежуточные результаты).
  • В плане видно огромное расхождение «оценено vs фактическое» по строкам (плохая кардинальность приводит к неверному выбору соединения).
  • Большие сортировки или хеш‑спиллы на диск (проблемы с памятью или отсутствующие поддерживающие индексы).
  • Маленькая отфильтрованная таблица соединяется поздно, а не рано (фильтры не применяются достаточно рано).
  • Предикат соединения не является чистым равенством совместимых типов (что мешает эффективной работе hash/merge).

Идеи из компиляторов внутри движков баз данных

Базы данных не просто «выполняют SQL». Они компилируют его. Влияние Ульмана охватывает теорию баз данных и мышление компиляторов — и это объясняет, почему движки запросов похожи на цепочки инструментов для языков программирования: они переводят, переписывают и оптимизируют, прежде чем выполнять работу.

Парсинг и синтаксические деревья: как читают SQL

Когда вы отправляете запрос, первый шаг похож на фронт‑энд компилятора. Движок разбивает ключевые слова и идентификаторы на токены, проверяет грамматику и строит дерево разбора (часто упрощаемое в абстрактное синтаксическое дерево). Здесь ловятся базовые ошибки: пропущенные запятые, неоднозначные имена столбцов, неверные правила группировки.

Полезная модель: SQL — это язык программирования, чья «программа» описывает отношения данных, а не циклы.

От дерева разбора к логическим операторам

Компиляторы переводят синтаксис в промежуточное представление (IR). Базы делают нечто похожее: переводят SQL‑синтаксис в логические операторы, такие как:

  • Selection (фильтрация строк)
  • Projection (выбор столбцов)
  • Join (объединение таблиц)
  • Aggregation (GROUP BY)

Эта логическая форма ближе к реляционной алгебре, чем к тексту SQL, и поэтому с ней легче работать в терминах смысла и эквивалентности.

Почему оптимизаторы похожи на оптимизации компилятора

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

  • выносить фильтры вперёд (меньше работы)
  • переставлять соединения (тот же результат, другая цена)
  • удалять избыточные вычисления

Это версия «удаления мёртвого кода» для баз данных: не те же техники, но та же философия — сохранить семантику, уменьшить стоимость.

Отладка: читать планы как скомпилированный код

Если ваш запрос медленный, не зацикливайтесь на SQL‑тексте. Посмотрите на план запроса так, как вы читаете вывод компилятора. План показывает, что движок действительно выбрал: порядок соединений, использование индексов и где тратится время.

Практический вывод: научитесь читать EXPLAIN как «листинг ассемблера» производительности. Это превращает настройку из гаданий в отлаживаемый процесс. Для практики смотрите /blog/practical-query-optimization-habits.

Теория проектирования схем, влияющая на реальную производительность

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

Хорошая производительность часто начинается ещё до написания SQL. Теория проектирования схем Ульмана (включая нормализацию) — о том, как структурировать данные, чтобы СУБД могла поддерживать корректность, предсказуемость и эффективность по мере роста.

Цели нормализации (зачем она нужна)

Нормализация направлена на:

  • Уменьшение аномалий (например, обновление адреса клиента в пяти местах и пропуск одного)
  • Улучшение согласованности за счёт хранения факта в одном «доме»
  • Выражаемость ограничений (ключи, внешние ключи), чтобы СУБД могла навязывать правила вместо кода приложения

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

Нормальные формы простыми словами

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

  • 1NF: храните атомарные значения (без списков в столбце). Это упрощает фильтрацию и индексацию.
  • 2NF: в таблицах с составным ключом каждый неключевой столбец зависит от всего ключа, а не от его части.
  • 3NF: неключевые столбцы должны зависеть только от ключа, а не от других неключевых столбцов.
  • BCNF: более строгая версия 3NF, где каждая детерминанта — кандидатный ключ; полезно при «почти уникальных» столбцах.

Когда денормализация оправдана

Денормализация может быть разумной при:

  • интенсивной аналитике (широкие fact‑таблицы для отчётности)
  • когда соединения становятся узким местом и вы готовы принять контролируемое дублирование
  • оптимизации чтения при наличии правил обновления (например, ночная перестройка)

Главное — денормализовать осознанно и иметь процесс синхронизации дублированных данных.

Как выбор схемы влияет на оптимизатор и масштабирование

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

Как теория проявляется при масштабировании систем

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

Масштаб — это чаще про физическое расположение и выбор плана

На малых объёмах многие планы «работают». На масштабе разница между сканированием таблицы, использованием индекса или использованием предвычисленного результата может означать секунды или часы. Теоретическая часть важна, потому что оптимизатору нужен безопасный набор правил переписывания (например, вынос фильтров, перестановка соединений), которые не меняют ответа — пусть они и радикально меняют выполняемую работу.

Партиционирование меняет «какой» запрос выполняется, даже если SQL не меняется

Партиционирование (по дате, клиенту, региону и т. п.) превращает одну логическую таблицу в множество физических кусков. Это влияет на планирование:

  • какие партиции можно пропустить (partition pruning)
  • происходят ли соединения внутри партиций или требуется перемешивание данных между узлами
  • можно ли группировать локально до глобального объединения

SQL‑текст может оставаться тем же, но лучший план теперь зависит от физического расположения строк.

Материализованные представления: предвычисление как алгебраические сокращения

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

Кэширование помогает, но не исправит неправильную форму работы

Кэш ускоряет повторные чтения, но он не спасёт запрос, который должен просканировать слишком много данных, перемешать огромные промежуточные результаты или вычислить гигантское соединение. При проблемах масштаба часто нужно: уменьшить объём обрабатываемых данных (layout/partitioning), убрать повторные вычисления (materialized views) или изменить план — а не просто «добавить кэш».

Практические привычки оптимизации, вдохновлённые Ульманом

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

1) Читайте EXPLAIN: с чего начинать

Начните с того, что обычно доминирует в времени выполнения:

  • Метод доступа: сканирует ли движок всю таблицу, когда вы ожидали индексный поиск?
  • Оценка строк vs фактические (если СУБД показывает оба): большие расхождения часто объясняют медлительность.
  • Порядок соединений: какая таблица драйвит соединение и начинается ли он с самого селективного фильтра?
  • Дорогие операторы: сортировки, сборы хеша, большие вложенные циклы — обычно они показывают, где действительно тратится время.

Если вы сделаете только одно — найдите первый оператор, где количество строк взрывается. Это обычно корень проблемы.

2) Распространённые анти‑паттерны, которые мешают оптимизатору

Легко написать, но дорого выполнять:

  • Функции над индексированными столбцами: WHERE LOWER(email) = ... может помешать использованию индекса (используйте нормализованный столбец или функциональный индекс, если поддерживается).
  • Отсутствующие предикаты: забытый диапазон по дате или фильтр по tenant превращает целевой запрос в широкое сканирование.
  • Случайные cross join: пропущенное условие соединения множит строки и порождает огромные промежуточные результаты.

3) Формулируйте гипотезу с алгебраическим мышлением

Реляционная алгебра подсказывает две практические вещи:

  • Выносите фильтры раньше: применяйте WHERE до соединений, когда это возможно, чтобы уменьшить входы.
  • Сокращайте столбцы рано: выбирайте только нужные столбцы (особенно до соединений), чтобы снизить память и I/O.

Хорошая гипотеза звучит так: «Это соединение дорогое, потому что мы соединяем слишком много строк; если сначала отфильтруем orders по последним 30 дням, вход для соединения уменьшится».

4) Индекс, переписывание или изменение схемы?

Простое правило решения:

  • Добавьте индекс, когда запрос корректен, селективен и повторяется.
  • Перепишите запрос, когда EXPLAIN показывает избыточную работу (ненужные соединения, поздние фильтры, несаргабельные предикаты).
  • Измените схему, когда паттерн нагрузки стабилен и вы постоянно боретесь с одним и тем же узким местом (предварительно вычисляемые агрегаты, денормализованные поля или партиционирование по времени/тенанту).

Цель не в «эффектном SQL», а в предсказуемых, маленьких промежуточных результатах — тот самый вид улучшений, которые делает проще заметить и реализовать теоретический фундамент Ульмана.

Применение этих идей при разработке продуктов

Выпустите более быстрый слой данных
Поднимите Go API с PostgreSQL и заранее проверьте индексы и фильтры.

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

Если вы используете vibe-coding рабочий процесс (например, генерируете приложение React + Go + PostgreSQL из чат‑интерфейса в Koder.ai), ментальные модели Ульмана — практичная страховка: вы можете проверить сгенерированную схему на предмет чистоты ключей и связей, просмотреть запросы, от которых зависит приложение, и проверить производительность через EXPLAIN до выхода в продакшен. Чем быстрее вы итеративно проходите «намерение запроса → план → фикc», тем больше выгоды приносит ускоренная разработка.

Где читать дальше и как применять это на работе

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

Ресурсы для начала

Ищите эти книги и лекционные темы (без аффилиации — просто популярные отправные точки):

  • «A First Course in Database Systems» (Ullman & Widom) — доступные основы баз данных с практической подачей.
  • «Principles of Database and Knowledge-Base Systems» (Ullman) — более глубокая теория.
  • «Compilers: Principles, Techniques, and Tools» (Aho, Lam, Sethi, Ullman) — для понимания, почему оптимизаторы напоминают компиляторы.
  • Темы для поиска: реляционная алгебра, переписывание запросов, порядок соединений, optimizaton на основе затрат, индексы и селективность, парсинг и языки запросов.

Лёгкий путь обучения

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

  1. Реляционная алгебра: изучите selection, projection, join и правила эквивалентности.
  2. Планы: научитесь читать ноды плана (типы сканов, фильтры, соединения, сортировки, агрегаты).
  3. Соединения: поймите различия nested loop vs hash join vs merge join и когда что выигрывает.
  4. Модель затрат: разберитесь с несколькими входами, которые определяют решения (число строк, селективность, I/O vs CPU).

Небольшие упражнения с быстрой отдачей

Выберите 2–3 реальных запроса и прогоняйте итерации:

  • Перепишите: замените IN на EXISTS, вынесите предикаты, сократите столбцы, сравните результаты.
  • Сравните планы: захватите планы «до/после» и отметьте, что изменилось (порядок соединений, тип соединения, тип скана).
  • Поиграйте с индексами: добавляйте/удаляйте индекс по одному и смотрите на оценки vs фактические числа строк.

Как доносить выводы команде

Говорите понятным план‑ориентированным языком:

  • «План сменился с последовательного скана на индексный, потому что фильтр стал селективным.»
  • «Оценка строк была ошибочна в 100×, поэтому оптимизатор выбрал неверный порядок соединений.»
  • «Это переписывание эквивалентно (тот же результат), но позволяет predicate pushdown и меньше строк в соединении.»

Это практический эффект основ Ульмана: общее словарное поле для объяснения производительности — без догадок.

FAQ

Кто такой Джеффри Ульман и почему его работа важна, если я только пишу SQL?

Джеффри Ульман помог формализовать то, как СУБД «представляют смысл запроса» и как они могут безопасно преобразовывать запросы в более быстрые эквиваленты. Это основа, которая проявляется каждый раз, когда движок переписывает запрос, меняет порядок соединений или выбирает иной план выполнения, при этом гарантируя тот же набор результатов.

Что такое реляционная алгебра и как она связана с SQL?

Реляционная алгебра — это небольшой набор операторов (select, project, join, union, difference), которые точно описывают результат запроса. Движки обычно переводят SQL в дерево операторов, похожее на алгебру, чтобы применять правила эквивалентности (например, вынос фильтров вперёд) перед выбором стратегии выполнения.

Почему «сохраняющие смысл» переписывания запросов важны на практике?

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

  • выносить WHERE перед соединением
  • раннее удаление ненужных столбцов
  • менять порядок соединений, когда это логически безопасно

Эти изменения могут резко сократить объём работы без изменения смысла.

В чём разница между логическим и физическим планом запроса?

Логический план описывает что нужно вычислить (фильтр, соединение, агрегирование) независимо от деталей хранения. Физический план выбирает как это выполнить (индексное чтение vs полный scan, hash join vs nested loop, параллелизм, стратегии сортировки). Большая часть различий в производительности связана с физическими решениями, которые становятся возможны после логических переписок.

Что такое cost-based optimization простыми словами?

Оптимизация на основе затрат сравнивает несколько корректных планов и выбирает тот, у которого наименьшая оценённая стоимость. Стоимости обычно определяют практические факторы: количество обрабатываемых строк, I/O, CPU и память (включая риск сброса хеша/сорта на диск).

Что такое оценка кардинальности и почему она приводит к непредсказуемой производительности?

Оценка кардинальности — это предположение оптимизатора о том, «сколько строк получится на выходе этого шага?» Эти оценки определяют порядок соединений, тип соединения и оправданность индексного поиска. Когда оценки ошибочны (часто из‑за устаревших или отсутствующих статистик), вы получаете резкие замедления, большие сбросы на диск или неожиданные смены плана.

Когда лучше использовать nested loop, hash join или merge join?
  • Nested loop join: хорошо, когда левая сторона мала, а правая может быстро искаться по индексу.
  • Hash join: отлично для больших входов по равенству, когда есть память для хеш‑таблицы; при сбросе на диск преимущество теряется.
  • Merge join: эффективен, когда обе стороны уже отсортированы или могут быть дешёво отсортированы (например, индекс даёт порядок по ключу соединения).
Как читать EXPLAIN‑план, чтобы не теряться?

Сосредоточьтесь на нескольких ключевых сигналах:

  • где «взрыв» числа строк (первая большая точка роста обычно и есть корень проблемы)
  • расхождения «оценено vs фактическое» (плохие статистики/предположения)
  • дорогие операторы (большие сортировки, сборы хеша, вложенные циклы по большим входам)
  • выбор доступа (полный scan вместо индексного поиска)

Смотрите на план как на скомпилированный код: он показывает, что движок фактически решил выполнить.

Как нормализация влияет на производительность запросов и когда допустима денормализация?

Нормализация уменьшает дублирование фактов и аномалии обновления, что часто даёт меньшие таблицы и индексы и более надёжные соединения. Денормализация уместна для аналитики или интенсивных чтений, но должна быть обдуманной (ясные правила обновления и контролируемое дублирование), чтобы целостность со временем не ухудшалась.

Какие приёмы помогают запросам оставаться быстрыми по мере роста объёма данных, не меняя результатов?

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

  • партиционирование для исключения партиций и локальности данных
  • материализованные представления для повторного использования эквивалентных подвыражений
  • обновление статистик и смена плана по мере роста данных

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

Похожие статьи