ТакПростоТакПросто.ai
ЦеныДля бизнесаОбразованиеДля инвесторов
ВойтиНачать

Продукт

ЦеныДля бизнесаДля инвесторов

Ресурсы

Связаться с намиПоддержкаОбразованиеБлог

Правовая информация

Политика конфиденциальностиУсловия использованияБезопасностьПолитика допустимого использованияСообщить о нарушении
ТакПросто.ai

© 2026 ТакПросто.ai. Все права защищены.

Главная›Блог›Джеффри Ульман и основы БД: теория оптимизации запросов
22 окт. 2025 г.·8 мин

Джеффри Ульман и основы БД: теория оптимизации запросов

Разбираем идеи Джеффри Ульмана: реляционная алгебра, переписывание запросов, оценка стоимости, планы выполнения и связь с компиляторами и масштабированием.

Джеффри Ульман и основы БД: теория оптимизации запросов

Почему Ульман важен для современных баз данных

Имя Джеффри Ульмана чаще звучит в контексте учебников и университетских курсов, но его идеи давно «живут» внутри промышленных СУБД. Когда вы пишете обычный SQL‑запрос, а система вдруг выполняет его за миллисекунды (или, наоборот, неожиданно медленно), за кулисами работает набор принципов, которые Ульман помог сформулировать: как представлять запрос формально, как преобразовывать его, как оценивать варианты исполнения и выбирать лучший.

Ульман и след, который виден в каждой СУБД

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

Что значит «теория» в мире практических запросов

Под теорией здесь понимаются не абстракции ради абстракций, а язык точных гарантий:

  • какие преобразования запроса сохраняют результат (а какие — нет);
  • почему один и тот же SQL можно выполнить множеством способов;
  • как сравнивать эти способы не «по ощущениям», а по цене: вводу‑выводу, памяти, числу операций.

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

О чём будет статья

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

Реляционная модель: от смысла запроса к его исполнению

Реляционная модель — это договор о том, как мы описываем данные и как задаём вопросы к ним. Данные представлены в виде таблиц (отношений): каждая строка — это запись (кортеж), а каждый атрибут — столбец с конкретным смыслом (например, customer_id, status, created_at). Важно, что модель говорит о значениях и связях, а не о том, как именно они лежат на диске.

Реляционная алгебра как «язык смысла»

SQL выглядит как язык для людей, но внутри СУБД стремится перевести его в более строгую форму — набор операций, похожих на конструктор: выборка (σ), проекция (π), соединение (⨝), группировка и т.д. Это и есть реляционная алгебра — «язык смысла» запроса.

Например, запрос «взять активных клиентов и их заказы за декабрь» на уровне смысла — это:

  • отфильтровать клиентов по status = 'active';
  • отфильтровать заказы по дате;
  • соединить по customer_id;
  • оставить нужные столбцы.

Логика против физики: где начинается оптимизация

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

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

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

Если сначала соединить две большие таблицы, а фильтр применить потом, СУБД может «таскать» миллионы лишних строк. Если же фильтр протолкнуть раньше, соединение станет меньше и дешевле. Формально результат тот же — но путь к нему разный. Именно на этом различии и строится оптимизация запросов.

SQL и реляционная алгебра: как СУБД «понимает» запрос

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

SQL как декларация намерения

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

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

Алгебраическое дерево запроса

Реляционная алгебра — это набор операций над отношениями (таблицами), из которых можно собрать запрос как выражение. На практике это выражение хранится как дерево:

  • выборка (σ) — фильтрация строк (WHERE);
  • проекция (π) — выбор столбцов (SELECT без агрегатов);
  • соединение (⨝) — связывание таблиц по условию (JOIN ... ON).

Например, запрос «взять имена клиентов, у которых есть заказы за декабрь» можно представить как цепочку: сначала соединение клиентов с заказами, затем фильтр по дате, затем проекция по имени. Важно, что это логическое дерево: оно описывает смысл, но ещё не говорит, каким алгоритмом всё будет выполнено.

Где появляются возможности для преобразований

Как только запрос стал деревом, оптимизатор может безопасно менять его форму:

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

Эквивалентность запросов простыми словами

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

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

