<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>ClickHouse - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/tag/clickhouse/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/tag/clickhouse/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Tue, 21 Oct 2025 06:31:58 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>ClickHouse - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/tag/clickhouse/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</title>
		<link>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/</link>
					<comments>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Mon, 20 Oct 2025 20:32:50 +0000</pubDate>
				<category><![CDATA[ClickHouse]]></category>
		<category><![CDATA[Bronze layer]]></category>
		<category><![CDATA[ClickPipes]]></category>
		<category><![CDATA[Gold layer]]></category>
		<category><![CDATA[Medallion architecture]]></category>
		<category><![CDATA[Silver layer]]></category>
		<category><![CDATA[sql.clickhouse.com]]></category>
		<category><![CDATA[движок ReplacingMergeTree]]></category>
		<category><![CDATA[движок S3Queue]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2203</guid>

					<description><![CDATA[<p>Ниже — перевод статьи “Building a Medallion architecture for Bluesky JSON data with ClickHouse” с сайта ClickHouse. Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse Мы так же взволнованы, как и вся остальная дата-сообщество, из-за недавнего всплеска популярности социальной сети BlueSky и её API, который позволяет получать доступ к потоку публикуемого [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/">Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Ниже — перевод статьи <a href="https://clickhouse.com/blog/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse" target="_blank" rel="noopener">“Building a Medallion architecture for Bluesky JSON data with ClickHouse”</a> с сайта ClickHouse.</p>
<h1>Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</h1>
<p>Мы так же взволнованы, как и вся остальная дата-сообщество, из-за недавнего всплеска популярности социальной сети <strong>BlueSky</strong> и её <strong>API</strong>, который позволяет получать доступ к потоку публикуемого контента.</p>
<p>Этот набор данных содержит поток с высокой пропускной способностью — тысячи <strong>JSON-событий</strong> в секунду, и мы подумали, что будет интересно сделать эти данные доступными для сообщества, чтобы каждый мог выполнять по ним запросы.</p>
<p>Во время исследования данных мы обнаружили, что во многих событиях присутствуют некорректные или повреждённые временные метки. Набор данных также содержит частые дубликаты. Поэтому мы не можем просто импортировать данные и на этом закончить — потребуется некоторая очистка.</p>
<p>Это идеальная возможность попробовать <strong>архитектуру Medallion</strong>, о которой мы недавно <a href="https://datatalks.ru/medallion-architecture-with-clickhouse/" target="_blank" rel="noopener">писали в блоге</a>. В этом посте мы оживим эти концепции на практическом примере.</p>
<p>Мы создадим рабочий процесс, который решает эти задачи, <strong>организуя набор данных в три отдельные уровня: бронзовый, серебряный и золотой</strong>. Мы будем придерживаться принципов архитектуры <strong>Medallion</strong> и активно использовать недавно представленный <strong>тип данных JSON</strong>.</p>
<p>Каждый уровень будет доступен для публичных запросов в нашей демо-среде на <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, где читатели смогут самостоятельно изучить и взаимодействовать с результатами. Мы даже подготовили несколько примерных аналитических запросов, чтобы вам было проще начать!</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2214" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png" alt="" width="2012" height="704" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png 2012w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-300x105.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1024x358.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-768x269.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1536x537.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-450x157.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-780x273.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1600x560.png 1600w" sizes="(max-width: 2012px) 100vw, 2012px" /></a></p>
<h1>Что такое Bluesky?</h1>
<p>Для тех, кто не так активен в социальных сетях, вы могли пропустить недавний взлёт популярности <strong>Bluesky</strong>, которая в настоящее время набирает почти миллион пользователей в день. <strong>Bluesky</strong> — это социальная сеть, похожая на X (бывший Twitter), но, в отличие от него, она полностью открыта и децентрализована!</p>
<p><strong>Bluesky</strong>, построенная на <strong>AT Protocol (ATProto)</strong>, представляет собой децентрализованную платформу социальных сетей, которая позволяет пользователям самостоятельно размещать свой контент. По умолчанию данные хранятся на <strong>Bluesky Personal Data Server (PDS)</strong>, но пользователи могут выбирать — размещать эти серверы (и свой контент) у себя. Такой подход отражает возврат к принципам раннего Интернета, когда пользователи имели контроль над своим контентом и связями, вместо того чтобы зависеть от централизованных платформ, которые доминируют и владеют пользовательскими данными.</p>
<p>Данные каждого пользователя управляются в лёгкой, открытой программной среде, где одна база данных SQLite используется для хранения. Такая структура обеспечивает взаимодействие между системами (interoperability) и гарантирует, что право собственности на контент остаётся за пользователем, даже если центральная платформа выйдет из строя или изменит свою политику.</p>
<p>И самое главное для нас: как и старый Twitter, Bluesky предоставляет бесплатный способ получать события — например, посты — в реальном времени, что открывает потенциально огромный набор данных для аналитики, по мере того как сеть набирает популярность.</p>
<h1>Чтение данных Bluesky</h1>
<p>Чтобы загрузить данные из Bluesky, мы используем недавно выпущенный <strong>Jetstream API</strong>, который упрощает потребление событий Bluesky, предоставляя потоки, закодированные в формате JSON. В отличие от оригинального <strong>firehose</strong>, который требует обработки <strong>бинарных данных CBOR</strong> и <strong>файлов CAR</strong>, Jetstream снижает сложность, делая процесс доступным для разработчиков, работающих с приложениями в реальном времени. Этот API идеально соответствует нашему случаю использования, позволяя фильтровать и обрабатывать тысячи событий в секунду из постов Bluesky, одновременно решая распространённые проблемы, такие как повреждённые данные и высокий уровень дублирования.</p>
<p>В нашей реализации мы подключаемся к публичному экземпляру <strong>Jetstream</strong>, потребляя непрерывный поток событий в формате JSON для загрузки. Для этого используется простой <strong>bash-скрипт</strong>, который обрабатывает <strong>поток JSON-событий</strong> в реальном времени из <strong>Jetstream</strong>.</p>
<p><a href="https://github.com/ClickHouse/sql.clickhouse.com/blob/main/load_scripts/bluesky/ingest.sh" target="_blank" rel="noopener">Ссылка на полный bash-скрипт.</a></p>
<p>Вкратце, он выполняет следующее:</p>
<ul>
<li>Проверяет <strong>GCS bucket</strong> на наличие самого последнего файла <code>.csv.gz</code>, извлекает его временную метку (используемую как курсор) и применяет её для возобновления подписки <strong>Jetstream</strong> с нужной позиции. Это обеспечивает непрерывность данных и минимизирует дублирование.</li>
<li>Инструмент <code><strong>websocat</strong></code> используется для подключения к <strong>Jetstream API</strong>, подписки на события и передачи <strong>JSON-потока</strong> для обработки. Параметр <code>wantedCollections</code> фильтрует нужные события, а <code>cursor</code> обеспечивает пошаговое (инкрементальное) получение данных, например:</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">websocat -Un --max-messages-rev $MAX_MESSAGES "$WS_URL/subscribe?wantedCollections=app.*&amp;cursor=$cursor" &gt; "$OUTPUT_FILE"</pre><p></p>
<ul>
<li>Входящие данные <strong>JSON</strong> разбиваются на фрагменты по <strong>500 000 строк</strong>, при этом каждый фрагмент представляет собой файл, где последняя временная метка используется в качестве идентификатора файла. Мы используем <code>clickhouse-local</code> для преобразования файла в CSV, затем сжимаем его в <code>.gz</code> и загружаем в <strong>GCS bucket</strong> с помощью <code>gsutil</code>.</li>
<li>Скрипт выполняется внутри <strong>Docker-контейнера</strong> ClickHouse, который запускается каждые 3 минуты с помощью <code>Google Cloud Run Job</code>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg"><img decoding="async" class="aligncenter size-full wp-image-2215" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg" alt="" width="1129" height="748" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg 1129w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-300x199.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-1024x678.jpg 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-768x509.jpg 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-450x298.jpg 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-780x517.jpg 780w" sizes="(max-width: 1129px) 100vw, 1129px" /></a></p>
<p>Обратите внимание, что файлы естественным образом упорядочены по своим именам, основанным на временной метке последнего события. Это критически важно для последующего эффективного инкрементального чтения из <strong>GCS bucket</strong>. Однако скрипт не гарантирует, что будут зафиксированы все события Bluesky.</p>
<h1>Выборка (sampling) данных</h1>
<p>На момент написания этого поста мы зафиксировали почти <strong>1,5 миллиарда событийных строк</strong>, собранных примерно за 21 день. Мы можем использовать функцию <strong>gcs</strong> в <strong>ClickHouse</strong>, чтобы выполнить запрос к данным напрямую и определить общее количество необработанных строк.</p><pre class="urvanov-syntax-highlighter-plain-tag">clickhouse-cloud :) SELECT count()
FROM gcs('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')

┌────count()─┐
│ 1484500000 │ -- 1.48 billion
└────────────┘

1 row in set. Elapsed: 72.396 sec. Processed 1.48 billion rows, 205.07 GB (20.51 million rows/s., 2.83 GB/s.)
Peak memory usage: 4.85 GiB.</pre><p>Мы можем взять выборку данных, используя ту же функцию, преобразовав каждую строку в тип <strong>JSON</strong> и применив формат <code>PrettyJSONEachRow</code>, чтобы получить читаемый результат.</p><pre class="urvanov-syntax-highlighter-plain-tag">SET allow_experimental_json_type = 1

SELECT data::'JSON' AS event
FROM gcs('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')
LIMIT 1
FORMAT PrettyJSONEachRow

{
  "account": {
    "active": true,
    "did": "did:plc:kjealuouxn3l6v4byxh2fhff",
    "seq": "706717212",
    "time": "2024-11-27T18:00:02.429Z"
  },
  "did": "did:plc:kjealuouxn3l6v4byxh2fhff",
  "kind": "account",
  "time_us": "1732730402720719"
}

1 row in set. Elapsed: 0.233 sec.</pre><p>Хотя приведённый выше пример даёт некоторое представление о структуре событий, он не полностью отражает сложность, изменчивость и непоследовательность данных. Столбец <code>kind</code> в значительной степени определяет последующую структуру, при этом <strong>API</strong> передаёт три типа событий: <code>commit</code>, <code>identity</code> и <code>account</code>.</p>
<p><strong>Краткое описание типов событий:</strong></p>
<ul>
<li><code>commit</code> — событие фиксации (commit) указывает на создание, обновление или удаление записи. Этот тип представляет большинство событий и включает посты, лайки и подписки.</li>
<li><code>identity</code> — обновление идентичности учётной записи.</li>
<li><code>account</code> — обновление состояния учётной записи.</li>
</ul>
<p>Мы подробнее исследуем эти данные после их загрузки в бронзовый слой (<strong>Bronze layer</strong>).</p>
<h1>Проблемы с данными Bluesky</h1>
<p>Данные Bluesky, как они поступают через JetStream API, имеют ряд проблем, включая следующее:</p>
<ul>
<li><strong>Повреждённый JSON (Malformed JSON)</strong> — время от времени встречаются некорректно сформированные JSON-события. Хотя они редки, такие записи могут нарушить обработку файла. Мы исключаем их с помощью функции <code>isValidJSON</code>, ограничивая загрузку в бронзовый слой (Bronze layer) только теми строками, для которых функция возвращает значение 1.</li>
<li><strong>Непоследовательная структура (Inconsistent structure)</strong> — хотя временная метка сбора данных (поле time_us) присутствует во всех событиях, путь JSON, содержащий время, когда событие произошло, зависит от типа события. Наш рабочий процесс должен извлекать единую согласованную временную метку, основываясь на этих условиях. Простой анализ показывает, что:
<ul>
<li><code>commit.record.createdAt</code> можно использовать для событий типа commit;</li>
<li><code>identity.time</code> — для событий identity;</li>
<li><code>account.time</code> — для событий account.</li>
</ul>
</li>
<li><strong>Будущие или некорректные временные метки (Future or invalid timestamps)</strong> — некоторые события имеют временные метки из будущего. Например, при выборке событий на момент написания поста 42 тысячи commit-событий имели будущие значения времени. Ещё 4 миллиона commit-событий имели метки времени, относящиеся к периоду до запуска Bluesky как сервиса.</li>
<li><strong>Повторяющиеся структуры (Repeated structures)</strong> — встречаются случаи, когда JSON содержит глубоко рекурсивные структуры. Это приводит к появлению более 1800 уникальных JSON-путей, большинство из которых, вероятно, не имеют существенной ценности для анализа содержимого.</li>
<li><strong>Дубликаты (Duplicates)</strong> — несмотря на использование курсора для поддержания последовательности данных, JetStream API создаёт дубликаты (где содержимое идентично, за исключением временной метки сбора). Удивительно, но такие дубликаты могут появляться в широком диапазоне времени — в некоторых случаях с разницей до 24 часов. Важно отметить, что большинство дубликатов встречаются в интервале около 20 минут.</li>
</ul>
<p>Приведённые выше пункты не представляют собой исчерпывающий список проблем с качеством данных — мы продолжаем находить новые сложности! Однако, в целях наглядности и сжатости примера, в нашем демонстрационном <strong>Medallion workflow</strong> мы сосредоточимся именно на перечисленных проблемах.</p>
<h1>Тип данных JSON в ClickHouse</h1>
<p><strong>JSON</strong> играет ключевую роль в реализации архитектуры <strong>Medallion</strong> для данных Bluesky, позволяя системе хранить высокодинамичную и полуструктурированную информацию в бронзовом слое (<strong>Bronze layer</strong>). Новый тип данных JSON в ClickHouse, представленный в версии 24.8, решил ключевые проблемы, с которыми сталкивались предыдущие реализации.</p>
<p>В отличие от традиционных подходов, которые предполагают единственный тип для каждого JSON-пути (что часто приводит к принудительному приведению типов или их преобразованию), <strong>JSON-тип в ClickHouse</strong> хранит значения каждого уникального пути и типа в отдельных подколонках (sub-columns).</p>
<p>Такой подход обеспечивает эффективное хранение, сводит к минимуму лишние операции ввода-вывода (I/O) и избегает затрат на приведение типов во время выполнения запроса.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png"><img decoding="async" class="aligncenter size-full wp-image-2216" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png" alt="" width="2560" height="1065" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-300x125.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1024x426.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-768x319.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1536x639.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-2048x852.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-450x187.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-780x324.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1600x665.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Например, когда в таблицу вставляются два JSON-пути с разными типами данных, ClickHouse сохраняет значения каждого типа в отдельных подколонках. Эти подколонки могут быть запрошены независимо, что снижает ненужные операции ввода-вывода.<br />
При этом, если запросить колонку, содержащую несколько типов данных, её значения всё равно возвращаются как единый столбец в ответе.</p>
<p>Кроме того, благодаря использованию смещений (<strong>offsets</strong>), ClickHouse гарантирует, что подколонки остаются плотными (<strong>dense</strong>) — то есть не хранят значения по умолчанию для отсутствующих JSON-путей. Такой подход максимизирует степень сжатия и дополнительно снижает нагрузку на I/O.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2217" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png" alt="" width="2560" height="1396" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-300x164.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1024x558.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-768x419.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1536x838.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-2048x1117.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-450x245.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-780x425.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1600x873.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Также данный тип данных не страдает от проблемы “взрыва подколонок” (sub-column explosion), возникающей при большом количестве уникальных JSON-путей.<br />
Это особенно важно для данных Bluesky, где при отсутствии фильтрации встречается более 1800 уникальных путей.<br />
При этом это не мешает хранению всех этих путей — новые пути просто сохраняются в общей колонке данных, если превышен лимит (при этом статистика ускоряет выполнение запросов).</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2219" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png" alt="" width="2560" height="1346" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-300x158.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1024x539.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-768x404.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1536x808.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-2048x1077.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-450x237.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-780x410.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1600x841.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Такое оптимизированное обращение с JSON обеспечивает эффективное хранение сложных, полуструктурированных наборов данных, таких как данные Bluesky, в бронзовом слое архитектуры.<br />
Для пользователей, заинтересованных в технических деталях реализации этого нового типа колонок, рекомендуется ознакомиться с подробным постом в нашем блоге (ссылка предоставлена в оригинале).</p>
<h1>Бронзовый уровень для необработанных данных (Bronze, сырые данные)</h1>
<p>Хотя исходное описание <strong>бронзового слоя (Bronze layer)</strong> не предполагает фильтрацию или преобразование данных, мы относимся к этому менее догматично и считаем, что минимальная фильтрация и недеструктивные преобразования данных могут быть полезны для исследования проблем и возможности воспроизведения данных в будущем.</p>
<p>Для преобразований мы рекомендуем ограничиться теми, которые можно реализовать с помощью <strong>материализованных колонок (Materialized columns)</strong>, как показано ниже в нашей схеме бронзового слоя:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_raw
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `_file` LowCardinality(String),
  `kind` LowCardinality(String) MATERIALIZED getSubcolumn(data, 'kind'),
  `scrape_ts` DateTime64(6) MATERIALIZED fromUnixTimestamp64Micro(CAST(getSubcolumn(data, 'time_us'), 'UInt64')),
  `bluesky_ts` DateTime64(6) MATERIALIZED multiIf(getSubcolumn(data, 'kind') = 'commit', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'commit.record.createdAt'), 'String')), getSubcolumn(data, 'kind') = 'identity', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'identity.time'), 'String')), getSubcolumn(data, 'kind') = 'account', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'account.time'), 'String')), toDateTime64(0, 6)),
  `dedup_hash` String MATERIALIZED cityHash64(arrayFilter(p -&gt; ((p.1) != 'time_us'), JSONExtractKeysAndValues(CAST(data, 'String'), 'String')))
)
ENGINE = ReplacingMergeTree
PRIMARY KEY (kind, bluesky_ts)
ORDER BY (kind, bluesky_ts, dedup_hash)</pre><p>Некоторые важные замечания по этой схеме:</p>
<ul>
<li><strong>Тип JSON</strong> — колонка data использует новый тип данных JSON и содержит всё событие целиком.<br />
Мы применяем оператор <strong>SKIP</strong>, чтобы исключить определённые пути JSON, которые, как показал анализ, были ответственны за повторяющиеся структуры, отмеченные ранее.</li>
<li><strong>Сохранение метаданных</strong> — колонка <code>_file</code> содержит ссылку на файл, из которого была загружена строка.</li>
<li><strong>Материализованные колонки (Materialized columns)</strong> — остальные колонки являются материализованными и вычисляются из колонки data во время вставки:
<ul>
<li><code>scrape_ts</code> — время, когда событие было доставлено; извлекается из поля JSON time_us.</li>
<li><code>kind</code> — тип события, как упоминалось ранее.</li>
<li><code>bluesky_ts</code> — выполняет условную логику, извлекая временную метку события на основе значения kind; это решает проблему непоследовательной структуры и обеспечивает единый формат временных меток для всех событий.</li>
<li><code>dedup_hash</code> — содержит хеш события.</li>
</ul>
</li>
<li>Для вычисления хеша создаётся массив всех <strong>JSON-путей</strong> и их значений, за исключением <code>time_us</code> (так как это поле отличается у дубликатов), с помощью функции <code>JSONExtractKeysAndValues</code>.<br />
Затем функция <code>cityHash64</code> обрабатывает этот массив, создавая уникальный хеш события.</li>
<li><strong>ReplacingMergeTree</strong> — используется движок <code>ReplacingMergeTree</code>, который позволяет устранять дубликаты записей, имеющих одинаковые значения ключей сортировки (<strong>ORDER BY</strong>).<br />
<strong>Дедупликация выполняется асинхронно</strong> во время фоновых слияний, которые происходят в неопределённое время и не могут быть напрямую контролируемы — то есть дедупликация осуществляется постепенно (<code>eventual deduplication</code>).</li>
</ul>
<p>В нашей схеме ключ <strong>ORDER BY</strong> включает <code>kind</code> и <code>bluesky_ts</code>, что:</p>
<ul>
<li>обеспечивает эффективное чтение;</li>
<li>гарантирует высокую степень сжатия, группируя строки с похожими атрибутами.</li>
</ul>
<p>Мы также добавляем <code>dedup_hash</code>, чтобы уникально идентифицировать строки для дедупликации, но не включаем его в <strong>PRIMARY KEY</strong>.<br />
Это оптимизация, которая предотвращает загрузку индекса по <code>dedup_hash</code> в память — разумное решение, так как мы не выполняем прямые запросы по хешу.</p>
<p>Наш бронзовый слой выполняет минимальные преобразования данных с помощью материализованных колонок, но при этом обеспечивает возможность дедупликации.<br />
Важно отметить, что использование <strong>ReplacingMergeTree</strong> здесь не является обязательным и не влияет на будущие слои.</p>
<p>Пользователи могут предпочесть обычный <strong>MergeTree</strong>, если хотят анализировать дубликаты напрямую.</p>
<p>Наш выбор обусловлен главным образом желанием минимизировать объём хранимых данных.</p>
<h2>Загрузка данных из объектного хранилища (s3, object storage)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2221" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png" alt="" width="977" height="616" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png 977w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-300x189.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-768x484.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-450x284.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-780x492.png 780w" sizes="(max-width: 977px) 100vw, 977px" /></a></p>
<p>Как описано выше, наш конвейер загрузки данных (<strong>ingestion pipeline</strong>) использует инструмент <strong>websocat</strong> для потоковой передачи данных из <strong>JetStream API</strong>, сохраняя события в виде файлов <code>.csv.gz</code> в <strong>Google Cloud Storage (GCS)</strong>.</p>
<p>Этот промежуточный шаг предоставляет несколько преимуществ:</p>
<ul>
<li>он позволяет воспроизводить данные (<strong>data replay</strong>),</li>
<li>сохраняет оригинальную копию необработанных данных (<strong>raw data</strong>)</li>
<li>и имитирует подход, который многие пользователи используют для загрузки данных из объектного хранилища.</li>
</ul>
<p>Чтобы считать эти файлы из <strong>GCS</strong> в нашу таблицу бронзового слоя <strong>bluesky_raw</strong>, мы используем движок таблицы <strong>S3Queue (S3Queue table engine)</strong>. Этот движок считывает данные из объектного хранилища, совместимого с <strong>S3</strong>, автоматически обрабатывает новые файлы по мере их добавления в бакет и вставляет их в указанную таблицу через <strong>материализованное представление (materialized view)</strong>.</p>
<p>Создание этой таблицы требует небольшой <strong>DDL-команды</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_queue
(
  `data` Nullable(String)
)
ENGINE = S3Queue('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')
SETTINGS mode = 'ordered', s3queue_buckets = 30, s3queue_processing_threads_num = 10;</pre><p>Обратите внимание, что мы указываем <strong>GCS-бакет</strong>, содержащий <strong>сжатые (gzipped) файлы</strong>,<br />
и определяем каждую строку как тип String с помощью объявления схемы.</p>
<p>Важно, что мы включаем <strong>&#171;ordered mode&#187;</strong> через настройку <code>mode = 'ordered'</code>. Это заставляет файлы обрабатываться в лексикографическом порядке, обеспечивая последовательную загрузку данных.</p>
<p>Хотя это означает, что файлы, добавленные с более ранним порядком сортировки, игнорируются, такая конфигурация поддерживает эффективную и инкрементальную обработку, и устраняет необходимость выполнять масштабные операции сравнения множеств, если файлы не имеют естественного порядка.</p>
<p>Наше раннее использование временных меток (<strong>timestamps</strong>) для имен файлов гарантирует, что данные обрабатываются в правильной последовательности, а движок <strong>S3Queue</strong> быстро распознаёт новые файлы, которые нужно загрузить.</p>
<p>Наша среда <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, в которую мы загружаем данные, состоит из трёх узлов, каждый из которых имеет 60 виртуальных процессорных ядер (<strong>vCPUs</strong>).</p>
<p>Параметр <code>s3queue_processing_threads_num</code> задаёт количество потоков для обработки файлов на каждом сервере.</p>
<p>Кроме того, при использовании ordered mode вводится дополнительная настройка &#8212; <code>s3queue_buckets</code>. Как рекомендуется, мы устанавливаем её как произведение количества реплик (3) на количество потоков обработки (10).</p>
<p>Чтобы потреблять строки из этой очереди, необходимо присоединить <strong>инкрементальное материализованное представление (Incremental Materialized View)</strong>. Это представление читает данные из очереди, выполняет SELECT-запрос над строками, а результат отправляется в таблицу бронзового слоя <strong>bluesky_raw</strong>.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.bluesky_mv TO bluesky.bluesky_raw
(
  `data` Nullable(String)
)
AS SELECT
  data,
  _file
