> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-fbfa8bee.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Проектирование схемы

> Оптимизация схемы ClickHouse для повышения производительности запросов

export const Image = ({img, alt, size}) => {
  return <Frame>
      <img src={img} alt={alt} />
    </Frame>;
};

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

<div id="stack-overflow-dataset">
  ## Набор данных Stack Overflow
</div>

В примерах этого руководства мы используем подмножество набора данных Stack Overflow. Оно включает все посты, голоса, пользователей, комментарии и значки, появившиеся на Stack Overflow с 2008 года по апрель 2024 года. Эти данные доступны в формате Parquet по схемам ниже в S3 бакете `s3://datasets-documentation/stackoverflow/parquet/`:

> Указанные первичные ключи и связи не обеспечиваются ограничениями (Parquet — это файловый, а не табличный формат) и лишь показывают, как данные связаны между собой и какими уникальными ключами они обладают.

<Image img="https://mintcdn.com/private-7c7dfe99-mintlify-fbfa8bee/qvDh3oxLPI7c74wx/images/data-modeling/stackoverflow-schema.png?fit=max&auto=format&n=qvDh3oxLPI7c74wx&q=85&s=7bcc5cbfa8fa9456c6bb9a0b285b6141" size="lg" alt="Схема Stack Overflow" width="1800" height="1128" data-path="images/data-modeling/stackoverflow-schema.png" />

<br />

Набор данных Stack Overflow содержит несколько связанных таблиц. В любой задаче моделирования данных мы рекомендуем пользователям сначала сосредоточиться на загрузке основной таблицы. Это не обязательно самая большая таблица, а скорее та, по которой, как вы ожидаете, будет выполняться большинство аналитических запросов. Это поможет вам познакомиться с основными концепциями и типами ClickHouse, что особенно важно, если вы преимущественно работали с OLTP-системами. По мере добавления новых таблиц эту таблицу, возможно, потребуется переработать, чтобы в полной мере использовать возможности ClickHouse и добиться оптимальной производительности.

Приведенная выше схема намеренно не оптимизирована для целей этого руководства.

<div id="establish-initial-schema">
  ## Создание начальной схемы
</div>

Поскольку таблица `posts` станет основной для большинства аналитических запросов, мы сосредоточимся на создании схемы именно для неё. Эти данные доступны в публичном S3 бакете `s3://datasets-documentation/stackoverflow/parquet/posts/*.parquet`, где каждому году соответствует отдельный файл.

> Загрузка данных из S3 в формате Parquet — наиболее распространённый и предпочтительный способ загрузки данных в ClickHouse. ClickHouse оптимизирован для обработки Parquet и может читать и вставлять из S3 десятки миллионов строк в секунду.

ClickHouse предоставляет возможность автоматического определения схемы, чтобы выявлять типы данных в наборе данных. Это поддерживается для всех форматов данных, включая Parquet. Мы можем воспользоваться этой возможностью, чтобы определить типы ClickHouse для данных с помощью табличной функции s3 и команды [`DESCRIBE`](/ru/reference/statements/describe-table). Обратите внимание, что ниже мы используем глоб-шаблон `*.parquet`, чтобы прочитать все файлы в папке `stackoverflow/parquet/posts`.

```sql theme={null}
DESCRIBE TABLE s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
SETTINGS describe_compact_output = 1
```

```response theme={null}
┌─name──────────────────┬─type───────────────────────────┐
│ Id                    │ Nullable(Int64)               │
│ PostTypeId            │ Nullable(Int64)               │
│ AcceptedAnswerId      │ Nullable(Int64)               │
│ CreationDate          │ Nullable(DateTime64(3, 'UTC')) │
│ Score                 │ Nullable(Int64)               │
│ ViewCount             │ Nullable(Int64)               │
│ Body                  │ Nullable(String)              │
│ OwnerUserId           │ Nullable(Int64)               │
│ OwnerDisplayName      │ Nullable(String)              │
│ LastEditorUserId      │ Nullable(Int64)               │
│ LastEditorDisplayName │ Nullable(String)              │
│ LastEditDate          │ Nullable(DateTime64(3, 'UTC')) │
│ LastActivityDate      │ Nullable(DateTime64(3, 'UTC')) │
│ Title                 │ Nullable(String)              │
│ Tags                  │ Nullable(String)              │
│ AnswerCount           │ Nullable(Int64)               │
│ CommentCount          │ Nullable(Int64)               │
│ FavoriteCount         │ Nullable(Int64)               │
│ ContentLicense        │ Nullable(String)              │
│ ParentId              │ Nullable(String)              │
│ CommunityOwnedDate    │ Nullable(DateTime64(3, 'UTC')) │
│ ClosedDate            │ Nullable(DateTime64(3, 'UTC')) │
└───────────────────────┴────────────────────────────────┘
```