Переписывание запросов: быстрые выигрыши без магии

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

Проталкивание фильтров (selection pushdown)

Самое практичное правило: применять условия WHERE как можно раньше — до соединений и агрегаций.

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

Перестановка и ассоциативность соединений

Соединения можно переупорядочивать, не меняя результат (для внутреннего INNER JOIN). Это не «хитрость оптимизатора», а свойство операции.

  • Коммутативность: A ⋈ B эквивалентно B ⋈ A
  • Ассоциативность: (A ⋈ B) ⋈ C эквивалентно A ⋈ (B ⋈ C)

Переписывание на этом уровне позволяет подставить «маленькие» таблицы или уже отфильтрованные наборы раньше, резко сокращая объём промежуточных данных.

Устранение лишних столбцов и вычислений

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

Аналогично с вычислениями: выражения, которые можно не считать на миллионах строк (или можно посчитать после фильтра), лучше отложить.

Материализация подзапросов: когда полезно, а когда нет

Иногда подзапрос выгодно материализовать: один раз посчитать результат и переиспользовать, особенно если он используется многократно или сильно сокращает данные.

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

Почему это работает ещё до выбора индексов и алгоритмов

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

Модель стоимости: как оптимизатор выбирает план

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

Почему «самый короткий план» не всегда самый быстрый

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

Из чего складывается стоимость

Обычно оптимизатор считает несколько компонентов:

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

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

Селективность и кардинальность

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

Статистика и гистограммы

Чтобы не гадать, СУБД хранит статистику: число строк, распределения значений, частоты. Гистограммы помогают понять, что значения распределены неравномерно (например, один регион встречается в 80% строк), и точнее оценить селективность.

Почему оценки ошибаются

Типовые источники промахов:

  • Скос данных (очень популярные значения).
  • Корреляции между колонками (например, город зависит от страны), из‑за которых независимые оценки «перемножением» дают неверный результат.
  • Устаревшая статистика после массовых вставок/удалений.

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

Порядок соединений: главная точка принятия решений

API, где важен join order
Опишите эндпоинты и модели, а TakProsto соберет backend на Go с PostgreSQL под ваши запросы.
Создать API

Когда в запросе несколько таблиц, СУБД почти всегда тратит больше всего «интеллектуальных усилий» на выбор порядка соединений (join order). Формально результат соединения ассоциативен: можно соединять A с B, а потом с C — или сначала B с C. Но по затратам эти варианты могут отличаться в разы, потому что промежуточные результаты бывают либо маленькими и «дешёвыми», либо огромными и дорогими по памяти и вводу‑выводу.

Что такое join order и почему он критичен

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

Идея динамического программирования — без формул

Классический подход из учебников Ульмана: рассматривать не «один план целиком», а лучший план для каждого подмножества таблиц. СУБД как бы собирает решение снизу вверх: нашла лучший способ соединить (A,B), лучший для (B,C), затем пробует расширять эти кусочки третьей таблицей и выбирает минимальную стоимость. Это даёт качество, близкое к оптимальному, но дорого при большом числе таблиц.

Эвристики, когда полный перебор слишком дорог

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

Как ограничения сужают пространство планов

Ключи и внешние ключи помогают не только проверять целостность, но и оптимизировать:

  • внешний ключ подсказывает, что соединение «многие‑к‑одному» не должно неожиданно умножить строки;
  • уникальность ключа даёт более точные оценки кардинальности;
  • некоторые соединения становятся фактически обязательными или, наоборот, могут быть безопасно переупорядочены.

Чем лучше СУБД знает ограничения и статистику, тем меньше «слепых» вариантов join order ей приходится перебрать.

Алгоритмы выполнения: что реально делает СУБД

Оптимизатор не просто «выбирает порядок таблиц», он выбирает конкретные алгоритмы, которыми движок будет читать строки, сопоставлять ключи, сортировать и считать агрегаты. Эти решения напрямую влияют на время ответа и нагрузку на диск/память.

Соединения: nested loop, hash join, sort-merge

Nested loop join уместен, когда внешняя таблица маленькая, а по внутренней есть хороший индекс по ключу соединения: тогда для каждой строки делается быстрый точечный поиск. Если индекса нет или внешняя сторона велика, nested loop быстро превращается в «миллионы мелких обращений» и тормозит.