FROM bluesky.bluesky_queue
WHERE isValidJSON(data) = 1</pre><p>Обратите внимание, что мы выполняем базовую фильтрацию уже на этом уровне:<br />
в таблицу бронзового слоя передаются только строки, где <code>isValidJSON(data) = 1</code>, то есть содержащие валидный <strong>JSON</strong>.</p>
<p>Также мы добавляем метаданные — колонку <code>_file</code>, чтобы иметь запись о том, из какого <strong>gzip-файла</strong> была загружена каждая строка.</p>
<h2>Потоковая передача Bluesky напрямую в ClickHouse (Streaming)</h2>
<p>Обратите внимание, что <strong>ClickHouse</strong> <strong>может напрямую выполнять потоковую загрузку данных</strong> с использованием <strong>JSON-форматов</strong> ввода, как недавно продемонстрировал наш технический директор (CTO) Алексей Миловидов.</p>
<p>Это можно реализовать, объединив тип данных JSON и формат ввода JSON.</p>
<p>Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">websocat -n "wss://jetstream1.us-east.bsky.network/subscribe?wantedCollections=app.*" | pv -l | split -l 1000 --filter='clickhouse-client --host sql-clickhouse.clickhouse.com --secure --password "" --query "INSERT INTO bluesky.bluesky_raw (data) FORMAT JSONAsObject"'</pre><p></p>
<h2>ClickPipes в ClickHouse Cloud</h2>
<p>Хотя механизм таблиц <strong>S3Queue</strong> позволяет нам выполнять потоковую передачу данных из объектного хранилища в ClickHouse, у него есть определённые ограничения. Помимо того, что он поддерживает только <strong>S3-совместимые хранилища</strong>, он обеспечивает <strong>семантику “по крайней мере один раз” (at-least-once)</strong>.</p>
<p>Пользователи ClickHouse Cloud могут предпочесть использовать <strong>ClickPipes</strong> — управляемое решение для загрузки данных, которое обеспечивает семантику <strong>“ровно один раз” (exactly-once)</strong>, поддерживает больше источников (например, <strong>Kafka</strong>) и разделяет ресурсы загрузки и ресурсы кластера.</p>
<p>Эта технология может быть использована для замены <strong>S3Queue</strong> в описанной выше архитектуре с минимальной настройкой через пошаговый мастер (<strong>guided wizard</strong>).</p>
<h2>Запросы к бронзовому уровню</h2>
<p>Хотя мы не рекомендуем предоставлять доступ к вашей таблице уровня <strong>Bronze</strong> конечным пользователям (<strong>downstream consumers</strong>), выбранный нами ключ сортировки (<strong>ordering key</strong>) позволяет эффективно исследовать данные, выявлять дополнительные проблемы с их качеством или, при необходимости, повторно воспроизводить данные через последующие уровни архитектуры.</p>
<p>Мы отмечали, что во время слияния (merge) <strong>движок ReplacingMergeTree</strong> определяет дубликаты строк, используя значения столбцов, указанных в <strong>ORDER BY</strong>, как уникальный идентификатор, и сохраняет только последнюю версию записи. Однако это обеспечивает лишь постепенную (<strong>eventual</strong>) корректность — то есть не гарантирует, что все дубликаты будут удалены, поэтому полагаться на это не стоит.</p>
<p>Чтобы гарантировать корректные результаты, пользователям необходимо дополнять <strong>фоновое объединение (background merges)</strong> операцией удаления дубликатов во время выполнения запроса, что можно сделать с помощью оператора <strong>FINAL</strong>.<br />
Однако это создаёт дополнительную нагрузку на ресурсы и негативно влияет на производительность запросов, что является ещё одной причиной, по которой мы не советуем предоставлять доступ к Bronze-таблицам потребителям данных.</p>
<p>В приведённых выше примерах запросов мы опускаем оператор <strong>FINAL</strong>, принимая небольшой уровень дублирования, поскольку это допустимо для разведочного анализа данных.</p>
<p>Большинство данных представляют собой <strong>commit-события (commit events)</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT kind, formatReadableQuantity(count()) AS c
FROM bluesky_raw
GROUP BY kind
FORMAT PrettyCompactMonoBlock
┌─kind─────┬─c──────────────┐
│ commit   │ 614.55 million │
│ account  │ 1.72 million   │
│ identity │ 1.70 million   │
└──────────┴────────────────┘

