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

Большинство людей, которые пишут SQL, строят дашборды или настраивают медленные запросы, уже воспользовались работой Джеффри Ульмана — даже если никогда не слышали его имени. Ульман — учёный в области информатики и преподаватель, чьи исследования и учебники помогли определить, как базы данных описывают данные, рассуждают о запросах и выполняют их эффективно.
Когда движок базы данных превращает ваш SQL в то, что он может быстро выполнить, он опирается на идеи, которые должны быть одновременно точными и адаптируемыми. Ульман помог формализовать смысл запросов (чтобы систему можно было безопасно переписывать) и связать мышление о базах данных с мышлением о компиляторах (чтобы запрос можно было распарсить, оптимизировать и перевести в исполняемые шаги).
Это влияние нельзя увидеть как кнопку в вашем BI‑инструменте или явную функцию в облачной панели — оно проявляется в:
JOINВ этой статье мы используем ключевые идеи Ульмана как экскурсию по внутренностям баз данных, которые важны на практике: как реляционная алгебра лежит в основе SQL, как переписывания запросов сохраняют смысл, почему оптимизаторы на основе затрат принимают те решения, которые принимают, и как алгоритмы соединений часто решают — завершится ли задача за секунды или за часы.
Мы также привяжем несколько концепций из мира компиляторов — парсинг, переписывание и планирование — потому что движки баз данных ведут себя скорее как сложные компиляторы, чем многие предполагают.
Короткое обещание: обсуждение будет точным, но без тяжёлых доказательств. Цель — дать ментальные модели, которые вы сможете применить на работе в следующий раз, когда появятся проблемы с производительностью, масштабированием или непонятным поведением запроса.
Если вы когда‑то писали SQL и думали, что он «просто означает одно и то же», вы опирались на идеи, которые Джеффри Ульман помог популяризовать и формализовать: понятную модель данных плюс точные способы описания того, что запрашивается.
В основе реляционной модели данные рассматриваются как таблицы (отношения). Каждая таблица имеет строки (кортежи) и столбцы (атрибуты). Это сейчас кажется очевидным, но важна дисциплина, которую это создаёт:
Такая формулировка позволяет рассуждать о корректности и производительности без неточных утверждений. Когда вы понимаете, что представляет таблица и как идентифицируются строки, вы можете предсказать поведение соединений, смысл дубликатов и почему определённые фильтры меняют результаты.
В обучении Ульмана реляционная алгебра часто выступает как своего рода калькулятор запросов: небольшой набор операций (select, project, join, union, difference), которые можно комбинировать, чтобы выразить требуемое.
Почему это важно при работе с SQL: базы данных переводят SQL в алгебраическую форму и затем переписывают её в эквивалентную форму. Два запроса, выглядящие по‑разному, могут быть алгебраически одинаковыми — именно поэтому оптимизаторы могут менять порядок соединений, выносить фильтры вниз или удалять лишнюю работу, не меняя смысла.
SQL в основном формулирует «что», но движки часто оптимизируют, опираясь на алгебраический «как».
Диалекты SQL различаются (Postgres vs Snowflake vs MySQL), но фундамент остаётся. Понимание ключей, связей и алгебраической эквивалентности помогает заметить, когда запрос логически неверен, когда он просто медленный и какие изменения сохранят смысл на разных платформах.
Реляционная алгебра — это «математика под» SQL: небольшой набор операторов, которые описывают нужный результат. Работа Ульмана помогла сделать этот взгляд на операторы ёмким и удобным для обучения — и это до сих пор ментальная модель, которую используют большинство оптимизаторов.
Запрос можно представить как конвейер из нескольких строительных блоков:
WHERE в SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT)Поскольку набор маленький, проще рассуждать о корректности: если два алгебраических выражения эквивалентны, они возвращают одну и ту же таблицу для любого корректного состояния базы.
Возьмём знакомый запрос:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Концептуально это:
начать с join customers и orders: customers ⋈ orders
select только заказы больше 100: σ(o.total > 100)(...)
project нужный столбец: π(c.name)(...)
Это не точная внутренняя нотация каждой СУБД, но идея верна: SQL становится деревом операторов.
Много разных деревьев могут означать одинаковый результат. Например, фильтры часто можно выносить раньше (применить σ перед большим join), а проекции — отбрасывать ненужные столбцы пораньше (применить π раньше).
Именно правила эквивалентности позволяют базе переписать ваш запрос в более дешёвый план не меняя смысла. Как только вы начинаете смотреть на запросы через призму алгебры, «оптимизация» перестаёт быть магией и превращается в безопасное преобразование по правилам.
Когда вы пишете SQL, база данных не выполняет его «как написано». Она переводит ваше выражение в план запроса — структурированное представление работы, которую нужно выполнить.
Полезная модель — это дерево операторов. Листья читают таблицы или индексы; внутренние узлы трансформируют и комбинируют строки. Обычные операторы: scan, filter (selection), project, join, group/aggregate, sort.
Обычно планирование разделяется на два уровня:
Влияние Ульмана видно в акценте на преобразованиях, сохраняющих смысл: переставлять логический план разными способами, не меняя ответа, а затем выбирать эффективную физическую стратегию.
Перед выбором финального подхода оптимизаторы применяют алгебраические «очистки». Эти переписывания не меняют результат; они сокращают лишнюю работу.
Примеры:
Предположим, нужно получить заказы пользователей из одной страны:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Наивный подход мог бы соединить всех пользователей со всеми заказами, а потом отфильтровать Канаду. Сохранение смысла позволяет выносить фильтр, чтобы соединение касалось меньше строк:
country = 'CA'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 — и это одно из самых прикладных мест, где теоретические идеи Ульмана проявляются в повседневной производительности.
Модель затрат — это система оценки, которой оптимизатор пользуется для сравнения планов. Большинство движков оценивают стоимость по нескольким ресурсам:
Модель не обязана быть идеальной; ей нужно чаще всего правильно показывать направление, чтобы выбирать хорошие планы.
Перед тем как оценить план, оптимизатор задаёт вопрос на каждом шаге: сколько строк получится? Это и есть оценка кардинальности.
Если вы фильтруете WHERE country = 'CA', движок оценивает, какая доля таблицы подходит под условие. Если вы соединяете customers и orders, он оценивает, сколько пар совпадений получится по ключу. Эти предположения о количестве строк определяют, предпочтёт ли движок индексный поиск или полный скан, hash join или nested loop, и будет ли сортировка маленькой или огромной.
Догадки оптимизатора основаны на статистике: счётчиках, распределениях значений, доле NULL и иногда корреляциях между столбцами.
Когда статистика устарела или отсутствует, движок может ошибиться в оценке числа строк на порядки. План, который на бумаге выглядит дешёвым, в реальности может оказаться дорогим — классические симптомы: внезапное замедление после роста данных, «рандомная» смена планов или соединения, которые неожиданно сливают на диск.
Лучшие оценки часто требуют дополнительных затрат: более детальная статистика, выборка или исследование большего числа кандидатов. Но планирование само по себе занимает время, особенно для сложных запросов.
Поэтому оптимизаторы балансируют две цели:
Понимание этого компромисса помогает интерпретировать EXPLAIN: оптимизатор не стремится быть хитрым, он пытается быть предсказуемо верным при ограниченной информации.
Ульман помог популяризовать простую, но мощную идею: SQL не столько «выполняется», сколько переводится в план выполнения. Особенно это заметно на соединениях. Два эквивалентных по результату запроса могут резко отличаться по времени выполнения в зависимости от выбранного алгоритма соединения и порядка их применения.
Nested loop join прост по концепции: для каждой строки слева ищем подходящие строки справа. Он может быть быстрым, когда левая сторона мала, а правая сторона имеет полезный индекс.
Hash join строит хеш‑таблицу из одного входа (обычно меньшего) и пробует другие строки по ней. Он хорошо работает для больших неотсортированных входов при равенствах (например, A.id = B.id), но требует памяти; при сбросе на диск преимущество теряется.
Merge join одновременно проходит по двум входам в отсортированном порядке. Подходит, когда обе стороны уже упорядочены (или их можно дешёво отсортировать), например когда индексы выдают строки в порядке ключа.
При трёх и более таблицах количество возможных порядков соединений взрывается. Соединение двух больших таблиц первым может породить огромный промежуточный результат, который замедлит всё остальное. Лучший порядок часто начинается с наиболее селективного фильтра (наименьшее число строк) и идёт наружу, удерживая промежуточные результаты маленькими.
Индексы не просто ускоряют поиски — они делают некоторые стратегии соединений жизнеспособными. Индекс по ключу соединения может превратить дорогой вложенный цикл в быстрый «seek per row». И наоборот, отсутствие индекса может подтолкнуть движок к hash join или большим сортировкам для merge join.
Базы данных не просто «выполняют SQL». Они компилируют его. Влияние Ульмана охватывает теорию баз данных и мышление компиляторов — и это объясняет, почему движки запросов похожи на цепочки инструментов для языков программирования: они переводят, переписывают и оптимизируют, прежде чем выполнять работу.
Когда вы отправляете запрос, первый шаг похож на фронт‑энд компилятора. Движок разбивает ключевые слова и идентификаторы на токены, проверяет грамматику и строит дерево разбора (часто упрощаемое в абстрактное синтаксическое дерево). Здесь ловятся базовые ошибки: пропущенные запятые, неоднозначные имена столбцов, неверные правила группировки.
Полезная модель: SQL — это язык программирования, чья «программа» описывает отношения данных, а не циклы.
Компиляторы переводят синтаксис в промежуточное представление (IR). Базы делают нечто похожее: переводят SQL‑синтаксис в логические операторы, такие как:
GROUP BY)Эта логическая форма ближе к реляционной алгебре, чем к тексту SQL, и поэтому с ней легче работать в терминах смысла и эквивалентности.
Оптимизации компилятора сохраняют результат программы и делают её исполнение дешевле. Оптимизаторы баз данных делают то же самое, применяя правила типа:
Это версия «удаления мёртвого кода» для баз данных: не те же техники, но та же философия — сохранить семантику, уменьшить стоимость.
Если ваш запрос медленный, не зацикливайтесь на SQL‑тексте. Посмотрите на план запроса так, как вы читаете вывод компилятора. План показывает, что движок действительно выбрал: порядок соединений, использование индексов и где тратится время.
Практический вывод: научитесь читать EXPLAIN как «листинг ассемблера» производительности. Это превращает настройку из гаданий в отлаживаемый процесс. Для практики смотрите /blog/practical-query-optimization-habits.
Хорошая производительность часто начинается ещё до написания SQL. Теория проектирования схем Ульмана (включая нормализацию) — о том, как структурировать данные, чтобы СУБД могла поддерживать корректность, предсказуемость и эффективность по мере роста.
Нормализация направлена на:
Эти выигрыши по корректности позже дают выгоды в производительности: меньше дублирования, меньшие индексы и более дешёвые обновления.
Не нужно заучивать доказательства, чтобы применять идеи:
Денормализация может быть разумной при:
Главное — денормализовать осознанно и иметь процесс синхронизации дублированных данных.
Форма схемы определяет, что оптимизатор может сделать. Явные ключи и внешние ключи дают лучшие стратегии соединений, безопасные переписывания и более точные оценки числа строк. С другой стороны, избыточность увеличивает индексы и замедляет записи, а многозначные столбцы блокируют эффективные предикаты. По мере роста объёма данных ранние решения по моделированию часто важнее, чем микро‑оптимизация одного запроса.
Когда система масштабируется, дело редко только в добавлении более мощных машин. Часто сложнее то, что один и тот же смысл запроса нужно сохранять, в то время как движок выбирает совсем другую физическую стратегию, чтобы время выполнения оставалось предсказуемым. Уделённость Ульмана формальным эквивалентностям — как раз то, что позволяет менять стратегии, не меняя результатов.
На малых объёмах многие планы «работают». На масштабе разница между сканированием таблицы, использованием индекса или использованием предвычисленного результата может означать секунды или часы. Теоретическая часть важна, потому что оптимизатору нужен безопасный набор правил переписывания (например, вынос фильтров, перестановка соединений), которые не меняют ответа — пусть они и радикально меняют выполняемую работу.
Партиционирование (по дате, клиенту, региону и т. п.) превращает одну логическую таблицу в множество физических кусков. Это влияет на планирование:
SQL‑текст может оставаться тем же, но лучший план теперь зависит от физического расположения строк.
Материализованные представления — это, по сути, «сохранённые подвыражения». Если движок может доказать, что ваш запрос совпадает (или может быть переписан) с сохранённым результатом, он может заменить дорогую работу быстрым поиском. Это реляционная алгебра в действии: распознать эквивалентность и переиспользовать результат.
Кэш ускоряет повторные чтения, но он не спасёт запрос, который должен просканировать слишком много данных, перемешать огромные промежуточные результаты или вычислить гигантское соединение. При проблемах масштаба часто нужно: уменьшить объём обрабатываемых данных (layout/partitioning), убрать повторные вычисления (materialized views) или изменить план — а не просто «добавить кэш».
Влияние Ульмана проявляется в простой установке: рассматривайте медленный запрос как формулировку намерения, которую база свободна переписать, а затем проверяйте, что она реаль но решила сделать. Вам не нужно быть теоретиком, чтобы извлечь выгоду — достаточно повторяемой рутины.
Начните с того, что обычно доминирует в времени выполнения:
Если вы сделаете только одно — найдите первый оператор, где количество строк взрывается. Это обычно корень проблемы.
Легко написать, но дорого выполнять:
WHERE LOWER(email) = ... может помешать использованию индекса (используйте нормализованный столбец или функциональный индекс, если поддерживается).Реляционная алгебра подсказывает две практические вещи:
WHERE до соединений, когда это возможно, чтобы уменьшить входы.Хорошая гипотеза звучит так: «Это соединение дорогое, потому что мы соединяем слишком много строк; если сначала отфильтруем orders по последним 30 дням, вход для соединения уменьшится».
Простое правило решения:
EXPLAIN показывает избыточную работу (ненужные соединения, поздние фильтры, несаргабельные предикаты).Цель не в «эффектном SQL», а в предсказуемых, маленьких промежуточных результатах — тот самый вид улучшений, которые делает проще заметить и реализовать теоретический фундамент Ульмана.
Эти концепции полезны не только администраторам баз данных. Если вы разворачиваете приложение, вы принимаете решения про базу и планирование запросов, даже не замечая: форма схемы, выбор ключей, шаблоны запросов и слой доступа к данным — всё это влияет на то, что оптимизатор сможет сделать.
Если вы используете vibe-coding рабочий процесс (например, генерируете приложение React + Go + PostgreSQL из чат‑интерфейса в Koder.ai), ментальные модели Ульмана — практичная страховка: вы можете проверить сгенерированную схему на предмет чистоты ключей и связей, просмотреть запросы, от которых зависит приложение, и проверить производительность через EXPLAIN до выхода в продакшен. Чем быстрее вы итеративно проходите «намерение запроса → план → фикc», тем больше выгоды приносит ускоренная разработка.
Вам не нужно изучать теорию как отдельное хобби. Самый быстрый путь получить выгоду от основ Ульмана — научиться достаточно хорошо читать планы запросов и практиковаться на собственной базе.
Ищите эти книги и лекционные темы (без аффилиации — просто популярные отправные точки):
Начните с малого и связывайте каждый шаг с наблюдаемым результатом:
Выберите 2–3 реальных запроса и прогоняйте итерации:
IN на EXISTS, вынесите предикаты, сократите столбцы, сравните результаты.Говорите понятным план‑ориентированным языком:
Это практический эффект основ Ульмана: общее словарное поле для объяснения производительности — без догадок.
Джеффри Ульман помог формализовать то, как СУБД «представляют смысл запроса» и как они могут безопасно преобразовывать запросы в более быстрые эквиваленты. Это основа, которая проявляется каждый раз, когда движок переписывает запрос, меняет порядок соединений или выбирает иной план выполнения, при этом гарантируя тот же набор результатов.
Реляционная алгебра — это небольшой набор операторов (select, project, join, union, difference), которые точно описывают результат запроса. Движки обычно переводят SQL в дерево операторов, похожее на алгебру, чтобы применять правила эквивалентности (например, вынос фильтров вперёд) перед выбором стратегии выполнения.
Потому что оптимизация опирается на доказательства того, что переписанный запрос возвращает те же результаты. Правила эквивалентности позволяют оптимизатору, например:
WHERE перед соединениемЭти изменения могут резко сократить объём работы без изменения смысла.
Логический план описывает что нужно вычислить (фильтр, соединение, агрегирование) независимо от деталей хранения. Физический план выбирает как это выполнить (индексное чтение vs полный scan, hash join vs nested loop, параллелизм, стратегии сортировки). Большая часть различий в производительности связана с физическими решениями, которые становятся возможны после логических переписок.
Оптимизация на основе затрат сравнивает несколько корректных планов и выбирает тот, у которого наименьшая оценённая стоимость. Стоимости обычно определяют практические факторы: количество обрабатываемых строк, I/O, CPU и память (включая риск сброса хеша/сорта на диск).
Оценка кардинальности — это предположение оптимизатора о том, «сколько строк получится на выходе этого шага?» Эти оценки определяют порядок соединений, тип соединения и оправданность индексного поиска. Когда оценки ошибочны (часто из‑за устаревших или отсутствующих статистик), вы получаете резкие замедления, большие сбросы на диск или неожиданные смены плана.
Сосредоточьтесь на нескольких ключевых сигналах:
Смотрите на план как на скомпилированный код: он показывает, что движок фактически решил выполнить.
Нормализация уменьшает дублирование фактов и аномалии обновления, что часто даёт меньшие таблицы и индексы и более надёжные соединения. Денормализация уместна для аналитики или интенсивных чтений, но должна быть обдуманной (ясные правила обновления и контролируемое дублирование), чтобы целостность со временем не ухудшалась.
Чтобы масштабировать без изменения смысла запроса, обычно меняют физическую стратегию, оставляя логику нетронутой. Частые инструменты:
Кэширование помогает при повторных чтениях, но не вылечит запрос, который вынужден читать или объединять слишком много данных.