Hash join обычно выигрывает на больших объёмах при равенстве ключей. СУБД строит хеш-таблицу по одной стороне (build), затем пробегает вторую (probe). Важно, помещается ли хеш-таблица в память: при нехватке памяти появятся разбиения/сбросы на диск.

Sort-merge join хорош, когда данные уже отсортированы (например, благодаря индексу) или сортировка всё равно нужна дальше по плану. Он устойчив на больших наборах, но может быть дорогим из‑за сортировки.

Индексы и не только: фильтры, сортировки, группировки

Индекс ускоряет не только WHERE, но и соединения (по ключу) и иногда ORDER BY — если порядок совпадает. Агрегаты и GROUP BY могут выполняться как Hash Aggregate (быстро, если помещается в память) или Group Aggregate после сортировки (дороже, но иногда неизбежно).

Почему кардинальность решает

Выбор алгоритма завязан на оценку кардинальности: сколько строк пройдет фильтр, сколько совпадений даст join, насколько «раздуются» промежуточные результаты. Ошибка в оценках часто приводит к неправильному выбору (например, nested loop вместо hash join).

Как читать план выполнения (EXPLAIN) и искать узкие места

Смотрите не на «красоту SQL», а на фактические узлы плана и расхождения оценок с реальностью:

  • какой тип соединения выбран и есть ли для него опора на индекс/сортировку;
  • где возникают большие проходы по таблицам (например, Seq Scan) и можно ли сузить их предикатом;
  • реальные vs оценённые строки (сильное расхождение — повод проверить статистику и условия);
  • наличие дорогих Sort/Hash и признаки spill на диск;
  • узлы‑лидеры по времени/строкам — они дают максимальный эффект от правок.

Связь с компиляторами: запрос как программа

Соберите стек под вашу БД
Опишите данные и сценарии, а TakProsto соберет React, Go и PostgreSQL по диалогу.
Начать чат

Идея Ульмана, которая особенно хорошо «цепляется» у тех, кто хоть раз видел устройство компилятора: SQL‑запрос — это не просто строка, а программа над данными. СУБД проходит похожий путь: от текста к структуре, от структуры к оптимизированному «исполняемому плану».

От SQL к дереву: лексинг, парсинг и AST

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

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

Оптимизация как набор проходов

Как и в компиляторах, оптимизация — это серия проходов над представлением программы:

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

Важный момент: многие «умные» ускорения получаются не за счёт магии, а за счёт систематических правил, похожих на компиляторные преобразования.

Генерация кода выполнения

Компилятор выдаёт машинный код, а СУБД — план выполнения: последовательность операторов (сканирование, соединение, сортировка и т. п.) с конкретными методами доступа (например, через индекс). Это тоже своего рода «код», только исполняемый интерпретатором запросов или JIT‑модулем.

Где граница возможностей оптимизатора

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

Память, ввод‑вывод и промежуточные результаты

Оптимизация запроса почти всегда упирается не в «скорость CPU», а в то, как часто СУБД вынуждена ходить на диск и сколько данных тащит через память. Идеи Ульмана полезны тем, что учат мыслить планом выполнения как потоком данных с ограниченными ресурсами.

Кэш и буферный пул: почему «теория» меняется в продакшене

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

Последовательный доступ против случайного

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

Пайплайнинг vs материализация

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

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

Компромисс между временем и памятью — и что можете сделать вы

Пользователь влияет на план сильнее, чем кажется:

  • Индексы: добавляйте их под реальные фильтры и соединения, следите за селективностью.
  • Схема: выбирайте правильные типы, нормализуйте/денормализуйте осознанно, избегайте лишних широких строк.
  • Запросы: выбирайте только нужные колонки, фильтруйте раньше, не заставляйте СУБД материализовать гигантские промежуточные наборы (например, из‑за неаккуратных JOIN и DISTINCT).

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

Масштабирование: от одного сервера к распределённым системам

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

Разделение данных: партиционирование и шардинг на уровне идей

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

Распределённые соединения: почему они дорогие

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

Pushdown вычислений ближе к данным