3 rows in set. Elapsed: 0.124 sec. Processed 617.97 million rows, 617.97 MB (5.00 billion rows/s., 5.00 GB/s.)
Peak memory usage: 139.03 MiB.</pre><p>Внутри этих <strong>commit-событий</strong> можно исследовать типы событий с помощью синтаксиса пути <strong>JSON (JSON path syntax)</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT
  data.commit.collection AS collection,
  count() AS c,
  uniq(data.did) AS users
FROM bluesky_raw
WHERE kind = 'commit'
GROUP BY ALL
ORDER BY c DESC
LIMIT 10
FORMAT PrettyCompactMonoBlock

┌─collection───────────────┬─────────c─┬───users─┐
│ app.bsky.feed.like       │ 705468149 │ 7106516 │
│ app.bsky.graph.follow    │ 406406091 │ 8629730 │
│ app.bsky.feed.post       │ 137946245 │ 4323265 │
│ app.bsky.feed.repost     │  90847077 │ 2811398 │
│ app.bsky.graph.block     │  25277808 │ 1523621 │
│ app.bsky.graph.listitem  │   8464006 │  166002 │
│ app.bsky.actor.profile   │   8168943 │ 4083558 │
│ app.bsky.graph.listblock │  643292   │  216695 │
│ app.bsky.feed.threadgate │  559504   │   94202 │
│ app.bsky.feed.postgate   │  275675   │   38790 │
└──────────────────────────┴───────────┴─────────┘

