Многопользовательский доступ
На платформе аналитики данных SaaS обычно несколько арендаторов, такие как организации, клиенты или бизнес-единицы, используют одну и ту же инфраструктуру базы данных, при этом сохраняется логическое разделение их данных. Это позволяет различным пользователям безопасно получать доступ к своим данным на одной и той же платформе.
В зависимости от требований, существуют различные способы реализации многопользовательского доступа. Ниже представлен гид по их реализации с ClickHouse Cloud.
Общая таблица
В этом подходе данные всех арендаторов хранятся в одной общей таблице, при этом для идентификации данных каждого арендатора используется поле (или набор полей). Чтобы максимизировать производительность, это поле должно быть включено в первичный ключ. Для обеспечения того, чтобы пользователи могли получать доступ только к данным, принадлежащим их арендаторам, используется контроль доступа на основе ролей, реализованный через политики строк.
Мы рекомендуем этот подход, так как он является самым простым в управлении, особенно когда все арендаторы используют одну и ту же схему данных, а объем данных умеренный (< TBs)
Консолидируя все данные арендаторов в одну таблицу, мы улучшаем эффективность хранения за счет оптимизированного сжатия данных и уменьшения накладных расходов на метаданные. Кроме того, обновления схемы упрощаются, так как все данные управляются централизованно.
Этот метод особенно эффективен для работы с большим количеством арендаторов (возможно, миллионов).
Однако альтернативные подходы могут быть более подходящими, если у арендаторов разные схемы данных или ожидается их расхождение с течением времени.
В случаях, когда существует значительный разрыв в объеме данных между арендаторами, меньшие арендаторы могут столкнуться с неоправданными проблемами производительности запросов. Обратите внимание, что эта проблема в значительной степени устраняется за счет включения поля арендатора в первичный ключ.
Пример
Вот пример реализации модели многопользовательского доступа с использованием общей таблицы.
Сначала создадим общую таблицу с полем tenant_id, включенным в первичный ключ.
--- Создание таблицы events. Использование tenant_id как части первичного ключа
CREATE TABLE events
(
tenant_id UInt32, -- Идентификатор арендатора
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (tenant_id, timestamp)
Вставим фейковые данные.
-- Вставка некоторых фиктивных строк
INSERT INTO events (tenant_id, id, type, timestamp, user_id, data)
VALUES
(1, '7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
(1, '846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
(1, '6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
(2, '7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
(2, '6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
(2, '43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
(1, '83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
(1, '975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}'),
(2, 'f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
(2, '5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}'),
Затем создадим двух пользователей user_1 и user_2.
-- Создание пользователей
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
Мы создаем политики строк, которые ограничивают доступ user_1 и user_2 только к данным их арендаторов.
-- Создание политик строк
CREATE ROW POLICY user_filter_1 ON default.events USING tenant_id=1 TO user_1
CREATE ROW POLICY user_filter_2 ON default.events USING tenant_id=2 TO user_2
Затем GRANT SELECT привилегии на общую таблицу с использованием общей роли.
-- Создание роли
CREATE ROLE user_role
-- Предоставление прав на чтение к таблице events.
GRANT SELECT ON default.events TO user_role
GRANT user_role TO user_1
GRANT user_role TO user_2
Теперь вы можете подключиться как user_1 и выполнить простой запрос select. Будут возвращены только строки от первого арендатора.
-- Вход под пользователем user_1
SELECT *
FROM events
┌─tenant_id─┬─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 1 │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 1 │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 1 │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 1 │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 1 │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└───────────┴──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
Отдельные таблицы
В этом подходе данные каждого арендатора хранятся в отдельной таблице в одной базе данных, что устраняет необходимость в конкретном поле для идентификации арендаторов. Доступ пользователей обеспечивается с использованием операторов GRANT, гарантирующих, что каждый пользователь может получать доступ только к таблицам, содержащим данные их арендаторов.
Использование отдельных таблиц - хороший выбор, когда у арендаторов разные схемы данных.
Для сценариев с несколькими арендаторами с очень большими наборами данных, где производительность запросов имеет критическое значение, этот подход может превзойти модель с общей таблицей. Поскольку нет необходимости фильтровать данные других арендаторов, запросы могут быть более эффективными. Кроме того, первичные ключи могут быть дополнительно оптимизированы, так как нет необходимости включать дополнительное поле (например, идентификатор арендатора) в первичный ключ.
Обратите внимание, что этот подход не масштабируется для 1000 арендаторов. См. лимиты использования.
Пример
Вот пример реализации модели многопользовательского доступа с использованием отдельных таблиц.
Сначала создадим две таблицы, одну для событий от tenant_1 и одну для событий от tenant_2.
-- Создание таблицы для арендатора 1
CREATE TABLE events_tenant_1
(
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может сосредоточиться на других атрибутах
-- Создание таблицы для арендатора 2
CREATE TABLE events_tenant_2
(
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может сосредоточиться на других атрибутах
Вставим фейковые данные.
INSERT INTO events_tenant_1 (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')
INSERT INTO events_tenant_2 (id, type, timestamp, user_id, data)
VALUES
('7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
('6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
('43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
('f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
('5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}')
Затем создадим двух пользователей user_1 и user_2.
-- Создание пользователей
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
Затем предоставим GRANT SELECT привилегии на соответствующую таблицу.
-- Предоставление прав на чтение к таблице событий.
GRANT SELECT ON default.events_tenant_1 TO user_1
GRANT SELECT ON default.events_tenant_2 TO user_2
Теперь вы можете подключиться как user_1 и выполнить простой запрос select из таблицы, соответствующей этому пользователю. Будут возвращены только строки от первого арендатора.
-- Вход под пользователем user_1
SELECT *
FROM default.events_tenant_1
┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
Отдельные базы данных
Данные каждого арендатора хранятся в отдельной базе данных в одном и том же сервисе ClickHouse.
Этот подход полезен, если каждый арендатор требует большое количество таблиц и, возможно, материализованных представлений и имеет разные схемы данных. Однако это может стать проблематично, если количество арендаторов велико.
Реализация аналогична подходу с отдельными таблицами, но вместо предоставления привилегий на уровне таблицы привилегии предоставляются на уровне базы данных.
Обратите внимание, что этот подход не масштабируется для 1000 арендаторов. См. лимиты использования.
Пример
Вот пример реализации модели многопользовательского доступа с использованием отдельных баз данных.
Сначала создадим две базы данных, одну для tenant_1 и одну для tenant_2.
-- Создание базы данных для арендатора_1
CREATE DATABASE tenant_1;
-- Создание базы данных для арендатора_2
CREATE DATABASE tenant_2;
-- Создание таблицы для арендатора_1
CREATE TABLE tenant_1.events
(
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (timestamp, user_id);
-- Создание таблицы для арендатора_2
CREATE TABLE tenant_2.events
(
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (timestamp, user_id);
Вставим фейковые данные.
INSERT INTO tenant_1.events (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')
INSERT INTO tenant_2.events (id, type, timestamp, user_id, data)
VALUES
('7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
('6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
('43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
('f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
('5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}')
Затем создадим двух пользователей user_1 и user_2.
-- Создание пользователей
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
Затем предоставим GRANT SELECT привилегии на соответствующую таблицу.
-- Предоставление прав на чтение к таблице событий.
GRANT SELECT ON tenant_1.events TO user_1
GRANT SELECT ON tenant_2.events TO user_2
Теперь вы можете подключиться как user_1 и выполнить простой запрос на таблице событий соответствующей базы данных. Будут возвращены только строки от первого арендатора.
-- Вход под пользователем user_1
SELECT *
FROM tenant_1.events
┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
Разделение вычислений
Три описанных выше подхода также могут быть дополнительно изолированы с использованием Складов данных. Данные хранятся в общем объектном хранилище, но у каждого арендатора может быть свой собственный вычислительный сервис благодаря разделению вычислений с разным соотношением CPU/Память.
Управление пользователями аналогично описанным ранее подходам, так как все сервисы в складе делятся контролем доступа.
Обратите внимание, что количество дочерних сервисов в складе ограничено небольшим числом. См. Ограничения на склады.
Отдельный облачный сервис
Наиболее радикальный подход - использовать отдельный сервис ClickHouse для каждого арендатора.
Этот менее распространенный метод может быть решением, если данные арендаторов должны храниться в разных регионах - по юридическим, Sicherheits- или пространственным причинам.
Необходимо создать учетную запись пользователя на каждом сервисе, где пользователь сможет получить доступ к данным своего арендатора.
Этот подход сложнее в управлении и требует дополнительных ресурсов для каждого сервиса, поскольку каждый из них требует своей собственной инфраструктуры для работы. Услуги могут управляться через ClickHouse Cloud API с возможной оркестрацией также через официальный провайдер Terraform.
Пример
Это пример реализации модели многопользовательского доступа с использованием отдельного сервиса. Обратите внимание, что пример показывает создание таблиц и пользователей на одном сервисе ClickHouse, то же самое должно быть воспроизведено на всех сервисах.
Сначала создадим таблицу events
-- Создание таблицы для арендатора_1
CREATE TABLE events
(
id UUID, -- Уникальный ID события
type LowCardinality(String), -- Тип события
timestamp DateTime, -- Время события
user_id UInt32, -- ID пользователя, который вызвал событие
data String, -- Данные события
)
ORDER BY (timestamp, user_id);
Вставим фейковые данные.
INSERT INTO events (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')
Затем создадим пользователя user_1
-- Создание пользователей
CREATE USER user_1 IDENTIFIED BY '<password>'
Затем предоставим привилегии GRANT SELECT на соответствующую таблицу.
-- Предоставление прав на чтение к таблице событий.
GRANT SELECT ON events TO user_1
Теперь вы можете подключиться как user_1 на сервисе для арендатора 1 и выполнить простой запрос select. Будут возвращены только строки от первого арендатора.
-- Вход под пользователем user_1
SELECT *
FROM events
┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