Одна из самых практичных идей — «проталкивать» фильтры, проекции и частичные агрегации туда, где данные находятся. Чем меньше строк и столбцов уедет по сети, тем дешевле план. Это прямое продолжение классических правил переписывания запросов, только выигрыш теперь измеряется не только в CPU, но и в мегабайтах трафика.

Что меняется в оптимизации при кластере и сети

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

Практические примеры: как теория превращается в ускорение

Заберите исходники и продолжайте
Получите исходники проекта и доведите оптимизацию SQL и схемы в привычном окружении.
Экспортировать код

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

Кейс 1: фильтры и проекции — как уменьшить объём данных раньше

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

Практика: переносить условия из WHERE как можно ближе к чтению таблицы и выбирать только нужные столбцы — особенно перед JOIN и GROUP BY.

Кейс 2: смена порядка соединений — почему результат тот же, а скорость разная

Реляционная алгебра разрешает переупорядочивать соединения (ассоциативность/коммутативность), и результат будет тем же. Но стоимость — разная.

Практика: сначала соединять/фильтровать «самые селективные» таблицы (те, что сильнее уменьшают количество строк), а уже потом подтягивать большие справочники. Иногда достаточно уточнить предикат, чтобы оптимизатор увидел селективность; иногда — обновить статистику.

Кейс 3: агрегаты и группировки — когда нужна сортировка, а когда хэш

Группировка может выполняться через сортировку (Sort + Group) или через хеширование (Hash Aggregate). Хэш обычно выигрывает на больших объёмах без подходящего порядка, а сортировка может быть быстрее, если данные уже идут в нужной последовательности (например, по индексу) или если требуется упорядоченная выдача.

Ограничения: почему советы зависят от данных и статистики

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

Итоги и как использовать идеи Ульмана в работе

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

Что стоит вынести из теории

Во‑первых, реляционная алгебра: она объясняет, почему СУБД может переставлять операции (например, проталкивать фильтры ближе к данным) и всё равно получать тот же результат.

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

В‑третьих, стоимость и планы выполнения: СУБД выбирает не «самый умный», а «самый дешёвый по модели» план. Отсюда главный практический вывод: модель ошибается, когда ошибаются оценки кардинальности.

Как применять на практике

Сделайте привычкой начинать не с переписывания SQL, а с проверки плана:

  • смотрите EXPLAIN/EXPLAIN ANALYZE и фиксируйте, где тратится время: join, сортировка, чтение с диска, агрегация;
  • задавайте себе вопрос: «Какая кардинальность ожидается на каждом шаге?» и совпадает ли она с реальностью;
  • проверяйте селективность фильтров, актуальность статистики, необходимость лишних столбцов и строк.

Часто самые ощутимые улучшения дают простые действия: убрать ненужный JOIN, добавить явный предикат, изменить порядок вычислений через подзапрос/CTE (с учётом особенностей вашей СУБД), добавить подходящий индекс или обновить статистику.

Если вы делаете продукт, где база данных — часть типового стека (например, backend на Go и PostgreSQL), удобно закладывать эти проверки прямо в процесс разработки. В TakProsto.AI, где приложения собираются через чат в режиме vibe‑coding и можно быстро получать рабочие инкременты, особенно полезно фиксировать требования к запросам и нагрузке в planning mode, а затем безопасно проверять изменения схемы и индексов с помощью snapshots и rollback.

Что изучать дальше

Дальше полезно чередовать теорию и «железо» конкретной СУБД: документацию по оптимизатору, статистике и типам join, а также материалы по внутреннему устройству исполнителя. Хорошая цель — уметь объяснить любой узел плана человеческими словами.

Следующий шаг для вашего проекта

Возьмите 10–20 самых частых или самых дорогих запросов из логов и проведите мини‑аудит: план, кардинальности, узкие места, гипотезы, изменения, повторное измерение. Это самый быстрый способ превратить идеи Ульмана в измеримое ускорение.

FAQ

Почему Ульман вообще важен для производительности SQL-запросов?

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

Практический эффект: оптимизатор может ускорять запросы без изменения результата, системно сравнивая альтернативы по стоимости (I/O, CPU, память).