10 rows in set. Elapsed: 19.923 sec. Processed 1.38 billion rows, 122.00 GB (69.50 million rows/s., 6.12 GB/s.)
Peak memory usage: 1003.91 MiB.</pre><p>Мы видим, что основная часть событий — это <strong>“лайки”</strong> и <strong>“подписки (follows)”</strong>, что вполне ожидаемо.</p>
<h1>Серебряный уровень для очищенных данных (Silver Layer)</h1>
<p><strong>Слой Silver (Серебряный)</strong> представляет собой следующий этап в архитектуре <strong>Medallion</strong>, преобразуя сырые данные из слоя <strong>Bronze (Бронзового)</strong> в более согласованную и структурированную форму.</p>
<p><strong>Этот слой решает проблемы качества данных:</strong> выполняет дополнительную фильтрацию, стандартизирует схемы, производит преобразования и обеспечивает полное удаление дубликатов.<br />
В ClickHouse обычно наблюдается прямая связь между таблицами Bronze и их эквивалентами в Silver.</p>
<p>Мы знаем, что дубликаты событий имеют одинаковые значения <code>bluesky_ts</code> (и других столбцов), различаясь лишь по <code>scrape_ts</code>, причём последнее значение может быть значительно позже.<br />
Однако ранее мы установили, что большинство дубликатов появляются в пределах 20 минут.<br />
Чтобы гарантировать, что в <strong>золотой слой (Gold)</strong> не попадут дубликаты, мы вводим понятие конечного <strong>окна дедупликации (finite duplication window)</strong> в слое <strong>Silver</strong>.</p>
<p>События будут распределяться по этим окнам дедупликации, которые смещены относительно текущего времени на основе значения <code>bluesky_ts</code>.<br />
Эти «окна» периодически сбрасываются (<strong>flushed</strong>) в слой Gold, с гарантией, что в каждое окно попадёт только одна копия события.</p>
<p><strong>Использование окон дедупликации избавляет нас от необходимости проводить дедупликацию за бесконечный период времени, что существенно снижает нагрузку на систему и делает задачу более управляемой.</strong></p>
<p>Как мы покажем далее, это можно эффективно реализовать в ClickHouse.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2227" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png" alt="" width="998" height="711" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png 998w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-300x214.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-768x547.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-450x321.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-780x556.png 780w" sizes="(max-width: 998px) 100vw, 998px" /></a></p>
<p>Назначение событий в окна дедупликации, которые синхронизируются с реальным временем и периодически сбрасываются, предполагает, что данные доставляются без значительных задержек.</p>
<p><strong>Анализ таблицы Bronze показывает, что:</strong></p>
<ul>
<li>90% событий имеют значение bluesky_ts, отличающееся от времени их поступления (извлечённого из имени файла в GCS) не более чем на 20 минут.<br />
Это возможно, если:</li>
<li>Обработка 1 миллиона сообщений за один раз не вызывает значительных задержек;</li>
<li>Время чтения и обработки через S3Queue также незначительно (это можно проверить через системные таблицы);</li>
<li>Время, извлечённое из имени файла, близко к реальному времени загрузки, что подтверждается запросами к GCS.</li>
</ul>
<p>Кроме того, более 94% событий имеют разницу между <code>scrape_ts</code> и <code>bluesky_ts</code> меньше 20 минут (в 90% случаев — даже менее 10 секунд).<br />
Это означает, что значение <code>scrape_ts</code> также не отстаёт от времени поступления данных.</p>
<p>Понимая, что события обычно доставляются в течение 20 минут после их <code>bluesky_ts</code>, мы можем надёжно формировать окна дедупликации в слое Silver.<br />
Для этого мы создаём <strong>раздел (partition)</strong> в ClickHouse для каждого 20-минутного интервала — таким образом, раздел фактически соответствует одному окну.</p>
<p>События распределяются по разделам в зависимости от того, в какой интервал они попадают, с помощью функции:</p><pre class="urvanov-syntax-highlighter-plain-tag">toStartOfInterval(bluesky_ts, toIntervalMinute(20))</pre><p><strong>Итоговая схема таблицы Silver выглядит следующим образом:</strong></p>
<ul>
<li>Мы используем ReplacingMergeTree, но выполняем дедупликацию только внутри каждого раздела, то есть слияние происходит только в пределах окна.</li>
<li>Для управления объёмом данных применяется TTL, который удаляет строки, старше 1440 секунд (24 часа).</li>
</ul>
<p>Параметр <code>ttl_only_drop_parts = 1</code> гарантирует, что части удаляются только тогда, когда все строки в них устарели.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_dedup
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `kind` LowCardinality(String),
  `scrape_ts` DateTime64(6),
  `bluesky_ts` DateTime64(6),
  `dedup_hash` String
)
ENGINE = ReplacingMergeTree
PARTITION BY toStartOfInterval(bluesky_ts, toIntervalMinute(20))
ORDER BY dedup_hash
TTL toStartOfMinute(bluesky_ts) + toIntervalMinute(1440) SETTINGS ttl_only_drop_parts=1</pre><p>Так как слишком большое количество разделов может привести к проблемам производительности и ошибкам вроде <strong>“Too many parts”</strong>, мы ограничиваем таблицу Silver только одними сутками данных (всего 72 окна по 20 минут). Старые данные автоматически удаляются с помощью правил TTL, сохраняя эффективность и стабильность системы.</p>
<h2>Инкрементные материализованные представления для фильтрации</h2>
<p>При применении фильтрации и правил дедупликации к данным <strong>уровня Bronze</strong>, пользователи часто сохраняют «негативные совпадения» (то есть записи, не прошедшие фильтры) в отдельной таблице — так называемой <strong>Dead-Letter таблице</strong> — для последующего анализа.</p>
<p>Так как мы планируем периодически отправлять свежие партиции из слоя Silver в слой Gold, нам нежелательно, чтобы события поступали слишком поздно.<br />
По этой причине, а также чтобы продемонстрировать <strong>принцип “dead letter queue”</strong>, мы будем отправлять все события из слоя Bronze, у которых разница между <code>scrape_ts</code> и <code>bluesky_ts</code> превышает 20 минут, в <strong>очередь “dead letter”</strong>.</p>
<p>События же, у которых задержка меньше 20 минут, будут вставляться в соответствующую партицию таблицы Silver, показанную ранее.</p>
<p>Для реализации этого подхода мы используем две <strong>инкрементные материализованные представления (incremental materialized views)</strong>.<br />
Каждое из них выполняет SELECT-запрос к строкам, вставленным в таблицу уровня Bronze (<strong>bluesky_raw</strong>), и отправляет результаты либо:</p>
<ul>
<li>в таблицу <strong>dead letter queue</strong>,</li>
<li>либо в таблицу Silver (<strong>bluesky_dedup</strong>).</li>
</ul>
<p>Основное различие между этими двумя представлениями заключается в их фильтрующих условиях.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2229" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png" alt="" width="981" height="637" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png 981w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-300x195.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-768x499.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-450x292.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-780x506.png 780w" sizes="(max-width: 981px) 100vw, 981px" /></a></p>
<p>Представление для отправки строк в <strong>таблицу Silver</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.bluesky_dedup_mv TO bluesky.bluesky_dedup
(
	`data` JSON,
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
AS SELECT
	data,
	kind,
	scrape_ts,
	bluesky_ts,
	dedup_hash
FROM bluesky.bluesky_raw
WHERE abs(timeDiff(scrape_ts, bluesky_ts)) &lt; 1200</pre><p>Схема таблицы <strong>Dead-Letter Queue</strong> и связанное с ней материализованное представление:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_dlq
(
	`data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
ENGINE = MergeTree
ORDER BY (kind, scrape_ts)

CREATE MATERIALIZED VIEW bluesky.bluesky_dlq_mv TO bluesky.bluesky_dlq
(
	`data` JSON,
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
AS SELECT
	data,
	kind,
	scrape_ts,
	bluesky_ts,
	dedup_hash
FROM bluesky.bluesky_raw
WHERE abs(timeDiff(scrape_ts, bluesky_ts)) &gt;= 1200</pre><p>Обратите внимание, что для <strong>очереди “dead letter”</strong> используется обычный движок <strong>MergeTree</strong>,<br />
так как дедупликация здесь не требуется — эти данные предназначены для анализа проблем и диагностики, а не для основной аналитики.</p>
<h2>Отправка данных на золотой уровень (Gold Layer)</h2>
<p>Описанный выше процесс оставляет <strong>разделы (partitions)</strong>, заполненные на уровне <strong>Silver</strong>.<br />
Периодически нам необходимо переносить данные из этих разделов в уровень <strong>Gold</strong>, гарантируя, что все события были полностью дедуплицированы, и при этом делать это достаточно оперативно, чтобы обеспечить наличие свежих данных в <strong>слое Gold</strong> для аналитики.</p>
<p>Мы реализуем этот периодический <strong>перенос (flushing)</strong> с помощью <strong>Refreshable Materialized View</strong>.<br />
Такие представления выполняются периодически по таблицам уровня Silver и позволяют выполнять сложные преобразования, включая денормализацию данных перед их записью в таблицы уровня Gold.</p>
<p>В нашем случае нам нужно просто периодически вставлять данные из последнего раздела, который больше не получает новых данных, в таблицу <strong>Gold</strong>.<br />
Запрос при этом должен выполняться с использованием оператора FINAL, чтобы гарантировать, что все события дедуплицированы.</p>
<p>Хотя такой запрос обычно более затратен вычислительно, здесь мы можем использовать два преимущества:</p>
<ul>
<li>Запрос выполняется периодически — в нашем случае каждые 20 минут, что смещает нагрузку с пользовательских запросов на уровень загрузки данных.</li>
<li>Мы обрабатываем только один раздел за одно выполнение. Можно ограничить дедупликацию во время выполнения только этим разделом, установив параметр <code>do_not_merge_across_partitions_select_final=1</code>, что дополнительно оптимизирует запрос и снижает нагрузку.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2265" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png" alt="" width="1112" height="840" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png 1112w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-300x227.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-1024x774.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-768x580.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-450x340.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-780x589.png 780w" sizes="(max-width: 1112px) 100vw, 1112px" /></a></p>
<p>Для этого требуется определить, какой именно раздел нужно перенести в <strong>Gold</strong> при каждом выполнении.<br />
Эта логика показана на диаграмме выше, а в кратком виде выглядит так:</p>
<ul>
<li>Мы определяем последний раздел в таблице Silver <strong>bluesky_dedup</strong> с помощью служебного поля <code>_partition_id</code>.<br />
Из этого значения вычитаем 40 минут, получая раздел, который был создан два окна назад (X &#8212; 2) — называем его <code>current_partition</code>.</li>
<li>В целевой таблице уровня Gold bluesky есть столбец <code>_rmt_partition_id</code>,<br />
заполняемый <strong>refreshable materialized view</strong>, где хранится, из какого раздела уровня Silver поступило каждое событие.<br />
Мы используем это поле, чтобы определить последний успешно перенесённый раздел, прибавляем 20 минут,<br />
получая раздел, который нужно обработать следующим — <code>next_to_process</code>.</li>
</ul>
<p>Если <code>next_to_process = 1200</code>, это значит, что таблица bluesky пуста<br />
(0 + 1200 секунд = 1200), и ещё не было ни одной передачи данных.<br />
В этом случае мы используем <code>current_partition</code> и вставляем все события, где <code>_partition_id = current_partition</code>.</p>
<p>Если <code>next_to_process &gt; 1200</code>, значит, переносы уже выполнялись.<br />
Если <code>current_partition &gt;= next_to_process</code>, то мы отстаём не менее чем на 40 минут (2 окна),<br />
и используем значение <code>next_to_process</code>, вставляя все события, где <code>_partition_id = next_to_process</code>.<br />
Если же <code>current_partition &lt; next_to_process</code>, выполняется <code>noop</code> (ничего не происходит) — данные не переносятся.</p>
<p>Эта логика устойчива к сбоям, таким как пропуски выполнения каждые 20 минут, повторные запуски или задержки выполнения. В результате формируется <code>Refreshable Materialized View</code>, в SELECT-запросе которого инкапсулирована описанная выше логика.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.blue_sky_dedupe_rmv
REFRESH EVERY 20 MINUTE APPEND TO bluesky.bluesky
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `kind` LowCardinality(String),
  `bluesky_ts` DateTime64(6),
  `_rmt_partition_id` LowCardinality(String)
)
AS WITH
  (
          --step 1
        SELECT toUnixTimestamp(subtractMinutes(CAST(_partition_id, 'DateTime'), 40))
        FROM bluesky.bluesky_dedup
        GROUP BY _partition_id
        ORDER BY _partition_id DESC
        LIMIT 1
  ) AS current_partition,
  (
          --step 2
        SELECT toUnixTimestamp(addMinutes(CAST(max(partition_id), 'DateTime'), 20))
        FROM bluesky.latest_partition
  ) AS next_to_process
SELECT
  data,
  kind,
  bluesky_ts,
  _partition_id AS _rmt_partition_id
FROM bluesky.bluesky_dedup
FINAL
--step 3 &amp; 4
WHERE _partition_id = CAST(if(next_to_process = 1200, current_partition, if(current_partition &gt;= next_to_process, next_to_process, 0)), 'String')
SETTINGS do_not_merge_across_partitions_select_final = 1</pre><p>Это представление выполняется каждые 20 минут, передавая очищенные и дедуплицированные данные в <strong>уровень Gold</strong>. Следует отметить, что данные появляются в <strong>Gold</strong> с задержкой около 40 минут, хотя при необходимости пользователи могут выполнять запросы к уровню <strong>Silver</strong> для получения более свежих данных.</p>
<p>Внимательный читатель заметит, что в шаге 2 и на диаграмме выше наш запрос использует таблицу <code>latest_partition</code>, а не обращается напрямую к <code>_rmt_partition_id</code> в таблице <strong>bluesky</strong> уровня Gold.<br />
Эта таблица создаётся с помощью инкрементного материализованного представления (incremental materialized view) и служит оптимизацией, которая ускоряет определение следующего раздела для обработки.</p>
<p>Это представление отслеживает последний вставленный раздел в таблицу <strong>Gold</strong> и выглядит следующим образом.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.latest_partition_mv TO bluesky.latest_partition
(
	`partition_id` UInt32
)
AS SELECT max(CAST(_rmt_partition_id, 'UInt32')) AS partition_id
FROM bluesky.bluesky

CREATE TABLE bluesky.latest_partition
(
	`partition_id` SimpleAggregateFunction(max, UInt32)
)
ENGINE = AggregatingMergeTree
ORDER BY tuple()</pre><p></p>
<h1>Золотой уровень для анализа данных (Gold Layer для аналитики)</h1>
<p>Указанное выше <strong>refreshable materialized view</strong> периодически отправляет данные в таблицу уровня Gold — bluesky.</p>
<p>Схема этой таблицы показана ниже:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky
(
	`data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
	`kind` LowCardinality(String),
	`bluesky_ts` DateTime64(6),
	`_rmt_partition_id` LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toStartOfInterval(bluesky_ts, toIntervalMonth(1))
ORDER BY (kind, bluesky_ts)</pre><p>Поскольку данные полностью дедуплицированы до момента вставки, мы можем использовать стандартный <strong>MergeTree</strong>.</p>
<p><strong>Ключ сортировки (ORDER BY)</strong> выбирается исключительно на основе шаблонов доступа потребителей данных и с целью оптимизации сжатия.</p>
<p><strong>Таблица разделена по месяцам (partitioned by month)</strong> &#8212; в первую очередь для удобства управления данными, а также потому, что ожидается, что большинство запросов будут обращаться к самым последним данным.</p>
<p>Обратите внимание: хотя мы по-прежнему используем тип данных <strong>JSON</strong> на этом уровне,<br />
возможно выполнение дополнительных трансформаций данных на этапе предыдущего<br />
<strong>refreshable materialized view</strong> — например:</p>
<ul>
<li>извлечение часто используемых полей в корень таблицы,</li>
<li>использование столбцов типа <strong>ALIAS</strong>, чтобы упростить синтаксис запросов и повысить удобство анализа.</li>
</ul>
<h2>Материализованные представления для общих запросов (Для часто запрашиваемых метрик)</h2>
<p>Этот <strong>слой gold</strong> должен быть полностью оптимизирован для выполнения запросов со стороны прикладных систем и потребителей данных. Хотя наш ключ сортировки направлен на то, чтобы облегчить этот процесс, не все шаблоны доступа будут одинаковыми. До настоящего времени наиболее распространённым применением инкрементных материализованных представлений было выполнение фильтрации и вставки данных между слоями. Однако наше более раннее использование представления для вычисления следующего раздела (partition) намекало на то, как ещё можно оптимизировать другие запросы.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2280" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png" alt="" width="1370" height="611" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png 1370w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-300x134.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-1024x457.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-768x343.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-450x201.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-780x348.png 780w" sizes="(max-width: 1370px) 100vw, 1370px" /></a></p>
<p>Помимо фильтрации и отправки подмножеств данных в целевую таблицу с другими ключами сортировки (оптимизированными под иные шаблоны доступа), материализованные представления могут использоваться для предварительного вычисления агрегатов во время вставки данных в таблицу gold.</p>
<p>Результаты таких агрегатов будут представлять собой уменьшенную форму исходных данных (частичный набросок, если речь идёт об агрегации). Это не только упрощает последующие запросы к целевой таблице, но и обеспечивает более высокую скорость выполнения, поскольку вычисления переносятся с момента запроса на момент вставки, тем самым снижая время отклика при запросе.</p>
<p>Полное руководство по материализованным представлениям можно найти здесь.</p>
<p>В качестве примера рассмотрим наш предыдущий запрос, который вычисляет наиболее распространённые типы <strong>commit-событий</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT data.commit.collection AS collection, count() AS c, uniq(data.did) AS users
FROM bluesky
WHERE kind = 'commit'
GROUP BY ALL
ORDER BY c DESC
LIMIT 10

┌─collection───────────────┬─────────c─┬───users─┐
│ app.bsky.feed.like       │ 269979403 │ 5270604 │
│ app.bsky.graph.follow    │ 150891706 │ 5631987 │
│ app.bsky.feed.post       │  46886207 │ 3083647 │
│ app.bsky.feed.repost     │  33249341 │ 1956986 │
│ app.bsky.graph.block     │   9789707 │  993578 │
│ app.bsky.graph.listitem  │   3231676 │  102020 │
│ app.bsky.actor.profile   │   1731669 │ 1280895 │
│ app.bsky.graph.listblock │  263667   │  105310 │
│ app.bsky.feed.threadgate │  215715   │   49871 │
│ app.bsky.feed.postgate   │   99625   │   19960 │
└──────────────────────────┴───────────┴─────────┘

10 rows in set. Elapsed: 6.445 sec. Processed 516.53 million rows, 45.50 GB (80.15 million rows/s., 7.06 GB/s.)
Peak memory usage: 986.51 MiB.</pre><p>Для 500 миллионов событий выполнение этого запроса занимает около 6 секунд.<br />
Чтобы преобразовать его в инкрементное материализованное представление, необходимо подготовить таблицу, которая будет получать результаты инкрементной агрегации:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.top_post_types
(
  `collection` LowCardinality(String),
  `posts` SimpleAggregateFunction(sum, UInt64),
  `users` AggregateFunction(uniq, String)
)
ENGINE = AggregatingMergeTree
ORDER BY collection</pre><p>Обратите внимание, что нам необходимо использовать <strong>AggregatingMergeTree</strong> и указать ключ сортировки как ключ группировки — результаты агрегации с одинаковыми значениями этого столбца будут объединяться.</p>
<p>Инкрементные результаты должны храниться в специальных типах столбцов <strong>SimpleAggregateFunction</strong> и <strong>AggregateFunction</strong> — для этого необходимо указать саму функцию и связанный с ней тип данных.</p>
<p>Ниже показано соответствующее материализованное представление, которое заполняет эту таблицу при вставке строк в таблицу <strong>gold</strong>. Обратите внимание, что используется суффикс &#8212; <code>State</code>, чтобы явно сгенерировать состояние агрегации:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW top_post_types_mv TO top_posts_types
AS
SELECT data.commit.collection AS collection, count() AS posts,
  uniqState(CAST(data.did, 'String')) AS users
FROM bluesky
WHERE kind = 'commit'
GROUP BY ALL

When querying this table, we use the -Merge suffix to merge aggregation states.


SELECT collection,
       sum(posts) AS posts,
       uniqMerge(users) AS users
FROM top_post_types
GROUP BY collection
ORDER BY posts DESC
LIMIT 10

10 rows in set. Elapsed: 0.042 sec.</pre><p>Производительность запроса улучшилась более чем в 150 раз!</p>
<p>Ниже приведена финальная диаграмма архитектуры, показывающая все наши уровни:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2281" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png" alt="" width="1795" height="883" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png 1795w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-300x148.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1024x504.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-768x378.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1536x756.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-450x221.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-780x384.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1600x787.png 1600w" sizes="(max-width: 1795px) 100vw, 1795px" /></a></p>
<h2>Примеры запросов и визуализации на sql.clickhouse.com</h2>
<p>Приведённый выше пример представляет собой очень простую демонстрацию. Эти данные доступны на сайте <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, где описанный выше рабочий процесс <strong>Medallion</strong> выполняется непрерывно. Мы также предоставили дополнительные материализованные представления в качестве примеров для эффективного выполнения запросов.</p>
<p>Например, чтобы определить, в какое время суток пользователи чаще всего ставят лайки, публикуют и репостят в Bluesky, можно выполнить следующий запрос:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT event, hour_of_day, sum(count) as count
FROM bluesky.events_per_hour_of_day
WHERE event in ['post', 'repost', 'like']
GROUP BY event, hour_of_day
ORDER BY hour_of_day;

72 rows in set. Elapsed: 0.007 sec.</pre><p>Запрос выполняется за 7 миллисекунд.</p>
<p>Вы можете запустить этот запрос в нашем playground, чтобы отобразить результат в виде графика.</p>
<p>Ниже приведено соответствующее материализованное представление и целевая таблица, которая заполняется по мере вставки строк в gold-таблицу:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.events_per_hour_of_day
(
    event LowCardinality(String),
    hour_of_day UInt8,
    count SimpleAggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (event, hour_of_day);


CREATE MATERIALIZED VIEW bluesky.events_per_hour_of_day_mv TO bluesky.events_per_hour_of_day
AS SELECT
    extract(data.commit.collection, '\\.([^.]+)</pre><p>Полный список запросов и соответствующих им представлений можно посмотреть здесь.<br />
Кроме того, вы можете напрямую выполнять запросы к <strong>gold</strong> или <strong>silver таблицам</strong>!</p>
<p><strong>Некоторые примеры, с которых можно начать:</strong></p>
<ul>
<li><a href="https://sql.clickhouse.com/?query_id=P2VKEOGYQVHFPA8F5IFZ2P&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Общее количество событий (Total events)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=51KVUJ5FGJUQV9XU13JKL3&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Когда пользователи чаще всего используют Bluesky (When do people use BlueSky)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=9WMMTPMMP7TAIWO5ZGWZZE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые популярные типы событий (Top event types)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=RJR6SMBYEKJSSWWUXFHP1U&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по количеству (Top event types by count)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=5C6SW7OHKEVFLRDMED2WNB&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по уникальным пользователям (Top event types by unique users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=8YAFPZQXXCGD75842UKE2W&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты (Most liked posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=7ICWGHB7HIIEWCMYFAWIAE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты о ClickHouse (Most liked posts about ClickHouse)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=BP4SSVSKXB4EJCMFHU8B6D&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые часто reposted посты (Most reposted posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=ATT83TXCQE8DUDCM84GB6E&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Самые используемые языки (Most used languages)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=H2CYZJDRCXCFYLPXMJVRCR&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные пользователи (Most liked users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=SH7GXXK4BYPHELXM5AHWDJ&amp;run_query=true&amp;tab=results">Самые часто reposted пользователи (Most reposted users)</a></li>
</ul>
<h1>Заключительные мысли</h1>
<p>В этом блоге мы продемонстрировали полностью реализованную архитектуру Medallion, построенную исключительно на ClickHouse, показав, как его мощные возможности позволяют преобразовывать “сырые”, полуструктурированные данные в качественные, готовые к запросам наборы данных.</p>
<p><strong>Через уровни Bronze, Silver и Gold</strong> мы решили типичные проблемы, такие как повреждённые данные (malformed data), несогласованность структуры и значительное количество дубликатов.<br />
Благодаря использованию типа данных JSON в ClickHouse, нам удалось эффективно обрабатывать по своей природе полуструктурированные и динамичные данные, при этом сохраняя высокую производительность.</p>
<p>Хотя эта архитектура обеспечивает надёжный и гибкий рабочий процесс, она всё же вносит определённые задержки по мере перемещения данных между слоями.<br />
В нашем решении <strong>“окна дедупликации” (deduplication windows)</strong> помогли минимизировать эти задержки, однако остаётся компромисс между скоростью доставки данных в реальном времени и качеством данных.<br />
Поэтому <strong>архитектура Medallion</strong> особенно хорошо подходит для наборов данных с высокой степенью дублирования и менее критичными требованиями к мгновенной доступности данных.</p><pre class="urvanov-syntax-highlighter-plain-tag">) AS event,
    toHour(bluesky_ts) as hour_of_day,
    count() AS count
FROM bluesky.bluesky
WHERE (kind = 'commit')
GROUP BY event, hour_of_day;</pre><p>Полный список запросов и соответствующих им представлений можно посмотреть здесь.<br />
Кроме того, вы можете напрямую выполнять запросы к <strong>gold</strong> или <strong>silver таблицам</strong>!</p>
<p><strong>Некоторые примеры, с которых можно начать:</strong></p>
<ul>
<li><a href="https://sql.clickhouse.com/?query_id=P2VKEOGYQVHFPA8F5IFZ2P&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Общее количество событий (Total events)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=51KVUJ5FGJUQV9XU13JKL3&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Когда пользователи чаще всего используют Bluesky (When do people use BlueSky)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=9WMMTPMMP7TAIWO5ZGWZZE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые популярные типы событий (Top event types)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=RJR6SMBYEKJSSWWUXFHP1U&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по количеству (Top event types by count)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=5C6SW7OHKEVFLRDMED2WNB&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по уникальным пользователям (Top event types by unique users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=8YAFPZQXXCGD75842UKE2W&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты (Most liked posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=7ICWGHB7HIIEWCMYFAWIAE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты о ClickHouse (Most liked posts about ClickHouse)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=BP4SSVSKXB4EJCMFHU8B6D&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые часто reposted посты (Most reposted posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=ATT83TXCQE8DUDCM84GB6E&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Самые используемые языки (Most used languages)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=H2CYZJDRCXCFYLPXMJVRCR&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные пользователи (Most liked users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=SH7GXXK4BYPHELXM5AHWDJ&amp;run_query=true&amp;tab=results">Самые часто reposted пользователи (Most reposted users)</a></li>
</ul>
<h1>Заключительные мысли</h1>
<p>В этом блоге мы продемонстрировали полностью реализованную архитектуру Medallion, построенную исключительно на ClickHouse, показав, как его мощные возможности позволяют преобразовывать “сырые”, полуструктурированные данные в качественные, готовые к запросам наборы данных.</p>
<p>Через уровни Bronze, Silver и Gold мы решили типичные проблемы, такие как <strong>повреждённые данные (malformed data)</strong>, несогласованность структуры и значительное количество дубликатов.<br />
Благодаря использованию типа данных JSON в ClickHouse, нам удалось эффективно обрабатывать по своей природе полуструктурированные и динамичные данные, при этом сохраняя высокую производительность.</p>
<p>Хотя эта архитектура обеспечивает надёжный и гибкий рабочий процесс, она всё же вносит определённые задержки по мере перемещения данных между слоями.</p>
<p>В нашем решении <strong>“окна дедупликации” (deduplication windows)</strong> помогли минимизировать эти задержки, однако остаётся компромисс между скоростью доставки данных в реальном времени и качеством данных.</p>
<p>Поэтому <strong>архитектура Medallion</strong> особенно хорошо подходит для наборов данных с высокой степенью дублирования и менее критичными требованиями к мгновенной доступности данных.</p>
<p>Сообщение <a href="https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/">Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Создание архитектуры Medallion с помощью ClickHouse</title>
		<link>https://datatalks.ru/medallion-architecture-with-clickhouse/</link>
					<comments>https://datatalks.ru/medallion-architecture-with-clickhouse/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Wed, 01 Jan 2025 18:34:23 +0000</pubDate>
				<category><![CDATA[ClickHouse]]></category>
		<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Bronze layer]]></category>
		<category><![CDATA[ClickPipes]]></category>
		<category><![CDATA[Gold layer]]></category>
		<category><![CDATA[Medallion architecture]]></category>
		<category><![CDATA[S3Queue]]></category>
		<category><![CDATA[Silver layer]]></category>
		<category><![CDATA[архитектура Medallion]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=527</guid>

					<description><![CDATA[<p>Перевод статьи: Building a Medallion architecture with ClickHouse Построение архитектуры Medallion с использованием ClickHouse Крупномасштабная обработка данных требует эффективной структуризации, трансформации и анализа наборов данных. Архитектура Medallion — это шаблон проектирования рабочего процесса данных для организации и повышения их качества посредством поэтапных преобразований, который широко используется для управления сложными наборами данных. Обычно она реализуется с [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/medallion-architecture-with-clickhouse/">Создание архитектуры Medallion с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>Перевод статьи:</strong> <a href="https://clickhouse.com/blog/building-a-medallion-architecture-with-clickhouse" target="_blank" rel="noopener">Building a Medallion architecture with ClickHouse</a></p>
<h1>Построение архитектуры Medallion с использованием ClickHouse</h1>
<p>Крупномасштабная обработка данных требует эффективной структуризации, трансформации и анализа наборов данных. <strong>Архитектура Medallion</strong> — это шаблон проектирования рабочего процесса данных для организации и повышения их качества посредством поэтапных преобразований, который широко используется для управления сложными наборами данных. Обычно она реализуется с помощью инструментов, таких как Spark и Delta Lake, и позволяет систематически очищать «сырой», неструктурированный набор данных до состояния, пригодного для анализа и прикладных приложений.</p>
<p>В этом посте мы исследуем, как <strong>архитектура Medallion</strong> может быть полностью реализована с использованием нативных конструкций ClickHouse, что устраняет необходимость во внешних фреймворках или инструментах. Благодаря высокой производительности запросов, поддержке широкого спектра форматов данных и встроенным функциям для управления и преобразования данных, ClickHouse можно эффективно использовать на каждом этапе этой архитектуры.</p>
<p><span style="color: #ff6600;"><strong>Цель этого поста</strong> </span>— показать, как три этапа архитектуры Medallion могут быть теоретически реализованы с помощью ClickHouse.</p>
<p><em>В следующем посте мы продемонстрируем это на практике, используя поток данных из набора Bluesky.</em> Этот набор данных включает множество типичных проблем, таких как некорректные события, высокая степень дублирования и несоответствия временных меток, что делает его подходящим для демонстрации описанных процессов.</p>
<h2>Что такое архитектура Medallion?</h2>
<p><strong>Архитектура Medallion</strong> — это широко используемый рабочий процесс обработки данных, который организует данные в многоуровневую структуру, где качество данных постепенно улучшается по мере их прохождения через этапы. Хотя она широко применяется в озёрах данных (data lake houses), эту архитектуру также можно использовать в режиме реального времени в хранилищах данных для обеспечения эффективного управления и трансформации данных.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-537 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture.jpeg" alt="" width="1639" height="644" srcset="https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture.jpeg 1639w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-300x118.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-1024x402.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-768x302.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-1536x604.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-450x177.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-780x306.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/what_is_medallion_architecture-1600x629.jpeg 1600w" sizes="(max-width: 1639px) 100vw, 1639px" /></a></p>
<p>Архитектура включает три слоя (или этапа), каждый из которых выполняет определённые задачи в процессе обработки данных:</p>
<h3><strong>Bronze слой</strong></h3>
<p>Этот слой служит зоной приёма для сырых, необработанных данных непосредственно из исходной системы — своего рода «зона промежуточного хранения». Данные хранятся в их оригинальной структуре с минимальными преобразованиями и дополнительными метаданными. Этот слой оптимизирован для быстрой загрузки данных и может служить историческим архивом исходных данных, которые всегда доступны для повторной обработки или отладки.</p>
<p><strong>Следует ли хранить все данные в бронзовом слое — вопрос спорный.</strong> Некоторые пользователи предпочитают фильтровать данные и применять преобразования, например, упрощение JSON, переименование полей или отсеивание некорректных данных. Мы не занимаем жёсткую позицию, но рекомендуем оптимизировать хранение для использования только серебряным слоем, а не другими потребителями.</p>
<h3><strong>Silver слой</strong></h3>
<p><strong>На этом этапе данные очищаются, дублирующиеся записи удаляются, и они приводятся к единой схеме.</strong> Сырые данные из бронзового слоя обогащаются и преобразуются для получения более точного и согласованного представления. Данные на этом этапе становятся пригодными для использования в масштабах предприятия, например, для задач машинного обучения и аналитики. Модель данных должна формироваться на этом уровне с акцентом на согласованность первичных и внешних ключей для упрощения последующих соединений.</p>
<p>Хотя это не является стандартом, приложения и конечные потребители могут обращаться к этому слою. Обычно это бизнес-приложения, которым требуется весь очищенный набор данных, например, для рабочих процессов машинного обучения. Важно отметить, что качество данных не улучшится после этого этапа, только удобство их эффективного запроса.</p>
<h3><strong>Gold слой</strong></h3>
<p><strong>Этот слой содержит полностью подготовленные, бизнес-ориентированные и проектно-специфические наборы данных, которые делают данные более доступными (и производительными) для конечных пользователей.</strong> Эти наборы данных часто денормализуются или предварительно агрегируются для оптимальной производительности при чтении и могут быть составлены из нескольких таблиц предыдущего серебряного слоя. Здесь внимание сосредоточено на применении окончательных преобразований и обеспечении высочайшего качества данных для потребления конечными пользователями или приложениями, такими как отчётность и пользовательские дашборды.</p>
<p>Этот многоуровневый подход к обработке данных нацелен на эффективное решение таких проблем, как качество данных, дублирование и несоответствия в схемах. Постепенно преобразуя необработанные данные, архитектура Medallion обеспечивает чёткую родословную данных и их последовательное улучшение, чтобы они были готовы к анализу или операционному использованию.</p>
<p>Хотя мы считаем, что название &#171;архитектура медальонов&#187; могло бы лучше отражать содержание её уровней, полезные процессы и дисциплина, которую она способствует, делают её ценной.</p>
<h2>Архитектура Medallion с использованием ClickHouse</h2>
<p>В этом разделе мы предлагаем, как каждый уровень архитектуры Medallion может быть реализован с помощью ClickHouse, и как встроенные функции системы могут быть использованы для передачи данных между уровнями. Этот подход является гибким и развивается на основе нашего внутреннего опыта и отзывов пользователей. Мы будем рады предложениям, которые помогут улучшить эти практики.</p>
<h3><strong>Bronze слой с использованием ClickHouse</strong></h3>
<p>Bronze слой служит точкой входа для сырых, необработанных данных, оптимизированных для высокоскоростной загрузки с использованием гибких и производительных конструкций ClickHouse. Этот слой может также выполнять функцию исторического архива, сохраняя необработанные данные для их родословной, отладки или повторной обработки без необходимости в полном очищении или удалении дубликатов на начальном этапе. Такой акцент на производительность и гибкость создаёт надёжную основу для дальнейшей обработки и преобразования данных на следующих этапах.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_bronze_layer.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-539 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_bronze_layer.jpeg" alt="" width="754" height="777" srcset="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_bronze_layer.jpeg 754w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_bronze_layer-291x300.jpeg 291w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_bronze_layer-450x464.jpeg 450w" sizes="(max-width: 754px) 100vw, 754px" /></a></p>
<p>Основные характеристики Bronze слоя при использовании ClickHouse.</p>
<h4><strong>Загрузка данных из источников</strong></h4>
<p>Данные могут загружаться в этот слой напрямую через клиенты, ELT-инструменты, такие как Fivetran, или потоки из Kafka с использованием ClickPipes или коннектора Kafka для ClickHouse. В ClickHouse Cloud доступны дополнительные возможности для построчного чтения данных из S3-хранилищ через S3Queue и ClickPipes, поддерживающих более 70 форматов данных (включая сжатые), таких как Parquet и форматы для озёр данных, например Iceberg. Подход с использованием S3 как зоны промежуточного хранения особенно часто применяется при обработке больших полуструктурированных данных с менее согласованной схемой.</p>
<h4><strong>Оптимизация для быстрой вставки</strong></h4>
<p>Bronze слой, как правило, реализуется с использованием MergeTree, который разработан для эффективной обработки быстрых вставок. Схема таблицы и ключ упорядочивания настраиваются для оптимизации операций вставки, а также для обеспечения высокой производительности чтения в случае необходимости повторной обработки данных (для Silver слоя) или их анализа на предмет проблем с качеством. Учитывая, что этот слой не предназначен для конечных потребителей, мы рекомендуем оптимизировать ключ упорядочивания для эффективного полного сканирования данных, например, использовать ключ, соответствующий порядку чтения — обычно по времени.</p>
<h4><strong>Поддержка полуструктурированных данных в формате JSON</strong></h4>
<p>Новый тип JSON в ClickHouse является ключевой функцией для обработки полуструктурированных данных в Bronze слое. Этот тип позволяет загружать данные с динамическими и непредсказуемыми схемами без необходимости строгого соблюдения структуры на этапе загрузки, что особенно ценно для наборов данных с несогласованными или изменяющимися структурами. Благодаря поддержке динамического JSON Bronze слой становится эффективной зоной приёма необработанных данных, способной обрабатывать сценарии, где согласованность схемы не гарантирована. Обратите внимание, что тип JSON позволяет применять фильтры с контролем, какие пути объектов сохраняются. Пользователи также могут отфильтровывать некорректные JSON перед сохранением в этом слое и, при необходимости, упрощать сложные структуры.</p>
<p>Использование этого типа данных не ограничивается Bronze слоем, так как он может быть полезен и на других уровнях, например, для колонок с динамическими схемами, таких как пользовательские теги.</p>
<h4><strong>Материализованные колонки для базовой обработки</strong></h4>
<p>Материализованные колонки предоставляют мощный механизм для извлечения и преобразования определённых полей во время загрузки данных. Хотя их возможности ограничены, они позволяют эффективно обрабатывать JSON-данные, создавая производные колонки для часто запрашиваемых атрибутов. Такой подход особенно полезен, если JSON-данные включают нерегулярные пути или структуры, что позволяет выполнять базовую предварительную обработку без необходимости полного соблюдения схемы. Эти извлечённые колонки также часто используются для последующей фильтрации.</p>
<h4><strong>Партиционирование и управление хранением данных</strong></h4>
<p>Таблицы Bronze слоя могут быть разделены на партиции для оптимизации производительности запросов и обеспечения эффективного управления данными. Рекомендуется использовать правила TTL для автоматического удаления устаревших данных, которые больше не нужны, что способствует соответствию требованиям и эффективному использованию хранилища.</p>
<h3><strong>Silver слой с использованием ClickHouse</strong></h3>
<p>Silver слой представляет следующий этап в рабочем процессе Medallion, преобразуя сырые данные из Bronze слоя в более согласованную и структурированную форму. Этот уровень решает проблемы качества данных, такие как фильтрация некорректных строк, стандартизация схем и выполнение преобразований. В ClickHouse обычно используется прямое сопоставление таблиц Bronze слоя с их эквивалентами в Silver, но с очищенным и обогащённым набором данных, который служит основой для дальнейшей обработки в Gold слое.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-541 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer.jpeg" alt="" width="1387" height="797" srcset="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer.jpeg 1387w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer-300x172.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer-1024x588.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer-768x441.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer-450x259.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_silver_layer-780x448.jpeg 780w" sizes="(max-width: 1387px) 100vw, 1387px" /></a></p>
<h4><strong>Инкрементальные материализованные представления</strong></h4>
<p>Silver слой, как правило, заполняется с использованием инкрементальных материализованных представлений, привязанных к таблицам Bronze слоя. Эти представления выполняют запросы на новых блоках данных, добавленных в Bronze слой, применяя фильтрацию, преобразования и нормализацию схемы перед записью результатов в таблицы Silver слоя для сохранения. Такие представления позволяют эффективно и непрерывно преобразовывать данные, а при использовании нескольких представлений пользователи могут создавать разные версии данных, каждая из которых предназначена для определённых таблиц Silver слоя с учётом конкретных задач. Кроме того, некорректные или необрабатываемые строки могут быть перенаправлены в очередь &#171;мертвых писем&#187; (dead letter queue) через отдельные материализованные представления, что позволяет их проверить и, возможно, восстановить, не засоряя основной набор данных.</p>
<h4><strong>Обработка дублирования и CDC</strong></h4>
<p>Для сценариев, требующих удаления дубликатов или обработки потоков <strong>Change Data Capture (CDC)</strong>, можно использовать движок таблиц <code>ReplacingMergeTree</code>. Ключ упорядочивания этого движка используется для выполнения удаления дубликатов (с уникальными наборами значений, идентифицирующими строку), причём обновления обрабатываются как версионные вставки — это особенно полезно в сценариях CDC. Обратите внимание, что <code>ReplacingMergeTree</code> выполняет удаление дубликатов во время слияния данных, поэтому он обеспечивает только возможную согласованность, требуя использования оператора FINAL при запросах, чтобы гарантировать отсутствие дубликатов в результатах. В общем случае мы рекомендуем конечным приложениям осторожно обращаться к этому слою, так как использование FINAL может значительно увеличить время выполнения запросов.</p>
<h4><strong>Партиционирование и управление хранением данных</strong></h4>
<p>Аналогично Bronze слою, таблицы <strong>Silver слоя</strong> могут быть разделены на партиции для оптимизации производительности запросов и управления данными с использованием TTL. Партиционирование также может улучшить производительность при чтении из таблиц <code>ReplacingMergeTree</code> с использованием <code>FINAL</code>. Мы рекомендуем следовать лучшим практикам, таким как оптимизация операций слияния, чтобы поддерживать высокую производительность. Так как этот слой не является долгосрочным архивом и может не использоваться в качестве источника для последующих этапов, данные могут храниться в этом слое более короткий период, чем в <strong>Bronze</strong> и <strong>Gold слоях</strong>, с более короткими интервалами партиционирования.</p>
<h3><strong>Gold слой с использованием ClickHouse</strong></h3>
<p><strong>Gold слой</strong> представляет заключительный этап <strong>архитектуры Medallion</strong>, где данные преобразуются в полностью денормализованные, готовые для бизнеса наборы данных, оптимизированные для использования конечными приложениями и аналитикой. Этот слой заполняется из <strong>Silver слоя</strong> с использованием обновляемых материализованных представлений, выполняющих сложные преобразования, включая соединения и агрегации. Это обеспечивает максимально удобную для использования форму данных, сводя к минимуму необходимость в объединениях и даже агрегациях на этапе выполнения запросов. Таблицы <strong>Gold слоя</strong> разрабатываются для обеспечения высокой производительности, поддерживая конечные приложения с минимальной задержкой и максимальной эффективностью.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-543 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer.jpeg" alt="" width="1845" height="654" srcset="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer.jpeg 1845w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-300x106.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-1024x363.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-768x272.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-1536x544.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-450x160.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-780x276.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_gold_layer-1600x567.jpeg 1600w" sizes="(max-width: 1845px) 100vw, 1845px" /></a></p>
<h4><strong>Обновляемые материализованные представления</strong></h4>
<p>В отличие от инкрементальных материализованных представлений, используемых для заполнения Silver слоя, Gold слой заполняется с помощью обновляемых материализованных представлений. Эти представления выполняются периодически для таблиц Silver слоя и позволяют выполнять продвинутые преобразования, такие как сложные объединения, денормализуя данные перед их записью в таблицы Gold слоя. При использовании данных из таблиц Silver слоя с движком ReplacingMergeTree, эти представления могут выполнять запросы с оператором FINAL, чтобы гарантировать вставку полностью очищенных от дубликатов данных в Gold слой. Такой подход обеспечивает наивысшее качество данных, сохраняя при этом гибкость для выполнения сложных запросов.</p>
<h4><strong>Проектирование таблиц для конечных приложений</strong></h4>
<p>Таблицы в Gold слое обычно реализуются с использованием стандартных таблиц <code>MergeTree</code>, где ключи упорядочивания оптимизируются специально под шаблоны доступа конечных приложений. Эти денормализованные наборы данных структурированы таким образом, чтобы минимизировать необходимость в дополнительных объединениях, что позволяет приложениям выполнять быстрые и эффективные запросы. Адаптация схемы и ключей к требованиям пользовательских запросов обеспечивает бесшовную интеграцию с инструментами отчётности, аналитическими панелями и интерактивными интерфейсами.</p>
<h4><strong>Инкрементальные материализованные представления для предвычисленных агрегаций</strong></h4>
<p>Помимо хранения денормализованных наборов данных, Gold слой часто включает инкрементальные материализованные представления для предвычисления агрегаций. Эти представления выполняют запросы с использованием <code>GROUP BY</code> для новых вставок в таблицы Gold слоя, записывая промежуточные результаты агрегации в целевые таблицы с использованием движка <code>AggregatingMergeTree</code>. Перенос вычислений с этапа выполнения запроса на этап вставки значительно снижает задержки запросов. Конечные запросы, в свою очередь, нуждаются только в объединении меньших промежуточных состояний, что делает их высокопроизводительными и подходящими для поддержки пользовательских приложений с богатыми возможностями фильтрации и агрегации.</p>
<p>Наше демонстрационное приложение <code>ClickPy</code> активно использует материализованные представления для предоставления возможностей визуализации и фильтрации данных по набору Python PYPI, содержащему триллион строк. Подробности доступны в репозитории ClickPy на GitHub.</p>
<h2>Полная архитектура Medallion с использованием ClickHouse</h2>
<p>Объединив все описанные выше этапы, мы получаем полную <strong>архитектуру Medallion</strong>, реализованную с помощью <strong>ClickHouse</strong>:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-544 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema.jpeg" alt="" width="1719" height="614" srcset="https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema.jpeg 1719w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-300x107.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-1024x366.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-768x274.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-1536x549.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-450x161.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-780x279.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/clickhouse_medallion_architecture_full_schema-1600x571.jpeg 1600w" sizes="(max-width: 1719px) 100vw, 1719px" /></a></p>
<p><strong>Эта архитектура Medallion</strong>, реализованная на основе <strong>ClickHouse</strong>, предлагает структурированный подход к управлению конвейерами данных через каскадные преобразования. Благодаря поддержке более чем 70 форматов файлов, сырые данные могут быть непосредственно загружены в <strong>Bronze слой</strong> с использованием функций <code>s3Queue</code> или <code>ClickPipe</code> (в ClickHouse Cloud), после чего они инкрементально очищаются и обогащаются в Silver слое, а затем подготавливаются в <strong>Gold слое</strong> для оптимального использования в приложениях и аналитике. Используя таблицы <code>MergeTree</code> <strong>ClickHouse</strong>, материализованные представления (инкрементальные/обновляемые) и поддержку различных форматов файлов, эта архитектура позволяет организовать процесс загрузки, преобразования и доставки данных без необходимости использования сторонних инструментов.</p>
<p>Обратите внимание, что пользователи не обязаны развертывать все три этапа этой архитектуры и могут исключить любой из слоев. Например, можно пропустить <strong>Silver слой</strong>, если данные уже предоставляются с минимальными проблемами качества и без дубликатов.</p>
<p>Хотя преимущества здесь весьма убедительны, с чёткой методологией и набором инструментов для предоставления чистых, оптимизированных данных конечным пользователям и приложениям, у данной архитектуры есть и некоторые недостатки.</p>
<p>Таблицы <strong>Gold слоя</strong> по своей природе представляют одни и те же данные в разных формах, каждая из которых оптимизирована для потребляющего её приложения. Это приводит к необходимости дублирования данных. Однако связанные с этим затраты на репликацию можно снизить, используя объектное хранилище для таблиц <code>MergeTree</code> с разделением хранения и вычислений.</p>
<p>Кроме того, архитектура требует управления несколькими уровнями, что добавляет сложности в конвейеры данных. Это требует мониторинга, который можно осуществить с помощью системных таблиц <strong>ClickHouse</strong>, предоставляющих видимость состояния материализованных представлений и процессов миграции данных. Также инструменты, такие как <strong>Grafana</strong>, могут уведомлять о проблемах, таких как сбои представлений или несоответствия данных, благодаря источнику данных <strong>ClickHouse</strong>.</p>
<p>Наиболее сложной для устранения является присущая данной архитектуре задержка в доступности данных из-за необходимости последовательного перемещения данных через каждый слой. В результате эту архитектуру сложнее оптимизировать для использования в реальном времени, где критична оперативная доступность данных.</p>
<h1>Заключительные мысли и вывод</h1>
<p><strong>Архитектура Medallion с использованием ClickHouse</strong> демонстрирует мощный, автономный подход к управлению рабочими процессами данных, обеспечивая загрузку, преобразование и потребление данных. Используя встроенные возможности <strong>ClickHouse</strong>, организации могут строить эффективные, масштабируемые конвейеры, которые предоставляют чистые и оптимизированные наборы данных для аналитики и приложений.</p>
<p>Отличительной особенностью реализации на основе <strong>ClickHouse</strong> является её автономный подход. Весь процесс — от загрузки данных до их преобразования и потребления — происходит нативно в ClickHouse без необходимости использования сторонних инструментов.</p>
<p>В нашем следующем блоге мы покажем шаги для практического развертывания архитектуры Medallion для данных Bluesky, размещённых на sql.clickhouse.com, где вы сможете исследовать и запрашивать каждый уровень архитектуры напрямую.</p>
<p>Сообщение <a href="https://datatalks.ru/medallion-architecture-with-clickhouse/">Создание архитектуры Medallion с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/medallion-architecture-with-clickhouse/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
