> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Мультиарендность

> Рекомендации по реализации мультиарендности

На SaaS-платформах аналитики данных несколько тенантов, например организации, клиенты или бизнес-подразделения, обычно используют общую инфраструктуру базы данных, сохраняя при этом логическое разделение данных. Это позволяет разным пользователям безопасно получать доступ к своим данным в рамках одной платформы.

В зависимости от требований мультиарендность можно реализовать по-разному. Ниже приведено руководство по реализации этих подходов в ClickHouse Cloud.

<div id="shared-table">
  ## Общая таблица
</div>

При таком подходе данные всех тенантов хранятся в одной общей таблице, а для идентификации данных каждого тенанта используется поле (или набор полей). Чтобы добиться максимальной производительности, это поле должно входить в [первичный ключ](/docs/ru/reference/statements/create/table#primary-key). Чтобы пользователи могли получать доступ только к данным своих тенантов, используется [ролевое управление доступом](/docs/ru/concepts/features/security/access-rights), реализованное с помощью [политик на уровне строк](/docs/ru/concepts/features/security/access-rights#row-policy-management).

> **Мы рекомендуем этот подход, так как им проще всего управлять, особенно если у всех тенантов одинаковая схема данных, а объёмы данных умеренные (\< ТБ)**

Объединение данных всех тенантов в одной таблице повышает эффективность хранения за счёт оптимизированного сжатия данных и снижения накладных расходов на метаданные. Кроме того, упрощается обновление схемы, поскольку все данные управляются централизованно.

Этот метод особенно эффективен при работе с большим числом тенантов (вплоть до миллионов).

Однако в некоторых случаях могут лучше подойти альтернативные подходы — например, если у тенантов разные схемы данных или предполагается, что со временем различия между ними будут увеличиваться.

Если объёмы данных у тенантов сильно различаются, у небольших тенантов может без необходимости снижаться производительность запросов. Обратите внимание: эта проблема в значительной степени решается, если включить поле тенанта в первичный ключ.

<div id="shared-table-example">
  ### Пример
</div>

Это пример реализации модели мультиарендности с общей таблицей.

Сначала создадим общую таблицу, где поле `tenant_id` входит в состав первичного ключа.

```sql theme={null}
--- Создание таблицы events. tenant_id используется как часть первичного ключа
CREATE TABLE events
(
    tenant_id UInt32,                 -- Идентификатор тенанта
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (tenant_id, timestamp)
```

Давайте вставим тестовые данные.

```sql theme={null}
-- Вставка тестовых строк
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`.

```sql theme={null}
-- Создать пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
```

Мы [создаем политики на уровне строк](/docs/ru/reference/statements/create/row-policy), которые позволяют `user_1` и `user_2` получать доступ только к данным своих тенантов.

```sql theme={null}
-- Создать политики на уровне строк
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`](/docs/ru/reference/statements/grant#usage) для общей таблицы с помощью общей роли.

```sql theme={null}
-- Создать роль
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-запрос. Будут возвращены только строки первого тенанта.

```sql theme={null}
-- Выполняется от имени user_1
SELECT *
FROM events
```

```response theme={null}
   ┌─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"} │
   └───────────┴──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
```

<div id="separate-tables">
  ## Отдельные таблицы
</div>

При таком подходе данные каждого тенанта хранятся в отдельной таблице в одной и той же базе данных, поэтому не требуется специальное поле для идентификации тенанта. Доступ пользователей настраивается с помощью [оператора GRANT](/docs/ru/reference/statements/grant), так что каждый пользователь может обращаться только к таблицам с данными своих тенантов.

> **Использование отдельных таблиц — хороший выбор, если у тенантов разные схемы данных.**

В сценариях с небольшим числом тенантов и очень большими наборами данных, где критична производительность запросов, этот подход может быть эффективнее модели с общей таблицей. Поскольку не нужно отфильтровывать данные других тенантов, запросы могут выполняться быстрее. Кроме того, первичные ключи можно дополнительно оптимизировать, так как в первичный ключ не нужно включать дополнительное поле (например, идентификатор тенанта).

Обратите внимание: этот подход не масштабируется на тысячи тенантов. См. [ограничения использования](/docs/ru/products/cloud/guides/best-practices/usagelimits).

<div id="shared-table-example">
  ### Пример
</div>

Это пример реализации модели мультиарендности с отдельными таблицами.

Сначала создадим две таблицы: одну для событий из `tenant_1` и одну для событий из `tenant_2`.

```sql theme={null}
-- Создать таблицу для тенанта 1 
CREATE TABLE events_tenant_1
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может быть ориентирован на другие атрибуты

-- Создать таблицу для тенанта 2 
CREATE TABLE events_tenant_2
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может быть ориентирован на другие атрибуты
```

Давайте вставим тестовые данные.

```sql theme={null}
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`.

```sql theme={null}
-- Создание пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
```

Затем выдайте привилегии `GRANT SELECT` для соответствующей таблицы.

```sql theme={null}
-- Предоставить права только на чтение для таблицы events.
GRANT SELECT ON default.events_tenant_1 TO user_1
GRANT SELECT ON default.events_tenant_2 TO user_2
```

Теперь можно подключиться как `user_1` и выполнить простой SELECT-запрос к таблице, соответствующей этому пользователю. В результате будут возвращены только строки первого тенанта.

```sql theme={null}
-- Выполнено от имени user_1
SELECT *
FROM default.events_tenant_1
```

```response theme={null}
   ┌─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"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
```

<div id="separate-databases">
  ## Отдельные базы данных
</div>

Данные каждого тенанта хранятся в отдельной базе данных в рамках одного сервиса ClickHouse.

> **Этот подход полезен, если каждому тенанту требуется большое количество таблиц и, возможно, materialized views, а также если схемы данных у тенантов различаются. Однако при большом числе тенантов управлять этим становится сложно.**

Реализация похожа на подход с отдельными таблицами, но вместо предоставления привилегий на уровне таблицы привилегии предоставляются на уровне базы данных.

Обратите внимание: этот подход не масштабируется на тысячи тенантов. См. [ограничения использования](/docs/ru/products/cloud/guides/best-practices/usagelimits).

<div id="shared-table-example">
  ### Пример
</div>

Это пример реализации модели мультиарендности с использованием отдельных баз данных.

Сначала создадим две базы данных: одну для `tenant_1` и одну для `tenant_2`.

```sql theme={null}
-- Создать базу данных для tenant_1
CREATE DATABASE tenant_1;

-- Создать базу данных для tenant_2
CREATE DATABASE tenant_2;
```

```sql theme={null}
-- Создать таблицу для tenant_1
CREATE TABLE tenant_1.events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);

