
Продолжаю цикл статей про использование Greenmask - инструмента, который написан на Go
специально для безопасной работы с данными PostgreSQL: он помогает делать логические бэкапы, восстанавливать таблицы и при необходимости — анонимизировать чувствительную информацию.
В первой части описаны базовые сценарии использования данного инструмента, а в этой части опишу что такое database subsets
и как использовать данный функционал для радикального снижения размера дампа базы данных.
Официальный сайт: https://greenmask.io
Документация: https://docs.greenmask.io/latest/
GitHub-репозиторий: https://github.com/GreenmaskIO/greenmask (уже 1414 звезд)
Telegram-канал: https://t.me/greenmask_ru
Одна из важнейших функций Greenmask — это возможность создавать подмножества данных (database subsets
). Она позволяет выгружать не всю базу целиком, а только нужную часть данных, по фильтрам и условиям. При этом запросы генерируются так, что дамп таблиц будет консистентным для всех зависимых таблиц включительно.
Это особенно полезно, когда база весит сотни гигабайт, а для задачи тестирования или отладки нужно всего пару таблиц — или только данные, относящиеся к определенной организации, пользователю или периоду времени.
Для примера возьмем следующую схему:

И рассмотрим пару сценариев, с которым сталкиваются почти все разработчики и QA на регулярной основе:
Быстрая подготовка тестовой среды
Представьте, у вас база весом 300 ГБ с данными пользователей за последние 5 лет. Команда тестирования просит обновить тестовый стенд, чтобы проверить новый интерфейс работы со статистикой. Им нужны только последние 10 тысяч пользователей и их активность за последние 30 дней.
В конфиге Greenmask можно указать условие, например:
transformation:
- schema: "public"
name: "clients"
subset_conds:
- 'event_date > now() - interval 30 days'
Тогда для каждой из таблиц будут сгенерированы следующие запросы:
SELECT "public"."clients".* FROM "public"."clients"
WHERE ( ( event_date > now() - interval '30 days' ) )
SELECT "public"."orders".* FROM "public"."orders"
INNER JOIN "public"."clients"
ON "public"."orders"."client_id" = "public"."clients"."id"
AND ( event_date > now() - interval '30 days' )
SELECT "public"."transactions".* FROM "public"."transactions"
INNER JOIN "public"."orders"
ON "public"."transactions"."order_id" = "public"."orders"."id"
INNER JOIN "public"."clients" ON "public"."orders"."client_id" = "public"."clients"."id"
AND ( event_date > now() - interval '30 days' )
В результате утилита создаст дамп только нужной части таблиц которая занимает например 3 ГБ или меньше, как итог создание тестового окружения занимает сильно меньше времени.
Локальная разработка без лишних данных
Допустим, вы разработчик, который работает над новой логикой расчета скидок. Вся логика живёт вокруг таблиц заказов и клиентов, но на проде этих данных — десятки миллионов строк. При этом вам для отладки и локального запуска достаточно одного клиента с его заказами.
В конфиге Greenmask вы просто указываете:
transformation:
- schema: "public"
name: "clients"
subset_conds:
- 'id = 123456'
Запросы также будут собраны для всех таблиц которые участвуют в отношениях (через FK) с необходимыми условиями в WHERE.
Утилита сделает дамп только по этому клиенту. А если вы добавите необходимые трансформации — то сразу и анонимизирует данные (email, имя, телефон и т.д.), чтобы можно было спокойно загрузить дамп базы в S3 для CI или отдать коллеге и при этом без риска утечки персональных данных. Примеры использования трансформаций описаны в первой части.
По факту subset_conds
— не просто фильтр. Это способ контролировать гранулярность данных, которые вы копируете между средами. В этом дампе баз данных содержится только та информацию что вам нужна для целей тестирования или чего-то еще, сами дампы занимают сильно меньше места и с ними проще работать на тестовых серверах. И что еще важно при этом получается целостный логический дамп, у которого удовлетворены ограничения целостности.
Какой еще функционал поддерживает Greenmask и почему это супер крутой инструмент для подготовки дампов баз данных:
Поддержка циклически связанных данных
Во многих БД бывают циклы во внешних ключах — например, таблица employees
(уже не из нашего примера), где каждый сотрудник может быть менеджером другого сотрудника. Такие взаимные связи часто становятся проблемой при создании подмножеств: стандартные инструменты для дампа баз данных не умеют сохранять целостность.
Greenmask умеет корректно работать с такими циклами, и корректно обрабатывает зависимости в обоих направлениях. Вы просто указываете начальное условие (например, id = 1
), и инструмент сам пройдется по связям и вытащит всю нужную структуру и сгенерирует рекурсивные запросы для таблиц которые участвуют в цикле.
Виртуальные связи: когда FK в базе нет, а данные всё равно связаны
Во многих проектах связи между таблицами не оформлены через внешний ключ (особенно если это старый монолит). Но логика связи при этом есть — например, user_id
в таблице audit_logs
связан с users.id
, просто без явного FK. Или итого сложнее, связь происходит через атрибут внутри JSON.
Greenmask позволяет вам вручную описать такие связи и при выгрузке будет учитывать их точно так же, как и настоящие внешние ключи.
Вот как можно описать такую виртуальную связь в конфигурационном файле Greenmask:
virtual_references:
- schema: "public" #
name: "clients" #
references: #
- schema: "public" #
name: "organizations" #
columns: #
- name: "organization_id"
not_null: false #
transformation:
- schema: "public"
name: "clients"
subset_conds:
- 'id IN (1,2,3,4,5,6)'
- schema: "public"
name: "organizations"
subset_conds:
- 'id = 1'
Здесь мы конфигурируем Greenmask следующим образом:
Опиши виртуальный внешний ключ на таблице customers на поле organization_id, который ссылается на таблицу organizations
Примени условие subset таблицах clients и organizations и на все таблицы что ссылаются
Теперь даже если нет никакого внешнего ключа в базе, то считай как будто он есть.
Таким образом, вы можете точно контролировать, какие данные попадут в дамп, даже когда связи оформлены не явно. Более подробно варианты использования виртуальных связей описаны в документации.
Поддержка полиморфных ссылок
Одна из самых сложных структур в базах данных — это полиморфные связи (polymorphic references). Это когда одна колонка может ссылаться на несколько разных таблиц, в зависимости от типа.
Как пример, в домене с таблицами comments, videos и posts полиморфная связь заключается в том, что каждая запись в comments хранит пару полей — commentable_type
(строка с значением 'video' или 'post') и commentable_id
(целое число). Когда commentable_type = 'video'
, то commentable_id
указывает на запись в таблице videos, а когда commentable_type = 'post'
, то на запись в posts. Таким образом одна колонка commentable_id
может "ссылаться" то на видео, то на пост, в зависимости от значения commentable_type
. При выборке данных приложение или SQL-запрос проверяют значение commentable_type
и соединяют comments с нужной таблицей — либо через JOIN videos ON comments.commentable_id = videos.id
, либо через JOIN posts ON comments.commentable_id = posts.id
. Это позволяет хранить комментарии к разным типам объектов в одной таблице без явных внешних ключей.
Детальный пример можно найти в документации, но вот как можно это описать в конфигурации Greenmask:
dump:
virtual_references:
- schema: "public"
name: "comments"
references:
- schema: "public"
name: "videos"
polymorphic_exprs:
- "public.comments.commentable_type = 'video'"
columns:
- name: "commentable_id"
- schema: "public"
name: "posts"
polymorphic_exprs:
- "public.comments.commentable_type = 'post'"
columns:
- name: "commentable_id"
Тогда сгенерированный запрос для таблицы comments будет выглядеть:
SELECT "public"."comments".* FROM "public"."comments"
LEFT JOIN "public"."posts"
ON "public"."comments"."commentable_id" = "public"."posts"."id"
AND (public.posts.id in (1))
AND public.comments.commentable_type = 'post'
LEFT JOIN "public"."videos" ON "public"."comments"."commentable_id" = "public"."videos"."id"
AND public.comments.commentable_type = 'video'
WHERE ((("public"."comments"."commentable_id" IS NULL OR "public"."posts"."id" IS NOT NULL OR
NOT (public.comments.commentable_type = 'post'))))
AND ((("public"."comments"."commentable_id" IS NULL OR "public"."videos"."id" IS NOT NULL OR
NOT (public.comments.commentable_type = 'video'))))
Поддержка виртуальных и полиморфных связей позволяет использовать Greenmask:
в наследованных, несогласованных или legacy-базах;
в проектах с большой долей микросервисов в экосистеме, где не ставят FK между доменами данных
В итоге вы получаете возможность делать точные, изолированные подмножества даже в сложных структурах — и при этом не терять целостность данных.
Как обычно
Для тех кто хочет самостоятельно разобраться в утилите - есть набор Docker образов Greenmask Playground, который позволяет развернуть demo базу postgresql, S3, последнюю версию greenmask в полноценный демо стенд.
Разработчик проекта достаточно активно отвечает в Telegram канале, если будут возникать какие-то вопросы - смело подключайтесь. Так же видно что его усилий не хватает для динамичного развития проекта, если вы хотите принять участие в разработке opensource проекта на Go - будет супер!