Своя СУБД на Haskell · урок 6: анализ, план, исполнитель, EXPLAIN
Кому это. Ты прошёл урок 5: строка SQL превращается в дерево. Под ним хранилище из уроков 1 до 4: страницы, пул, куча и индекс. Сегодня их соединит верхняя половина движка, и SELECT * FROM users WHERE id = 2 наконец вернёт строку. Тот же урок есть в TS-треке. В Haskell-порте видна граница, к которой мы шли пять уроков: binder и планировщик это чистые функции над видом каталога, Either DbError от начала до конца, а IO начинается только там, где операторы тянут строки из кучи через Stream из урока 3.
Важно про код. Урок большой: это вся верхняя половина движка разом. Но ни одна фаза не возится с байтами. Эталон ourdb на Haskell написан сразу под все одиннадцать уроков, и его операторы уже несут контекст транзакции, блокировки и журнал. В листингах этого урока показана форма этапа 6, та самая, что проверяют тесты этапа: строки про журнал и транзакции (уроки 7 до 9) вырезаны, и каждое такое место отмечено комментарием -- урок N:. Сигнатуры от этого становятся короче, но имена и устройство функций те же, что в эталоне.
Где мы в движке
┌────────────────────────────────────────────────┐
│ Бинарный протокол + TCP-сервер │ урок 11
├────────────────────────────────────────────────┤
│ Исполнитель (Volcano) + EXPLAIN │ урок 6 ← мы здесь
│ Парсер, лексер, pretty-printer │ урок 5
├────────────────────────────────────────────────┤
│ Vacuum │ урок 10
│ Блокировки (2PL), граф ожидания │ урок 9
│ MVCC, менеджер транзакций │ урок 8
│ WAL (журнал, redo, recovery) │ урок 7
├────────────────────────────────────────────────┤
│ B+Tree индекс │ урок 4
│ Куча (slotted pages, seq scan) │ урок 3
├────────────────────────────────────────────────┤
│ Буферный пул (кэш страниц, вытеснение) │ урок 2
│ DiskManager (страницы поверх файла) │ урок 1
└────────────────────────────────────────────────┘
Конвейер запроса
Text ──parseCommand──▶ Command ──bind──▶ LogicalPlan ──plan──▶ Operator ──pull──▶ строки
(урок 5, чисто) (чисто, вид каталога) (чисто, вид каталога) (IO, Stream)
Четыре фазы, и только последняя живёт в IO.
- Разбор.
parseCommandиз урока 5 даётCommand. - Анализ (binding). Binder берёт дерево и вид каталога, проверяет, что таблица и колонки существуют, превращает имена колонок в их номера и выводит типы выражений. Результат это
LogicalPlan: что запрос хочет сделать, без решения как. - Планирование. Планировщик решает как: пройти всю кучу или спуститься по индексу. Результат это дерево физических операторов.
- Исполнение. Операторы тянут строки друг из друга по одной, это модель Volcano.
Binder и планировщик не трогают диск вообще. Им нужен только вид каталога: функция, которая по имени таблицы отдаёт схему или доступ к куче. Каталог отдаёт такой вид в начале каждого запроса, и дальше обе фазы это Either DbError без единого IO.
Ошибки движка: тот же DbError
Новых типов ошибок нет. Binder возвращает EngineError с текстом (“неизвестная колонка “bad""), проверка уникальности первичного ключа бросает UniquenessError. Оба конструктора уже есть в DbError с урока 1. В TS-эталоне UniquenessError это подкласс EngineError, и тесты проверяют instanceof EngineError. В Haskell подклассов нет, поэтому в OurDb.Error есть предикат:
-- из src/OurDb/Error.hs
-- | EngineError или его подкласс (в эталоне UniquenessError и прочие наследуют EngineError).
isEngineError :: DbError -> Bool
isEngineError = \case
SqlError {} -> False
StorageError {} -> False
_ -> True
engineError :: Text -> IO a
engineError = throwIO . EngineError
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.