-- Создать таблицу для tenant_2
CREATE TABLE tenant_2.events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);
```

Давайте вставим тестовые данные.

```sql theme={null}
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`.

```sql theme={null}
-- Создать пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'
```

Затем выдайте привилегию `GRANT SELECT` на соответствующую таблицу.

```sql theme={null}
-- Предоставить доступ только на чтение к таблице events.
GRANT SELECT ON tenant_1.events TO user_1
GRANT SELECT ON tenant_2.events TO user_2
```

Теперь вы можете подключиться как `user_1` и выполнить простой запрос SELECT к таблице events в нужной базе данных. Будут возвращены только строки первого тенанта.

```sql theme={null}
-- Выполнено под пользователем user_1
SELECT *
FROM tenant_1.events
```

```response theme={null}
   ┌─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"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
```

<div id="compute-compute-separation">
  ## Разделение вычислительных ресурсов
</div>

Три описанных выше подхода также можно дополнительно изолировать с помощью [хранилищ](/docs/ru/products/cloud/features/infrastructure/warehouses#what-is-a-warehouse). Данные совместно используют общее Объектное хранилище, но благодаря [разделению вычислительных ресурсов](/docs/ru/products/cloud/features/infrastructure/warehouses#what-is-compute-compute-separation) с различным соотношением CPU/Memory у каждого тенанта может быть собственный вычислительный сервис.

Управление пользователями аналогично описанным ранее подходам, поскольку все сервисы в хранилище [используют общее управление доступом](/docs/ru/products/cloud/features/infrastructure/warehouses#database-credentials).

Обратите внимание, что число дочерних сервисов в хранилище ограничено. См. [Ограничения хранилища](/docs/ru/products/cloud/features/infrastructure/warehouses#limitations).

<div id="separate-service">
  ## Отдельный сервис ClickHouse Cloud
</div>

Самый радикальный подход — использовать отдельный сервис ClickHouse для каждого тенанта.

> **Этот менее распространённый метод может подойти, если данные тенантов должны храниться в разных регионах — по юридическим причинам, из соображений безопасности или географической близости.**

В каждом сервисе, к которому пользователь должен иметь доступ к данным соответствующего тенанта, необходимо создать учётную запись пользователя.

Этим подходом сложнее управлять, и с каждым новым сервисом растут накладные расходы, поскольку для работы каждого из них требуется собственная инфраструктура. Сервисами можно управлять через [ClickHouse Cloud API](/docs/ru/products/cloud/features/admin-features/api/api-overview); оркестрация также возможна с помощью [официального Terraform-провайдера](https://registry.terraform.io/providers/ClickHouse/clickhouse/latest/docs).

<div id="shared-table-example">
  ### Пример
</div>

Это пример реализации модели мультиарендности с отдельными сервисами. Обратите внимание: в примере показано создание таблиц и пользователей в одном сервисе ClickHouse; то же самое потребуется повторить во всех сервисах.

Сначала создадим таблицу `events`

```sql theme={null}
-- Создать таблицу для tenant_1
CREATE TABLE events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- Идентификатор пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);
```

Давайте вставим тестовые данные.

```sql theme={null}
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`

```sql theme={null}
-- Создание пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
```

Затем выдайте привилегию `GRANT SELECT` на соответствующую таблицу.

```sql theme={null}
-- Предоставить доступ только на чтение к таблице events.
GRANT SELECT ON events TO user_1
```

Теперь вы можете подключиться к сервису для тенанта 1 под именем `user_1` и выполнить простой запрос SELECT. Будут возвращены только строки первого тенанта.

```sql theme={null}
-- Подключено как user_1
SELECT *
FROM events
```

```response theme={null}
   ┌─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"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘
```
