MPP-база на основе PostgreSQL. Мастер (координатор) принимает запросы, хранит каталог и строит план; сегменты хранят данные и выполняют свою часть плана параллельно; интерконнект пересылает строки между сегментами (Motion); зеркала сегментов и резервный мастер дают отказоустойчивость. Таблицы распределены по сегментам по ключу дистрибуции, внутри сегмента могут быть партиционированы.
По хранению: heap (как в PostgreSQL, для таблиц с частыми обновлениями), append-optimized строковые (AO, для больших таблиц с массовой вставкой) и колоночные (AOCO, для широких таблиц аналитики и сжатия). По распределению: distributed by (ключ), distributed randomly (равномерно, без ключа) и replicated (полная копия на каждом сегменте). Replicated для маленьких справочников, которые часто соединяют с большими фактами: соединение становится локальным без Motion. Внешние таблицы (PXF) читают данные из HDFS, S3 и других баз. Сжатие (zlib, zstd) и колоночная ориентация выбираются под профиль чтения.
Горизонтально: добавлением сегмент-хостов с последующим перераспределением данных (gpexpand), что требует окна и времени пропорционально объёму. Вертикально: больше памяти и дисков на хостах. Ограничения: мастер один (резервный только на случай отказа), поэтому точечные запросы и много мелких транзакций упираются в него; ресурсные группы делят CPU и память между пользователями.
У каждого primary-сегмента может быть зеркало (mirror segment) на другом хосте: изменения с primary доезжают на зеркало через WAL-репликацию. Если primary падает, мастер переключает сегмент на зеркало и кластер продолжает работать в деградированном режиме, пока упавший хост не починят и не вернут через gprecoverseg. Мастер тоже дублируется: standby master получает копию каталога и поднимается вручную или скриптом при отказе основного. Без зеркал отказ одного хоста останавливает весь кластер, потому что каждый запрос трогает все сегменты.
Внешняя таблица это описание данных вне базы: файл на диске или на gpfdist-сервере, S3, Hadoop через PXF. Данные не хранятся в Greenplum, читаются при каждом запросе сразу всеми сегментами параллельно, поэтому это основной способ быстрой загрузки (INSERT INTO t SELECT * FROM ext_table). Обычно только чтение, писать можно в writable external table. Foreign table это стандартный для PostgreSQL механизм FDW: подключение к другой базе (PostgreSQL, другой Greenplum) так, будто таблица локальная, с чтением и, в зависимости от обёртки, записью. Разница в назначении: external для файлов и массовой загрузки, foreign для федеративных запросов к другой базе.
Ключ дистрибуции определяет, на какой сегмент попадает строка. Правильный ключ: с высокой кардинальностью и равномерным распределением (нет перекоса), совпадающий с ключом самых частых соединений, чтобы JOIN выполнялся локально без Motion. Плохие кандидаты при неравномерных данных: статус, дата ежедневной загрузки (все строки дня попадут на один сегмент), колонка с большой долей NULL или другим частым значением. Одно поле для дистрибуции и партиционирования использовать можно. Выбор проверяют отдельно для равномерности распределения и отсечения партиций: партиции обычно по дате для отсечения диапазонов, а дистрибуция по ключу сущности для соединений; дата как ключ дистрибуции может дать перекос, если большой объём приходится на несколько дат.
При перекосе часть сегментов хранит и обрабатывает данных больше других: запрос ждёт самый загруженный сегмент, остальные простаивают, память и диск одного узла заканчиваются раньше. Бывает перекос хранения (плохой ключ дистрибуции) и перекос обработки (соединение или группировка по ключу с частыми значениями, например NULL или «не указан»). Проверить: gp_toolkit.gp_skew_coefficients, или SELECT gp_segment_id, count(*) FROM t GROUP BY 1 и сравнить сегменты. Лечение: другой ключ дистрибуции, DISTRIBUTED RANDOMLY для таблиц без естественного ключа соединения, «соль» для частого значения при JOIN, вынос NULL-ключей.
Варианты: сделать маленькую таблицу replicated, тогда соединение локально на каждом сегменте без пересылки; перераспределить одну из таблиц заранее под ключ соединения (CREATE TABLE ... DISTRIBUTED BY или ALTER TABLE SET DISTRIBUTED BY), если это соединение основное; при разовом запросе планировщик сам выберет broadcast маленькой стороны, если оценка объёма мала, иначе redistribute. Если replicated-таблицу не использовать, справочник будет пересылаться при каждом запросе, а при плохой статистике большая таблица может уехать в broadcast и забить память сегментов.
Когда операция (hash join, сортировка, агрегация) не помещается в выделенную память, движок сбрасывает промежуточные данные на диск и обрабатывает частями. Spill позволяет продолжить обработку на диске, но увеличивает I/O и может сильно замедлить запрос. При превышении лимитов временных файлов или нехватке места запрос завершится ошибкой. В плане Greenplum это видно по Work_mem wanted и spill files, в Spark по Spill (Memory/Disk) в UI. Лечение: больше памяти на операцию (ресурсные группы, statement_mem), меньше данных до операции (фильтры, нужные колонки), устранение перекоса.
Это два независимых слоя. Дистрибуция (DISTRIBUTED BY) физическая: строка по хешу ключа попадает на сегмент, партиционированные и обычные таблицы раскладываются по сегментам одинаково. Партиционирование (PARTITION BY) логическое: уже внутри сегмента строка попадает в свою партицию, которая физически отдельная таблица. Документация формулирует это прямо: партиционирование не меняет распределение данных по сегментам. Поэтому и смотреть на них нужно по-разному: ключ дистрибуции отвечает за равномерность и за соединения без перераспределения, партиции за отсечение данных по фильтру (обычно по дате). DISTRIBUTED RANDOMLY даёт равномерность без подходящего ключа, но любой JOIN по такой таблице потребует redistribute motion, поэтому для больших таблиц фактов это запасной вариант.
Партиция это логическое деление таблицы на части по диапазону или списку значений внутри каждого сегмента. Поле выбирают по самому частому фильтру и по единице обслуживания: обычно дата события или загрузки (по месяцам или дням), потому что запросы берут период, а загрузка перезаписывает период целиком. Субпартиции это второй уровень деления внутри партиции (месяц → регион или тип); они оправданы только при большом объёме и реальных фильтрах по второму полю, иначе множат число файлов и замедляют планирование. Партиции не по дате бывают по списку: регион, источник, тип документа, когда запросы и загрузки идут именно в этом разрезе.
Партиция по умолчанию принимает строки, не подходящие ни под одну явную партицию. В неё могут незаметно попасть ошибочные даты или строки будущих периодов, для которых ещё не создали партиции. Отсечение default зависит от условий запроса, границ и выбранного плана; в Greenplum 7 partition pruning может её исключить. При добавлении партиции на новый диапазон нужно вынести из default строки этого диапазона. Без default вставка строки вне известных диапазонов завершится ошибкой: отсутствие подходящей партиции сразу станет видно. Практика: создавать будущие партиции заранее и контролировать размер и содержимое default, если она есть.
EXPLAIN ANALYZE, в плане искать Motion (broadcast/redistribute большой таблицы), перекос между сегментами (max и avg по сегментам в узлах плана), spill на диск, устаревшую статистику (ANALYZE), отсутствие отсечения партиций. Основная проблема в MPP почти всегда движение данных и перекос: один сегмент работает за всех. Дальше: дистрибуция под соединение, replicated-справочники, фильтры до соединения, колоночные таблицы со сжатием для широких сканов, ресурсные группы под память.
Аналитические запросы читают большие диапазоны, где полное сканирование сегментами параллельно быстрее индекса, а индексы замедляют массовую вставку и занимают место. Индексы оправданы для точечных выборок по высокоселективному условию и для таблиц с частыми обновлениями (heap). Вместо индексов роль играют дистрибуция, партиции, колоночное хранение и сжатие. Bitmap-индексы для колонок с малым числом значений встречаются, B-tree редко.
Блокировки согласуют доступ транзакций к таблицам. Длинный SELECT может задержать TRUNCATE и DDL, которым нужна блокировка ACCESS EXCLUSIVE; у разных форм ALTER TABLE требования отличаются. AO-таблицы поддерживают UPDATE и DELETE, но режим блокировок и совместимость с параллельной загрузкой проверяют для конкретной версии Greenplum. Сначала смотрят pg_locks, сессии и цепочку ожидания. Для массовой замены периода можно подготовить новую таблицу или партицию, проверить данные и выбрать способ переключения с учётом зависимостей и блокировок. В Greenplum 7 ATTACH PARTITION и legacy EXCHANGE PARTITION требуют разных блокировок. Небольшое обновление и пересборка периода имеют разную стоимость; универсального запрета на UPDATE в AO нет.
Массовый UPDATE/DELETE на AO-таблицах порождает скрытые строки и требует VACUUM, а на больших таблицах занимает часы и держит блокировки. Поэтому обновления делают заменой: пересобрать партицию или таблицу запросом (CREATE TABLE AS ... DISTRIBUTED BY), затем ALTER TABLE ... EXCHANGE PARTITION или переименование в одной транзакции. Удаление периода это DROP PARTITION. Для SCD2 закрытие версий это редкий точечный UPDATE на heap-таблице или перезапись сателлита.
Heap-таблица живёт по правилам PostgreSQL: UPDATE пишет новую версию строки, старая остаётся мёртвой до VACUUM, таблица «раздувается». AO-таблица никогда не перезаписывает данные, а только дописывает блоки, поэтому её можно сжимать (zlib, zstd, RLE) и хранить поколоночно. UPDATE и DELETE в ней не трогают строки, а помечают их в отдельной битовой карте видимости; каждая такая операция раздувает карту, а VACUUM должен перестроить сжатые блоки и карту целиком, что заметно тяжелее heap. Одиночные INSERT тоже вредны: каждый создаёт накладные структуры, AO рассчитана на пачки. Отсюда правило: справочники и часто меняющиеся таблицы в heap, большие таблицы фактов с пакетной загрузкой в AO (чаще column-oriented), а изменения в них делать перезаливкой партиции.
Как в PostgreSQL: B-tree по умолчанию, bitmap для колонок с умеренной кардинальностью (сотни и тысячи значений, хорошо сочетается с AND/OR по нескольким колонкам), GIN и GiST для полнотекстового поиска и геоданных, BRIN для больших таблиц, отсортированных по времени. Особенность MPP: уникальный индекс и первичный ключ возможны только если колонки индекса включают ключ дистрибуции, иначе сегмент не может проверить уникальность, потому что дубликат может лежать на другом сегменте. Уникальность в AO-таблицах не поддерживается вовсе. На аналитической нагрузке индексы используют редко: план и так читает большие диапазоны, а индекс замедляет загрузку и занимает место.