3. Типы баз данных. OLTP vs OLAP

Великий раскол баз данных

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

По мере эволюции любого успешного Go-бэкенда наступает день, когда основная транзакционная СУБД (например, PostgreSQL) начинает захлебываться. Вызов аналитического отчета или построение дашборда для руководства парализует обработку клиентских заказов: дисковая очередь взлетает до небес, страницы в оперативной памяти яростно вытесняются, а время ответа API подскакивает с 5 миллисекунд до десятков секунд.

Чтобы предотвратить подобные катастрофы, инженер обязан понимать фундаментальную причину великого архитектурного раскола: разницу между двумя полярными типами нагрузок — OLTP (Online Transaction Processing) и OLAP (Online Analytical Processing).


OLTP (Online Transaction Processing)

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

Ключевые характеристики OLTP-нагрузки:

  • Характер операций: Короткие, атомарные точечные запросы (CRUD: создание, чтение, обновление, удаление). Запрос затрагивает одну строку или небольшую пачку связанных сущностей по первичному ключу.
  • Степень параллелизма (Concurrency): Колоссальная. Тысячи и десятки тысяч запросов в секунду (RPS / TPS), поступающих одновременно от независимых клиентов.
  • Требования к надежности: Бескомпромиссная консистентность. Потеря даже одного финансового перевода недопустима — необходимо строгое следование гарантиям 1. ACID. Основы.
  • Объем полезных данных: Микроскопический (от нескольких сотен байт до десятков килобайт). Запрос SELECT * FROM users WHERE id = 42 возвращает конкретный профиль конкретного человека.

Mechanical Sympathy: Строковое хранение (Row-Oriented)

Чтобы СУБД могла атомарно обновить профиль пользователя (например, изменить баланс или адрес доставки), все атрибуты этого пользователя должны располагаться на диске физически рядом. Именно поэтому реляционные OLTP-базы (PostgreSQL, MySQL, Oracle) организуют хранение по принципу строкового формата (Row-Oriented Storage).

Файл таблицы на диске разбивается на страницы по 8 КБ (или 16 КБ), внутри которых кортежи строк записываются последовательно:
[ID1, Имя1, Возраст1, Баланс1] [ID2, Имя2, Возраст2, Баланс2]...


OLAP (Online Analytical Processing)

OLAP — это оперативная аналитическая обработка. Это мир бизнес-аналитики (BI), витрин данных (Data Warehousing) и машинного обучения. В такие системы выгружаются миллиарды исторических записей, чтобы ответить на стратегические вопросы: «Каков средний чек пользователей за последние три года по регионам?» или «Сколько кликов привел конкретный рекламный баннер в Черную Пятницу?».

Ключевые характеристики OLAP-нагрузки:

  • Характер операций: Тяжелые агрегационные запросы (SUM, AVG, COUNT, GROUP BY, фильтрация по диапазонам дат), охватывающие миллионы и миллиарды строк.
  • Степень параллелизма: Относительно невысокая. Десятки аналитиков или регулярных фоновых Cron-джобов, но каждый отдельный запрос утилизирует все доступные ядра CPU и максимальную пропускную способность дисковой подсистемы.
  • Требования к транзакциям: Сложные многошаговые транзакции с блокировками здесь избыточны. Данные поступают в хранилище пакетно (Append-Only) и практически никогда не обновляются точечно.
  • Объем обрабатываемых данных: Гигантский (сотни гигабайт и десятки терабайт на один аналитический отчет).

Mechanical Sympathy: Колоночное хранение (Column-Oriented)

Представьте, что мы пытаемся выполнить аналитический запрос к классической строковой OLTP-базе:

SQL
SELECT SUM(balance) FROM users;

В таблице 100 миллионов пользователей. В строковой СУБД для расчета суммы процессор вынужден поднять с диска в память все страницы таблицы целиком — с именами, хэшами паролей, номерами телефонов и адресами. Чтобы извлечь жалкие 400 МБ полезных чисел баланса, СУБД прочитает с накопителя 50–100 ГБ мусорных для этого запроса данных!