> [Табличная функция S3](/ru/reference/functions/table-functions/s3) позволяет выполнять запросы к данным в S3 напрямую из ClickHouse. Эта функция совместима со всеми файловыми форматами, которые поддерживает ClickHouse.

В результате мы получаем исходную неоптимизированную схему. По умолчанию ClickHouse преобразует их в эквивалентные типы Nullable. Мы можем создать таблицу ClickHouse с этими типами с помощью простой команды `CREATE EMPTY AS SELECT`.

```sql theme={null}
CREATE TABLE posts
ENGINE = MergeTree
ORDER BY () EMPTY AS
SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
```

Несколько важных моментов:

Наша таблица posts пуста после выполнения этой команды. Данные ещё не загружены.
В качестве движка таблицы мы указали MergeTree. MergeTree — самый распространённый движок таблиц ClickHouse, который вы, скорее всего, будете использовать. Это универсальный инструмент в вашем арсенале ClickHouse: он способен работать с петабайтами данных и подходит для большинства аналитических сценариев. Существуют и другие движки таблиц — например, для сценариев CDC, где требуется поддержка эффективных обновлений.

Выражение `ORDER BY ()` означает, что у нас нет индекса, а точнее — никакого порядка в данных. Подробнее об этом позже. Пока достаточно знать, что все запросы будут требовать линейного сканирования.

Чтобы убедиться, что таблица создана:

```sql theme={null}
SHOW CREATE TABLE posts

CREATE TABLE posts
(
        `Id` Nullable(Int64),
        `PostTypeId` Nullable(Int64),
        `AcceptedAnswerId` Nullable(Int64),
        `CreationDate` Nullable(DateTime64(3, 'UTC')),
        `Score` Nullable(Int64),
        `ViewCount` Nullable(Int64),
        `Body` Nullable(String),
        `OwnerUserId` Nullable(Int64),
        `OwnerDisplayName` Nullable(String),
        `LastEditorUserId` Nullable(Int64),
        `LastEditorDisplayName` Nullable(String),
        `LastEditDate` Nullable(DateTime64(3, 'UTC')),
        `LastActivityDate` Nullable(DateTime64(3, 'UTC')),
        `Title` Nullable(String),
        `Tags` Nullable(String),
        `AnswerCount` Nullable(Int64),
        `CommentCount` Nullable(Int64),
        `FavoriteCount` Nullable(Int64),
        `ContentLicense` Nullable(String),
        `ParentId` Nullable(String),
        `CommunityOwnedDate` Nullable(DateTime64(3, 'UTC')),
        `ClosedDate` Nullable(DateTime64(3, 'UTC'))
)
ENGINE = MergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
ORDER BY tuple()
```

После определения исходной схемы мы можем загрузить данные с помощью `INSERT INTO SELECT`, читая их через табличную функцию S3. Ниже показано, как загрузить данные `posts` примерно за 2 минуты на 8-ядерном экземпляре ClickHouse Cloud.

```sql theme={null}
INSERT INTO posts SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
```

```response theme={null}
0 rows in set. Elapsed: 148.140 sec. Processed 59.82 million rows, 38.07 GB (403.80 thousand rows/s., 257.00 MB/s.)
```

