OLTP обслуживает приложение: много коротких транзакций, точечные чтения и записи по ключу, нормализованная схема, строгие ограничения, важна задержка одной операции. OLAP обслуживает аналитику: мало запросов, но каждый читает миллионы строк, агрегаты по колонкам, денормализованные витрины, важна пропускная способность. Отсюда разные движки: строковые базы с индексами для OLTP, колоночные и MPP для OLAP. Нагрузка одного на базу другого плохо заканчивается: аналитический запрос блокирует и тормозит приложение, а OLTP-база не выдержит сканирований.
База данных хранит текущее состояние для работы приложения. Хранилище собирает данные из многих источников, хранит историю, приводит данные к единым справочникам и моделям и оптимизировано под аналитические запросы. В хранилище данные не меняются приложением, а загружаются пайплайнами по расписанию, поэтому там допустимы денормализация, отсутствие внешних ключей и перезапись периодов.
Колоночное хранение читает с диска только нужные колонки, а однотипные данные в колонке сжимаются в разы лучше. Аналитический запрос «сумма по двум колонкам из пятидесяти» читает 4% данных. Плюс векторизованная обработка. Минус: точечные обновления и чтение целой строки дороги, поэтому для OLTP колоночные базы не подходят. ClickHouse, Parquet, колоночные таблицы Greenplum устроены так.
DWH: структурированные данные в базе со схемой, строгими слоями и SQL, дорого масштабировать объём. Data Lake: файлы любого формата в объектном хранилище, схема при чтении, дёшево хранить всё, но без транзакций и с риском «болота». Lakehouse: файлы в озере плюс табличный формат (Iceberg, Delta, Hudi), который даёт схему, транзакции, обновления и версии, и SQL-движки поверх (Trino, Spark). Выбор зависит от объёма, разнообразия данных и того, кто и чем их читает.
Greenplum это PostgreSQL, размноженный на сегменты: таблицы распределяются по узлам по ключу, запрос выполняется параллельно, добавляются Motion-операции для перемещения строк между узлами. Это MPP для аналитики с полноценным SQL и JOIN, но с медленными точечными операциями. ClickHouse колоночный, заточен на вставку больших порций и агрегаты по одной широкой таблице со скоростью, недостижимой для Greenplum, но хуже с JOIN больших таблиц, без полноценных транзакций и обновлений (ReplacingMergeTree вместо UPDATE). Greenplum для хранилища с моделью и сложными соединениями, ClickHouse для витрин, событий и дашбордов.
Монолит выполняет запрос на одном узле, масштабируется вертикально и ограничен его диском и памятью. MPP разбивает данные по узлам (shared nothing), каждый узел обрабатывает свою часть параллельно, координатор собирает результат; масштабирование добавлением узлов. Цена: данные нужно правильно распределить, соединения по разным ключам требуют пересылки по сети, перекос данных делает один узел узким местом, а точечные операции и транзакции дороже, чем в монолите.
Raw / staging: копируют как есть, фиксируют время загрузки, ничего не меняют. ODS (операционный слой): очищают, приводят типы, дедублицируют, иногда хранят короткую историю. Core / DDS (детальный слой): интегрируют источники в единую модель (Kimball, Data Vault, якорная), ведут историю по SCD, задают ключи. Data mart (витрины): агрегируют и денормализуют под конкретных потребителей, считают метрики. Глаголы для ответа: raw копирует, staging приводит, core интегрирует и хранит историю, mart отвечает на вопрос бизнеса.
Денормализован. Витрина отвечает на запросы аналитиков и BI, а соединения из десяти таблиц в каждом запросе это медленно и ошибкоопасно (задвоения, потерянные строки). Денормализация даёт один-два JOIN или вообще одну широкую таблицу, предсказуемую производительность и понятную для потребителя структуру. Цена: избыточность и необходимость пересчитывать витрину при изменении справочников, что и делает пайплайн.
Полное состояние таблицы или витрины на момент времени, сохранённое отдельно (партиция по дате снимка). Снимки позволяют сравнить состояние на две даты, пересчитать отчёт «как было», откатиться. Альтернатива снимкам это хранение изменений (SCD2, diff): экономнее по месту, но запрос «состояние на дату» сложнее.
ETL: извлечь, преобразовать на отдельном движке (Spark, Informatica, Python), загрузить готовое. ELT: извлечь, загрузить сырое в хранилище, преобразовать там SQL-запросами (dbt поверх Greenplum, ClickHouse, Snowflake). ELT проще и использует мощность хранилища, ETL нужен, когда хранилище слабое, данных слишком много для него или преобразование не выражается в SQL (парсинг, ML, вызовы API). Трансформацию на источнике делают только лёгкую фильтрацию, чтобы не нагружать боевую базу.
Инмон строит сначала нормализованное корпоративное хранилище (3НФ) как единый источник истины, а витрины отдельно сверху: долго, строго, хорошо для больших организаций. Кимбалл строит хранилище из витрин со схемой «звезда» с общими измерениями (bus matrix), быстро даёт результат бизнесу и проще для аналитиков. На практике смешивают: детальный слой в 3НФ или Data Vault, витрины по Кимбаллу.
Звезда: таблица фактов и денормализованные измерения вокруг, один JOIN до любого атрибута. Снежинка: измерения нормализованы в несколько таблиц (товар → категория → отдел), меньше избыточности, но больше соединений и сложнее запросы. Для витрин и BI почти всегда звезда; снежинка там, где измерения огромны или часто меняются по иерархии.
Data Vault: хабы (бизнес-ключи), линки (связи между ключами) и сателлиты (атрибуты с историей). Преимущества: гибкость к изменениям источников, параллельная загрузка, полная история, аудируемость, хеш-ключи без обращения к базе. Недостатки: много таблиц и соединений, витрины обязательны, порог входа и дисциплина. Якорная модель идёт дальше: каждый атрибут в отдельной таблице (6НФ), история на уровне атрибута, изменений схемы ещё меньше, но таблиц и соединений ещё больше; практична только с генерацией запросов инструментом.
Факт: составной ключ из внешних ключей на измерения, числовые аддитивные меры (сумма, количество), дата события, мало текстовых колонок, много строк. Измерение: суррогатный первичный ключ, описательные атрибуты (названия, категории, текст), поля истории SCD, относительно мало строк. Большие varchar в фактах раздувают самую большую таблицу, портят сжатие и скорость сканирования, а главное, текст это описание сущности и должен жить в измерении, на которое факт ссылается ключом.
Нормализация делит данные на таблицы так, чтобы каждый факт хранился один раз: заказ ссылается на клиента по ключу, а не копирует его имя и адрес. Убирает аномалии обновления и экономит место, но требует соединений. Денормализация сознательно дублирует данные ради скорости чтения: витрины, таблицы для BI, широкие таблицы в ClickHouse, кэши. Пример нормализованной схемы: customers(id, name), orders(id, customer_id, date), order_items(order_id, product_id, qty), products(id, title, category_id).
Натуральный ключ приходит из предметной области (ИНН, номер договора, email); суррогатный генерируется хранилищем (последовательность, UUID, хеш бизнес-ключа) и не несёт смысла. Суррогатный ключ нужен, чтобы история версий (SCD2) имела уникальный идентификатор строки, чтобы не зависеть от изменения натурального ключа в источнике и чтобы объединять несколько источников с разными ключами. Первичный ключ бывает простым и составным; у составного важны минимальность (нет лишних колонок), стабильность (значения не меняются), и то, что по нему реально ищут и соединяют: ключ из пяти колонок тяжёл в JOIN и индексах.
ORDER BY ts DESC LIMIT 1000 с индексом по ts; без индекса база отсортирует всю таблицу. Если в хранилище таблица партиционирована по дате, добавить фильтр по последним партициям, чтобы не сканировать всё. Для «последних по каждому клиенту» уже нужна оконная функция.
Полуинтервалом: ts >= '2025-04-01' AND ts < '2025-04-02'. Приведение ts::date = '2025-04-01' даёт тот же результат, но отключает индекс и отсечение партиций, а BETWEEN '2025-04-01' AND '2025-04-02' захватывает полночь следующего дня. Часовой пояс уточнить: день в UTC и в Москве это разные интервалы.
В реляционной модели отношение это таблица: заголовок из пар «имя, тип» и тело из множества кортежей. Кортеж это строка, атрибут это колонка. Связи между сущностями: один к одному (человек и паспорт, часто это просто одна таблица, разделённая по соображениям доступа), один ко многим (мать и дети, внешний ключ на стороне «многих»), многие ко многим (студенты и преподаватели), которая в реляционной базе реализуется только через промежуточную таблицу с двумя внешними ключами, образующими составной ключ.
Первая: значения атомарны, нет повторяющихся групп и списков в одной ячейке, строки не дублируются. Вторая: первая плюс каждый неключевой атрибут зависит от всего составного ключа, а не от его части, иначе данные о части ключа выносятся в свою таблицу. Третья: вторая плюс нет транзитивных зависимостей, неключевой атрибут не зависит от другого неключевого (город не хранится вместе с индексом в таблице заказов). Цель нормализации убрать дублирование и аномалии вставки, обновления и удаления. В хранилищах витрины осознанно денормализуют ради скорости чтения, а анкорная модель и Data Vault это примеры сильно нормализованных схем (шестая нормальная форма).
Slowly Changing Dimensions: способы хранить изменения атрибутов измерения. Тип 0: не менять. Тип 1: перезаписать, истории нет. Тип 2: новая строка на каждую версию с valid_from, valid_to, часто is_current и хешем атрибутов; история полная, таблица растёт. Тип 3: предыдущее значение в соседней колонке, история на один шаг. Тип 4: история в отдельной таблице. Тип 6 (1+2+3): текущая версия в каждой строке плюс полная история. В хранилищах стандарт это SCD2 для измерений с историей и SCD1 для справочников, где история не нужна.
Версии не обновляются, а дописываются: каждая новая версия это новая строка с valid_from, а закрытие старой версии делается либо перезаписью партиции целиком из источника, либо движком ReplacingMergeTree по ключу (бизнес-ключ, valid_from) с колонкой версии, либо на чтении: valid_to вычисляется оконной функцией по следующей версии. Запрос «состояние на дату» через argMax по версии не позднее даты или FINAL. Главное решить заранее, какой слой отвечает за закрытие версий: обычно это делает хранилище-источник (Postgres/Greenplum), а ClickHouse получает готовые версии.
Меньше данных через сеть и диск, короче окно загрузки, меньше нагрузка на источник, дешевле повторный запуск. Цена: нужна надёжная метка изменений в источнике, обработка опоздавших и удалённых записей, идемпотентность на стыках окон. Полная перезагрузка остаётся для маленьких справочников и как периодическая сверка.
Единый DAG с задачами на каждую таблицу (генерируемыми из конфига): проще обозримость, общие зависимости и порядок, одна точка настройки расписания; минус: одна длинная задача задерживает остальные, перезапуск одной таблицы требует частичного запуска, DAG с сотнями задач тяжёл для планировщика. Отдельный DAG на таблицу: независимые расписания и ретраи, перезапуск точечный; минус: сотни DAG в интерфейсе, дублирование настроек, зависимости между DAG через сенсоры или datasets. Компромисс: DAG на группу таблиц одного источника с динамической генерацией задач из конфига, тяжёлые таблицы отдельно.
Этапы: инвентаризация схем и объёмов; маппинг типов (NUMBER без точности, DATE с временем, CLOB, часовые пояса); выгрузка полная для первого раза (параллельно по партициям, через PXF или файлы в Parquet) и инкремент по метке изменений или CDC дальше; загрузка в staging Greenplum с правильной дистрибуцией; сверка числа строк и контрольных сумм по дням; переключение потребителей. Несовместимость: PL/SQL-процедуры, функции (NVL, DECODE, CONNECT BY), поведение пустой строки и NULL в Oracle, сортировка и регистр; переписываются на PL/pgSQL и стандартный SQL с тестами на эталонных выборках.
PXF это внешние таблицы Greenplum: сегменты читают HDFS, S3 или другую базу параллельно, напрямую в Greenplum, без промежуточного кластера, и SQL-запрос сразу пишет в целевую таблицу. Это выгоднее Spark, когда нужно просто перелить данные: нет накладных расходов на запуск приложения, нет лишнего копирования, параллелизм на стороне сегментов. Spark нужен, когда до загрузки требуется сложная обработка, которой нет в SQL, или данные нужно отдать не только в Greenplum.
Сессия с таймаутами и повторами на сетевые ошибки и 5xx, пагинация курсором, инкремент по дате изменения с запасом, сырые ответы сохраняются как есть в raw (JSON Lines в S3), разбор вложенных структур на следующем шаге: плоские поля в таблицу, повторяющиеся вложенные списки в дочерние таблицы со ссылкой на родителя, схема зафиксирована моделью. POST встречается для выгрузки отчётов по заказу: создать задание, опрашивать статус, скачать файл.
Проверки в самом пайплайне: тесты dbt (unique, not_null, accepted_values, relationships), Great Expectations или Soda для деклараций ожиданий, собственные SQL-проверки с таблицей результатов. Наблюдаемость: свежесть, объём и схема таблиц отслеживаются по метаданным (Elementary, Monte Carlo, OpenMetadata), алерты в Grafana по метрикам из таблицы результатов проверок. Минимальная рабочая схема без покупных инструментов: таблица dq_results(check, table, dt, failed_rows, threshold), задача Airflow после каждой загрузки и алерт при превышении порога.
Технические метрики: доля NULL в обязательных полях, уникальность ключа, ссылочная целостность, свежесть, число строк относительно вчера и относительно источника. Бизнес-метрики: сумма проводок сходится с источником, нет отрицательных остатков, доля заказов без клиента ниже порога. Метрика это SQL-запрос с порогом и владельцем, результат пишется в таблицу, чтобы видеть динамику. Важно называть конкретику: «сверка суммы выдач по дням с ядром банка, расхождение больше 0,01% блокирует публикацию».
Определить зерно: одна строка на пару «клиент, товар». Источники по SCD2 (клиенты, товары) соединяются по периоду действия версии на дату транзакции, а не просто по ключу, иначе покупки размножатся на все версии. Окно: транзакции за полгода от даты расчёта, дата передаётся параметром. Возраст считается на дату расчёта. Витрина материализуется таблицей с перезаписью целиком раз в неделю (или инкрементом по неделям с пересчётом хвоста), с датой расчёта в колонке. Представление поверх соединений плохо, потому что каждый запрос аналитика будет заново соединять три больших таблицы, а «текущий возраст» и «полгода от сегодня» будут меняться от запроса к запросу, и отчёты перестанут сходиться.
Сначала отобрать клиентов со статусом A или B и хотя бы одной покупкой за три месяца до даты (EXISTS). Затем для покупок за предшествующий месяц соединить транзакции с локациями и регионами по периоду действия локации, посчитать по клиенту общее число покупок и число покупок в нужных регионах (count(*) FILTER (WHERE region_name IN (...))), оставить нужные / всего >= 0.6. Деление делать в numeric, регионы брать из справочника по id, а не по LIKE по названию; даты полуинтервалом.
Raw (копия источника, иммутабельная, с датой загрузки), staging (типизация, дедупликация), core (интегрированная модель с историей), marts (под потребителей), плюс служебные: метаданные загрузок, качество, карантин. Поток: загрузка в raw инкрементом с идемпотентностью, преобразования SQL между слоями оркестрируются по зависимостям, каждый шаг пишет в свой слой и не трогает чужой, витрины пересчитываются от core. Доступ потребителям только к marts и, точечно, к core.
Отталкиваться от вопросов, на которые отвечает дашборд: метрики и разрезы определяют зерно и измерения. Звезда с готовыми агрегатами по самым частым разрезам, денормализованные измерения с понятными названиями, предрасчитанные флаги и категории вместо вычислений в BI, партиции по дате, ключ сортировки под фильтры. Отдельно таблица агрегатов по дням для быстрых графиков и детальная для проваливания. Документация полей и владелец у каждой витрины.
Инвентаризация объектов и потребителей (что реально используется), приоритизация по критичности, маппинг типов и функций, перенос данных слоями с историей, параллельная работа двух систем с ежедневной сверкой, переключение потребителей по одному. Несовместимые функции и процедуры переписываются с тестами на эталонных выборках; то, что не используется, не переносится. Главные риски: неучтённые потребители и расхождения из-за разницы в семантике NULL, дат и сортировки.