Зачем СУБД переводит SQL в реляционную алгебру?

Реляционная алгебра — это внутренний «язык смысла», в который СУБД переводит SQL: выборка (фильтры), проекция (столбцы), соединения, группировки.

Она полезна тем, что даёт:

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

Потому что логически эквивалентные запросы могут требовать радикально разный объём работы.

Чаще всего скорость меняется из‑за:

  • порядка применения фильтров (до/после JOIN);
  • порядка соединений (join order);
  • необходимости сортировок и размера промежуточных результатов.
Что такое переписывание запросов и когда оно помогает больше всего?

Переписывание (rewriting) — это преобразования, которые сохраняют смысл, но уменьшают «лишнюю работу» ещё до выбора индексов и алгоритмов.

Типовые действия:

  • протолкнуть фильтры ниже (selection pushdown);
  • убрать ненужные столбцы раньше (ранняя проекция);
  • переупорядочить INNER JOIN, если это безопасно.
Что такое порядок соединений (join order) и почему это главный источник проблем?

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

Практика: старайтесь, чтобы самые селективные условия (сильно уменьшающие строки) применялись как можно раньше — через явные предикаты, корректные JOIN-условия и актуальную статистику.

Как оптимизатор решает, какой план выполнения «дешевле»?

Оптимизатор выбирает план по модели стоимости: приблизительно оценивает, сколько ресурсов потребует каждый вариант.

Обычно учитываются:

  • ввод‑вывод (чтения/записи страниц);
  • CPU (сравнения, вычисления);
  • сортировки/хеширование и возможные spill на диск.

Вывод: «короткий план» не обязан быть быстрым — важнее, сколько данных реально прочитали и переработали.

Почему оценка кардинальности так сильно влияет на план выполнения?

Кардинальность — сколько строк ожидается на каждом этапе плана. От неё зависят:

  • выбор индекса vs полный скан;
  • выбор алгоритма JOIN (nested loop/hash/sort-merge);
  • потребление памяти и риск spill.

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

Почему оптимизатор ошибается и выбирает медленный план?

Частые причины:

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

Практический шаг: если EXPLAIN ANALYZE показывает сильный разрыв между estimated rows и actual rows, начните с обновления статистики и проверки предикатов.

Как правильно читать EXPLAIN/EXPLAIN ANALYZE, чтобы найти узкое место?

Смотрите на узлы, которые дают максимум времени/строк, и на расхождения оценок.

Мини‑чек‑лист:

  • большие сканы без селективного фильтра;
  • дорогие Sort/Hash и признаки spill на диск;
  • неожиданный рост строк после JOIN;
  • actual vs estimated (если разница кратная — проблема в оценках).

Дальше действуйте точечно: индекс, переписывание предиката, обновление статистики, упрощение JOIN-цепочки.

Как идеи оптимизации запросов работают в распределённых базах данных?

В распределённых системах к стоимости добавляется сеть: пересылка данных часто дороже локальных вычислений.

Поэтому особенно важны:

  • pushdown фильтров/проекций/частичных агрегаций «ближе к данным»;
  • выбор ключей партиционирования/шардинга, чтобы JOIN становился локальным;
  • контроль перекосов (skew), когда одна партиция перегружена.

Идея та же, что и на одном сервере: минимизировать промежуточные результаты, но теперь это ещё и трафик.

Содержание
Почему Ульман важен для современных баз данныхРеляционная модель: от смысла запроса к его исполнениюSQL и реляционная алгебра: как СУБД «понимает» запросПереписывание запросов: быстрые выигрыши без магииМодель стоимости: как оптимизатор выбирает планПорядок соединений: главная точка принятия решенийАлгоритмы выполнения: что реально делает СУБДСвязь с компиляторами: запрос как программаПамять, ввод‑вывод и промежуточные результатыМасштабирование: от одного сервера к распределённым системамПрактические примеры: как теория превращается в ускорениеИтоги и как использовать идеи Ульмана в работеFAQ
Поделиться
ТакПросто.ai
Создайте свое приложение с ТакПросто сегодня!

Лучший способ понять возможности ТакПросто — попробовать самому.

Начать бесплатноЗаказать демо