> Приведенный выше запрос загружает 60 млн строк. Хотя для ClickHouse это немного, пользователи с более медленным интернет-соединением могут захотеть загрузить только часть данных. Для этого достаточно указать годы, которые нужно загрузить, с помощью глоб-шаблона, например `https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/2008.parquet` или `https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/{2008, 2009}.parquet`. Подробнее о том, как использовать глоб-шаблоны для выбора подмножеств файлов, см. [здесь](/ru/reference/functions/table-functions/file#globs-in-path).

<div id="optimizing-types">
  ## Оптимизация типов
</div>

Один из секретов высокой производительности запросов к ClickHouse — сжатие.

Чем меньше данных на диске, тем меньше I/O и тем быстрее выполняются запросы и вставка. Накладные расходы любого алгоритма сжатия на CPU в большинстве случаев с избытком компенсируются снижением объема I/O. Поэтому при работе над ускорением запросов к ClickHouse в первую очередь стоит улучшать сжатие данных.

> Если вам интересно, почему ClickHouse так хорошо сжимает данные, рекомендуем [эту статью](https://clickhouse.com/blog/optimize-clickhouse-codecs-compression-schema). Кратко: ClickHouse как столбцовая база данных записывает значения по столбцам. Если эти значения отсортированы, одинаковые значения оказываются рядом друг с другом. Алгоритмы сжатия эффективно используют такие непрерывные шаблоны в данных. Кроме того, в ClickHouse есть кодеки и типы данных разной точности, которые позволяют дополнительно настраивать методы сжатия.

На сжатие в ClickHouse влияют 3 основных фактора: ключ сортировки, типы данных и используемые кодеки. Все это настраивается через схему.

Наибольшего начального прироста в сжатии и производительности запросов можно добиться простым процессом оптимизации типов. Для оптимизации схемы можно применить несколько простых правил:

* **Используйте строгие типы** - В нашей исходной схеме для многих столбцов, которые явно являются числовыми, использовался тип String. Использование корректных типов обеспечит ожидаемую семантику при фильтрации и агрегации. То же самое относится и к типам дат, которые уже корректно заданы в файлах Parquet.
* **Избегайте столбцов с типом Nullable** - По умолчанию для приведенных выше столбцов предполагалось значение NULL. Тип Nullable позволяет запросам различать пустое значение и NULL. Для этого создается отдельный столбец типа UInt8. Этот дополнительный столбец приходится обрабатывать каждый раз, когда пользователь работает со столбцом с типом Nullable. Это приводит к дополнительным затратам места в хранилище и почти всегда негативно влияет на производительность запросов. Используйте Nullable только в том случае, если между пустым значением по умолчанию для типа и NULL действительно есть разница. Например, значение 0 для пустых значений в столбце `ViewCount` скорее всего будет достаточным для большинства запросов и не повлияет на результаты. Если пустые значения нужно обрабатывать иначе, их часто можно исключить из запроса с помощью фильтра.
* **Используйте минимально необходимую точность для числовых типов** - В ClickHouse есть множество числовых типов, рассчитанных на разные диапазоны значений и точность. Всегда старайтесь минимизировать количество бит, используемых для представления столбца. Помимо целочисленных типов разного размера, например Int16, ClickHouse предлагает беззнаковые варианты, у которых минимальное значение равно 0. Это может позволить использовать для столбца меньше бит: например, UInt16 имеет максимальное значение 65535, что вдвое больше, чем у Int16. По возможности предпочитайте эти типы более крупным знаковым вариантам.
* **Минимальная точность для типов даты** - ClickHouse поддерживает несколько типов для даты и дата-времени. Date и Date32 можно использовать для хранения только даты, при этом второй поддерживает больший диапазон дат ценой использования большего числа бит. DateTime и DateTime64 поддерживают значения дата-время. DateTime ограничен точностью до секунды и использует 32 бита. DateTime64, как следует из названия, использует 64 бита, но поддерживает точность вплоть до наносекунд. Как и всегда, выбирайте наиболее грубый вариант, приемлемый для запросов, чтобы минимизировать необходимое количество бит.
* **Используйте LowCardinality** - Числа, строки, а также столбцы Date или DateTime с небольшим количеством уникальных значений потенциально можно кодировать с помощью типа LowCardinality. Этот словарь кодирует значения, уменьшая размер данных на диске. Рассмотрите этот вариант для столбцов с менее чем 10 тыс. уникальных значений.
* **FixedString для особых случаев** - Строки фиксированной длины можно кодировать с помощью типа FixedString, например коды языков и валют. Это эффективно, когда данные имеют длину ровно N байт. Во всех остальных случаях это, скорее всего, снизит эффективность, и предпочтительнее использовать LowCardinality.
* **Enum для валидации данных** - Тип Enum можно использовать для эффективного кодирования перечислимых типов. Enum может занимать 8 или 16 бит в зависимости от количества уникальных значений, которые нужно хранить. Рассмотрите этот вариант, если вам нужна связанная с ним валидация во время вставки (необъявленные значения будут отклонены) или если вы хотите выполнять запросы, использующие естественный порядок значений Enum, например представьте столбец с отзывами, содержащий ответы пользователей `Enum(':(' = 1, ':|' = 2, ':)' = 3)`.

> Совет: Чтобы определить диапазон значений для всех столбцов и количество различных значений, можно использовать простой запрос `SELECT * APPLY min, * APPLY  max, * APPLY uniq FROM table FORMAT Vertical`. Рекомендуем выполнять его на меньшем подмножестве данных, так как это может быть затратно. Для точного результата этот запрос требует, чтобы числовые значения были определены именно как числовые типы, а не как String.

Применив эти простые правила к нашей таблице posts, мы можем определить оптимальный тип для каждого столбца:

| Столбец                 | Числовой | Мин., макс.                                                  | Уникальные значения | NULL | Комментарий                                                                                                                               | Оптимизированный тип                                                                                                                                         |
| ----------------------- | -------- | ------------------------------------------------------------ | ------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PostTypeId`            | Да       | 1, 8                                                         | 8                   | Нет  |                                                                                                                                           | `Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8)` |
| `AcceptedAnswerId`      | Да       | 0, 78285170                                                  | 12282094            | Да   | Различать NULL и значение 0                                                                                                               | UInt32                                                                                                                                                       |
| `CreationDate`          | Нет      | 2008-07-31 21:42:52.667000000, 2024-03-31 23:59:17.697000000 | \*                  | Нет  | Точность до миллисекунд не требуется, используйте DateTime                                                                                | DateTime                                                                                                                                                     |
| `Score`                 | Да       | -217, 34970                                                  | 3236                | Нет  |                                                                                                                                           | Int32                                                                                                                                                        |
| `ViewCount`             | Да       | 2, 13962748                                                  | 170867              | Нет  |                                                                                                                                           | UInt32                                                                                                                                                       |
| `Body`                  | Нет      | -                                                            | \*                  | Нет  |                                                                                                                                           | String                                                                                                                                                       |
| `OwnerUserId`           | Да       | -1, 4056915                                                  | 6256237             | Да   |                                                                                                                                           | Int32                                                                                                                                                        |
| `OwnerDisplayName`      | Нет      | -                                                            | 181251              | Да   | Считать NULL пустой строкой                                                                                                               | String                                                                                                                                                       |
| `LastEditorUserId`      | Да       | -1, 9999993                                                  | 1104694             | Да   | 0 — неиспользуемое значение, его можно использовать для NULL                                                                              | Int32                                                                                                                                                        |
| `LastEditorDisplayName` | Нет      | \*                                                           | 70952               | Да   | Считать NULL пустой строкой. LowCardinality протестирован, пользы нет                                                                     | String                                                                                                                                                       |
| `LastEditDate`          | Нет      | 2008-08-01 13:24:35.051000000, 2024-04-06 21:01:22.697000000 | -                   | Нет  | Миллисекундная точность не требуется, используйте DateTime                                                                                | DateTime                                                                                                                                                     |
| `LastActivityDate`      | Нет      | 2008-08-01 12:19:17.417000000, 2024-04-06 21:01:22.697000000 | \*                  | Нет  | Точность до миллисекунд не требуется, используйте DateTime                                                                                | DateTime                                                                                                                                                     |
| `Title`                 | Нет      | -                                                            | \*                  | Нет  | Считать NULL пустой строкой                                                                                                               | String                                                                                                                                                       |
| `Tags`                  | Нет      | -                                                            | \*                  | Нет  | Считать NULL пустой строкой                                                                                                               | String                                                                                                                                                       |
| `AnswerCount`           | Да       | 0, 518                                                       | 216                 | Нет  | Считать NULL и 0 равнозначными                                                                                                            | UInt16                                                                                                                                                       |
| `CommentCount`          | Да       | 0, 135                                                       | 100                 | Нет  | Считать NULL и 0 равнозначными                                                                                                            | UInt8                                                                                                                                                        |
| `FavoriteCount`         | Да       | 0, 225                                                       | 6                   | Да   | Считать NULL и 0 равнозначными                                                                                                            | UInt8                                                                                                                                                        |
| `ContentLicense`        | Нет      | -                                                            | 3                   | Нет  | LowCardinality превосходит FixedString по производительности                                                                              | LowCardinality(String)                                                                                                                                       |
| `ParentId`              | Нет      | \*                                                           | 20696028            | Да   | Считать NULL пустой строкой                                                                                                               | String                                                                                                                                                       |
| `CommunityOwnedDate`    | Нет      | 2008-08-12 04:59:35.017000000, 2024-04-01 05:36:41.380000000 | -                   | Да   | Рекомендуется использовать значение по умолчанию 1970-01-01 для значений NULL. Миллисекундная точность не требуется, используйте DateTime | DateTime                                                                                                                                                     |
| `ClosedDate`            | Нет      | 2008-09-04 20:56:44, 2024-04-06 18:49:25.393000000           | \*                  | Да   | Для NULL рассмотрите значение по умолчанию 1970-01-01. Миллисекундная точность не требуется, используйте DateTime                         | DateTime                                                                                                                                                     |

<br />

Получаем следующую схему:

```sql theme={null}
CREATE TABLE posts_v2
(
   `Id` Int32,
   `PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
   `AcceptedAnswerId` UInt32,
   `CreationDate` DateTime,
   `Score` Int32,
   `ViewCount` UInt32,
   `Body` String,
   `OwnerUserId` Int32,
   `OwnerDisplayName` String,
   `LastEditorUserId` Int32,
   `LastEditorDisplayName` String,
   `LastEditDate` DateTime,
   `LastActivityDate` DateTime,
   `Title` String,
   `Tags` String,
   `AnswerCount` UInt16,
   `CommentCount` UInt8,
   `FavoriteCount` UInt8,
   `ContentLicense`LowCardinality(String),
   `ParentId` String,
   `CommunityOwnedDate` DateTime,
   `ClosedDate` DateTime
)
ENGINE = MergeTree
ORDER BY tuple()
COMMENT 'Optimized types'
```

Мы можем заполнить её с помощью простого `INSERT INTO SELECT`, прочитав данные из предыдущей таблицы и вставив их в эту:

```sql theme={null}
INSERT INTO posts_v2 SELECT * FROM posts
```

```response theme={null}
0 rows in set. Elapsed: 146.471 sec. Processed 59.82 million rows, 83.82 GB (408.40 thousand rows/s., 572.25 MB/s.)
```

В новой схеме мы не храним значения NULL. Приведённая выше вставка неявно преобразует их в значения по умолчанию для соответствующих типов — 0 для целых чисел и пустое значение для строк. ClickHouse также автоматически приводит любые числовые значения к нужной точности.
Первичные (ключи сортировки) в ClickHouse
Пользователи, переходящие с OLTP-баз данных, часто ищут в ClickHouse аналогичное понятие.

<div id="choosing-an-ordering-key">
  ## Выбор ключа сортировки
</div>

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

<Image img="https://mintcdn.com/private-7c7dfe99-mintlify-fbfa8bee/qvDh3oxLPI7c74wx/images/data-modeling/schema-design-indices.png?fit=max&auto=format&n=qvDh3oxLPI7c74wx&q=85&s=591707cd3f25fb9a40cc6b4350fad015" size="md" alt="Разреженная индексация в ClickHouse" width="1600" height="972" data-path="images/data-modeling/schema-design-indices.png" />

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

> Все столбцы в таблице будут отсортированы по значению указанного ключа сортировки, независимо от того, входят ли они в сам ключ. Например, если в качестве ключа используется `CreationDate`, порядок значений во всех остальных столбцах будет соответствовать порядку значений в столбце `CreationDate`. Можно указать несколько ключей сортировки — в этом случае сортировка будет выполняться с той же семантикой, что и секция `ORDER BY` в запросе `SELECT`.

При выборе ключа сортировки можно руководствоваться несколькими простыми правилами. Иногда они могут противоречить друг другу, поэтому рассматривайте их по порядку. В результате этого процесса можно определить несколько ключей; обычно достаточно 4–5:

* Выбирайте столбцы, которые соответствуют вашим типичным фильтрам. Если столбец часто используется в секциях `WHERE`, включать его в ключ следует в первую очередь по сравнению с теми, которые используются реже.
  Отдавайте предпочтение столбцам, которые при фильтрации позволяют исключить большую часть строк и тем самым уменьшить объем данных, которые нужно прочитать.
* Отдавайте предпочтение столбцам, которые, вероятно, сильно коррелируют с другими столбцами таблицы. Это поможет обеспечить их последовательное хранение и улучшить сжатие.
  Операции `GROUP BY` и `ORDER BY` для столбцов, входящих в ключ сортировки, также можно сделать более эффективными с точки зрения использования памяти.

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

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

Применяя приведённые выше рекомендации к нашей таблице `posts`, предположим, что пользователи хотят выполнять аналитические запросы с фильтрацией по дате и типу поста, например:

"У каких вопросов было больше всего комментариев за последние 3 месяца".

Запрос для этого вопроса с использованием нашей таблицы `posts_v2`, созданной ранее с оптимизированными типами, но без ключа сортировки:

```sql theme={null}
SELECT
    Id,
    Title,
    CommentCount
FROM posts_v2
WHERE (CreationDate >= '2024-01-01') AND (PostTypeId = 'Question')
ORDER BY CommentCount DESC
LIMIT 3
```

```response theme={null}
┌───────Id─┬─Title─────────────────────────────────────────────────────────────┬─CommentCount─┐
│ 78203063 │ How to avoid default initialization of objects in std::vector?     │               74 │
│ 78183948 │ About memory barrier                                               │               52 │
│ 77900279 │ Speed Test for Buffer Alignment: IBM's PowerPC results vs. my CPU │        49 │
└──────────┴───────────────────────────────────────────────────────────────────┴──────────────

10 строк в наборе. Elapsed: 0.070 sec. Processed 59.82 million rows, 569.21 MB (852.55 million rows/s., 8.11 GB/s.)
Пиковое потребление памяти: 429.38 MiB.
```

> Запрос здесь выполняется очень быстро, хотя все 60 млн строк были линейно просканированы — ClickHouse просто быстрый :) Вам придётся поверить нам на слово: на масштабах TB и PB ключи сортировки действительно того стоят!

Давайте выберем столбцы `PostTypeId` и `CreationDate` в качестве ключей сортировки.

Возможно, в нашем случае пользователи всегда будут фильтровать по `PostTypeId`. Этот столбец имеет мощность 8 и выглядит логичным выбором для первого элемента ключа сортировки. Поскольку фильтрации по дате, скорее всего, будет достаточно (хотя это всё равно поможет и с фильтрами по дате и времени), в качестве 2-го компонента ключа мы используем `toDate(CreationDate)`. Это также позволит создать индекс меньшего размера, так как дату можно представить 16 битами, что ускоряет фильтрацию. Последний элемент нашего ключа — `CommentCount`, чтобы упростить поиск постов с наибольшим числом комментариев (финальную сортировку).

```sql theme={null}
CREATE TABLE posts_v3
(
        `Id` Int32,
        `PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
        `AcceptedAnswerId` UInt32,
        `CreationDate` DateTime,
        `Score` Int32,
        `ViewCount` UInt32,
        `Body` String,
        `OwnerUserId` Int32,
        `OwnerDisplayName` String,
        `LastEditorUserId` Int32,
        `LastEditorDisplayName` String,
        `LastEditDate` DateTime,
        `LastActivityDate` DateTime,
        `Title` String,
        `Tags` String,
        `AnswerCount` UInt16,
        `CommentCount` UInt8,
        `FavoriteCount` UInt8,
        `ContentLicense` LowCardinality(String),
        `ParentId` String,
        `CommunityOwnedDate` DateTime,
        `ClosedDate` DateTime
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CommentCount)
COMMENT 'Ordering Key'