Специализированные аналитические СУБД (ClickHouse, Snowflake, Greenplum, Amazon Redshift) применяют диаметрально противоположный подход — колоночное хранение (Columnar Storage). Данные каждого столбца сохраняются в отдельных физических файлах.

flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph Row["Строковое хранение (OLTP - PostgreSQL)"]
        direction LR
        Page1["Страница 1<br>[1, Ivan, 30] [2, Anna, 25]"]
    end

    subgraph Col["Колоночное хранение (OLAP - ClickHouse)"]
        direction LR
        F1["Файл IDs<br>[1, 2, 3...]"]
        F2["Файл Names<br>[Ivan, Anna...]"]
        F3["Файл Ages<br>[30, 25...]"]
    end
  

Архитектурные паттерны в Go: Как писать в разные СУБД

Осознание физических различий OLTP и OLAP кардинально меняет то, как мы пишем код на Go для работы с этими системами.

1. Работа с OLTP (PostgreSQL / MySQL)

В OLTP мы ориентируемся на атомарность, быстрые одиночные коммиты и аккуратный пул соединений. Работа строится через стандартный пакет database/sql:

Go
// OLTP-стиль: атомарная быстрая вставка одной сущности
func CreateOrder(ctx context.Context, db *sql.DB, order Order) error {
    query := `INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id`
    return db.QueryRowContext(ctx, query, order.UserID, order.Total).Scan(&order.ID)
}

Каждый вызов изолирован, берет соединение из пула на миллисекунду и возвращает его обратно.

2. Работа с OLAP (ClickHouse)

Никогда не отправляйте точечные INSERT-запросы по одной строке в колоночную базу данных!

Колоночные СУБД используют под капотом архитектуру LSM-дерева или механизм слияния кусков (MergeTree, подробности в 12. ClickHouse под капотом). Каждая операция INSERT создает на диске независимый набор файлов (part) для каждого столбца. Если вы будете слать одиночные вставки в цикле под трафиком 5 000 событий в секунду, база создаст десятки тысяч мелких файлов. Фоновый процесс фонового слияния (Merge) захлебнется, и сервер упадет с фатальной ошибкой Too many parts in all data in table....

Канонический паттерн работы с OLAP на Go — это Батчинг (Batching). События аккумулируются в буфере памяти (или вычитываются пачками из топиков Apache Kafka) и сбрасываются в СУБД укрупненными блоками (от 5 000 до 50 000 строк за один сетевой запрос):

Go
// OLAP-стиль: буферизация и массовая вставка (Batch Insert)
// В реальности лучше использовать драйвер clickhouse-go и его метод PrepareBatch
func FlushAnalyticsToOLAP(ctx context.Context, conn driver.Conn, events []Event) error {
    batch, err := conn.PrepareBatch(ctx, "INSERT INTO user_events")
    if err != nil { return err }

    for _, e := range events {
        // Мы добавляем строки в локальный буфер драйвера, а не шлем по сети
        if err := batch.Append(e.UserID, e.EventType, e.Timestamp); err != nil {
            return err
        }
    }
    // Отправляем 10 000 строк одним TCP-пакетом и одним сжатым блоком на диск
    return batch.Send() 
}

Более детально этот паттерн и управление буферизацией мы разберем в статье 16. Batch запросы.


Итог

  1. OLTP (Row-Oriented): Оптимизировано для быстрых точечных записей и чтения сущностей целиком. Фундамент бизнес-логики и пользовательских сценариев (PostgreSQL, MySQL).
  2. OLAP (Column-Oriented): Оптимизировано для агрегации огромных объемов данных по ограниченному набору колонок. Требует обязательного батчинга при записи и векторизованной обработки при чтении (ClickHouse, Greenplum).
  3. Mechanical Sympathy: Разница в производительности кроется в физическом размещении байтов на дисковых страницах и кэш-линиях процессора. Выбирайте архитектуру хранения в строгом соответствии с профилем нагрузки вашей системы.

Теперь, когда мы осознали фундаментальное разделение подходов в мире СУБД, самое время сосредоточиться на фундаменте транзакционных систем. В следующей статье мы познакомимся с математической теорией, на которой держится вся индустрия реляционных баз: переходим к 4. Реляционная модель данных. Основы.