Обрезание баз данных без боли — использование и реализация database subsets в Greenmask
- среда, 4 июня 2025 г. в 00:00:09
Продолжаю цикл статей про использование 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
), и инструмент сам пройдется по связям и вытащит всю нужную структуру и сгенерирует рекурсивные запросы для таблиц которые участвуют в цикле.
Во многих проектах связи между таблицами не оформлены через внешний ключ (особенно если это старый монолит). Но логика связи при этом есть — например, 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 - будет супер!