--заполнить таблицу из существующей таблицы

INSERT INTO posts_v3 SELECT * FROM posts_v2
```

```response theme={null}
0 rows in set. Elapsed: 158.074 sec. Processed 59.82 million rows, 76.21 GB (378.42 thousand rows/s., 482.14 MB/s.)
Peak memory usage: 6.41 GiB.
```

Наш предыдущий запрос сокращает время ответа более чем в 3 раза:

```sql theme={null}
SELECT
    Id,
    Title,
    CommentCount
FROM posts_v3
WHERE (CreationDate >= '2024-01-01') AND (PostTypeId = 'Question')
ORDER BY CommentCount DESC
LIMIT 3
```

```response theme={null}
10 rows in set. Elapsed: 0.020 sec. Processed 290.09 thousand rows, 21.03 MB (14.65 million rows/s., 1.06 GB/s.)
```

Если вас интересует, какого улучшения сжатия можно добиться за счёт использования определённых типов данных и подходящих ключей сортировки, см. [Сжатие в ClickHouse](/ru/guides/clickhouse/data-modelling/compression/compression-in-clickhouse). Если вам нужно дополнительно повысить степень сжатия, рекомендуем также раздел [Выбор подходящего кодека сжатия для столбца](/ru/guides/clickhouse/data-modelling/compression/compression-in-clickhouse#choosing-the-right-column-compression-codec).

<div id="next-data-modeling-techniques">
  ## Далее: методы моделирования данных
</div>

До этого момента мы мигрировали только одну таблицу. Хотя это позволило познакомить вас с некоторыми базовыми концепциями ClickHouse, большинство схем, к сожалению, устроены сложнее.

В других руководствах, перечисленных ниже, мы рассмотрим несколько подходов к перестройке более широкой схемы для оптимального выполнения запросов в ClickHouse. На протяжении всего этого процесса мы стремимся сохранить `Posts` как центральную таблицу, через которую будет выполняться большинство аналитических запросов. Хотя остальные таблицы по-прежнему можно запрашивать отдельно, мы исходим из того, что основная часть аналитики будет выполняться в контексте `posts`.

> В этом разделе мы используем оптимизированные варианты других таблиц. Хотя мы приводим их схемы, ради краткости не останавливаемся на принятых решениях. Они основаны на правилах, описанных ранее, и выводы о том, почему были выбраны именно такие решения, мы оставляем читателю.

Все приведенные ниже подходы направлены на то, чтобы свести к минимуму необходимость использовать JOIN, оптимизировать чтение и повысить производительность запросов. Хотя JOIN в ClickHouse полностью поддерживаются, для достижения наилучшей производительности мы рекомендуем использовать их умеренно (JOIN-запросы с 2–3 таблицами — это нормально).

> В ClickHouse нет понятия внешних ключей. Это не запрещает JOIN, но означает, что управление ссылочной целостностью остается на стороне пользователя и реализуется на уровне приложения. В OLAP-системах, таких как ClickHouse, целостность данных часто обеспечивается на уровне приложения или в процессе ингестии данных, а не самой базой данных, где это создавало бы существенные накладные расходы. Такой подход дает больше гибкости и ускоряет вставку данных. Это соответствует ориентации ClickHouse на скорость и масштабируемость запросов на чтение и вставку при работе с очень большими наборами данных.

Чтобы свести к минимуму использование JOIN на этапе выполнения запроса, у пользователей есть несколько инструментов и подходов:

* [**Денормализация данных**](/ru/guides/clickhouse/data-modelling/denormalization) - Денормализуйте данные, объединяя таблицы и используя сложные типы для связей, отличных от 1:1. Это часто означает перенос JOIN с этапа выполнения запроса на этап вставки.
* [**Dictionaries**](/ru/concepts/features/dictionaries/index) - Специфичная для ClickHouse возможность для direct joins и поиска по ключу-значению.
* [**Incremental Materialized Views**](/ru/concepts/features/materialized-views/incremental-materialized-view) - Возможность ClickHouse, позволяющая перенести вычислительную нагрузку с этапа выполнения запроса на этап вставки, включая инкрементальное вычисление агрегированных значений.
* [**Refreshable Materialized Views**](/ru/concepts/features/materialized-views/refreshable-materialized-view) - Подобно materialized view в других СУБД, позволяет периодически вычислять результаты запроса и кэшировать их.

В каждом из руководств мы рассматриваем один из этих подходов, показывая, когда он уместен, и приводя пример того, как его можно применить для решения задач на наборе данных Stack Overflow.
