<?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>Data Architecture / Data Modeling - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/category/data-architecture/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/category/data-architecture/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Sat, 11 Oct 2025 09:23:21 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>Data Architecture / Data Modeling - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/category/data-architecture/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Глава 5. Кодирование и Эволюция (Encoding and Evolution)</title>
		<link>https://datatalks.ru/chapter-5-encoding-and-evolution/</link>
					<comments>https://datatalks.ru/chapter-5-encoding-and-evolution/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 06 Sep 2025 17:15:26 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Designing Data-Intensive Applications]]></category>
		<category><![CDATA[Encoding and Evolution]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2113</guid>

					<description><![CDATA[<p>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта Глава 5. Кодирование и эволюция Всё изменяется и ничто не стоит на месте. Гераклит Эфесский, цитата у Платона в «Кратиле» (360 г. до н. э.) Приложения неизбежно со временем меняются. Новые фичи добавляются или модифицируются, когда запускаются новые продукты, пользовательские требования становятся лучше понятны [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-5-encoding-and-evolution/">Глава 5. Кодирование и Эволюция (Encoding and Evolution)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта</em></p>
<h1>Глава 5. Кодирование и эволюция</h1>
<blockquote><p>Всё изменяется и ничто не стоит на месте.<br />
<strong>Гераклит Эфесский, цитата у Платона в «Кратиле» (360 г. до н. э.)</strong></p></blockquote>
<hr />
<p>Приложения неизбежно со временем меняются. Новые фичи добавляются или модифицируются, когда запускаются новые продукты, пользовательские требования становятся лучше понятны или меняются бизнес-обстоятельства. В Главе 2 мы ввели понятие <strong>Эволюционности (Расширяемости)</strong>: мы должны стремиться строить системы так, чтобы адаптация к изменениям была максимально простой.</p>
<p>В большинстве случаев изменение функциональности приложения также требует изменения данных, которые оно хранит: возможно, нужно захватить новое поле или тип записи, или, возможно, существующие данные должны быть представлены по-новому.</p>
<p>Модели данных по-разному справляются с такими изменениями. <strong>Реляционные базы данных</strong> обычно предполагают, что все данные в базе соответствуют одной схеме: хотя эта схема может быть изменена (через миграции схемы, т. е. операторы ALTER), в любой момент времени действует ровно одна схема. Для сравнения, <strong>базы данных со схемой «на чтение» (schema-on-read, «безсхемные»)</strong> схему не навязывают, поэтому база может содержать смесь старых и новых форматов данных, записанных в разное время (см. «Гибкость схемы в документной модели»).</p>
<p>Когда формат данных или схема меняется, часто требуется соответствующее изменение в коде приложения (например, вы добавляете новое поле в запись, и код приложения начинает читать и записывать это поле). Однако в большом приложении изменения в коде не всегда могут быть выполнены мгновенно:</p>
<p>В серверных приложениях вы можете захотеть выполнить <strong>rolling upgrade</strong> (также называемый <strong>staged rollout</strong>) — выкатывать новую версию на несколько нод за раз, проверять, что новая версия работает стабильно, и постепенно проходить через все ноды. Это позволяет деплоить новые версии без даунтайма сервиса и таким образом стимулирует более частые релизы и лучшую эволюционность (расширяемость).</p>
<p>В клиентских приложениях вы полностью зависите от пользователя, который может не установить обновление ещё какое-то время.</p>
<p>Это означает, что старые и новые версии кода, а также старые и новые форматы данных потенциально могут одновременно сосуществовать в системе. Чтобы система продолжала работать корректно, необходимо поддерживать совместимость в обоих направлениях:</p>
<ul>
<li><strong>Обратная совместимость</strong><br />
Новый код может читать данные, которые были записаны старым кодом.</li>
<li><strong>Прямая совместимость</strong><br />
Старый код может читать данные, которые были записаны новым кодом.</li>
</ul>
<p><strong>Обратная совместимость обычно несложно достигается:</strong> как автор нового кода вы знаете формат данных, записанных старым кодом, и можете явно обрабатывать их (при необходимости просто сохранив старый код для чтения старых данных). Прямая совместимость может быть более сложной, так как она требует, чтобы старый код игнорировал добавления, сделанные более новой версией кода.</p>
<p>Ещё одна проблема с прямой совместимостью проиллюстрирована на рисунке 5-1. Допустим, вы добавляете поле в схему записи, и новый код создаёт запись с этим новым полем и сохраняет её в базу. Затем более старая версия кода (которая ещё не знает о новом поле) читает эту запись, обновляет её и записывает обратно. В этой ситуации желаемым поведением обычно является сохранение старым кодом нового поля нетронутым, даже если оно не может быть интерпретировано. Но если запись декодируется в объект модели, который явно не сохраняет неизвестные поля, данные могут быть утеряны — как на рисунке 5-1.</p>
<p><strong>Рисунок 5-1. Когда более старая версия приложения обновляет данные, ранее записанные более новой версией приложения, данные могут быть потеряны, если не быть осторожным</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2188" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png" alt="" width="2560" height="1391" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-300x163.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1024x556.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-768x417.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1536x835.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-2048x1113.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-450x245.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-780x424.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1600x869.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>В этой главе мы рассмотрим несколько форматов кодирования данных, включая <code>JSON</code>, <code>XML</code>, <code>Protocol Buffers</code> и <code>Avro</code>. В частности, мы посмотрим, как они обрабатывают изменения схем и как они поддерживают системы, в которых старые и новые данные и код должны сосуществовать. Затем мы обсудим, как эти форматы используются для хранения данных и для коммуникации: в базах данных, веб-сервисах, REST API, удалённых вызовах процедур (RPC), движках workflow и системах, основанных на событиях, таких как акторы и очереди сообщений.</p>
<h1>Форматы кодирования данных</h1>
<p>Программы обычно работают с данными в (как минимум) двух различных представлениях:</p>
<ol>
<li>В памяти данные хранятся в объектах, структурах (structs), списках, массивах, хэш-таблицах, деревьях и т. д. Эти структуры данных оптимизированы для эффективного доступа и манипуляций со стороны CPU (обычно с использованием указателей).</li>
<li>Когда вы хотите записать данные в файл или отправить их по сети, необходимо закодировать их как некоторую самодостаточную последовательность байт (например, JSON-документ). Так как указатель не имеет смысла для любого другого процесса, это представление в виде последовательности байт зачастую выглядит совсем иначе, чем структуры данных, которые обычно используются в памяти.</li>
</ol>
<p>Таким образом, необходим некий перевод между двумя представлениями. Преобразование из представления в памяти в последовательность байт называется <strong>кодированием</strong> (также известно как <strong>сериализация</strong> или <strong>маршаллинг</strong>), а обратное преобразование — <strong>декодированием</strong> (<strong>парсинг</strong>, <strong>десериализация</strong>, <strong>анмаршаллинг</strong>).</p>
<hr />
<p><strong>КОНФЛИКТ ТЕРМИНОВ</strong></p>
<p><strong>Термин «сериализация»</strong> к сожалению также используется в контексте транзакций, но с совершенно иным значением. Чтобы избежать перегрузки слова, в этой книге мы будем придерживаться термина «кодирование», хотя «сериализация» является, возможно, более распространённым термином.</p>
<hr />
<p>Есть исключения, когда <strong>кодирование/декодирование</strong> не требуется — например, когда база данных работает напрямую с сжатыми данными, загруженными с диска, как обсуждается в разделе «Выполнение запросов: компиляция и векторизация». Существуют также <strong>zero-copy форматы данных</strong>, которые спроектированы для использования как во время выполнения, так и на диске/в сети без явного шага преобразования, такие как <strong>Cap’n Proto</strong> и <strong>FlatBuffers</strong>.</p>
<p>Однако большинство систем нуждаются в преобразовании между объектами в памяти и плоскими последовательностями байт. Так как это настолько распространённая задача, существует множество различных библиотек и форматов кодирования на выбор. Давайте сделаем краткий обзор.</p>
<h2>Языко-специфичные форматы</h2>
<p>Многие языки программирования поставляются со встроенной поддержкой кодирования объектов из памяти в последовательности байт. Например, в <strong>Java</strong> это <code>java.io.Serializable</code>, в <strong>Python</strong> — <code>pickle</code>, в <strong>Ruby</strong> — <code>Marshal</code> и т. д. Также существует множество сторонних библиотек, например <code>Kryo</code> для <strong>Java</strong>.</p>
<p>Эти библиотеки кодирования очень удобны, так как позволяют сохранять и восстанавливать объекты из памяти с минимальным дополнительным кодом. Однако у них есть несколько серьёзных проблем:</p>
<ul>
<li>Кодирование часто привязано к конкретному языку программирования, и чтение данных на другом языке крайне затруднительно. Если вы храните или передаёте данные в таком кодировании, вы фактически связываете себя с текущим языком программирования на очень долгое время и исключаете возможность интеграции ваших систем с системами других организаций (которые могут использовать другие языки).</li>
<li>Чтобы восстановить данные в тех же типах объектов, процесс декодирования должен уметь инстанцировать произвольные классы. Это часто является источником проблем с безопасностью: если злоумышленник сможет заставить ваше приложение декодировать произвольную последовательность байт, он сможет инстанцировать произвольные классы, что в свою очередь зачастую позволяет делать ужасные вещи, такие как удалённое выполнение произвольного кода.</li>
<li>Версионирование данных часто является второстепенной задачей в этих библиотеках: так как они предназначены для быстрого и лёгкого кодирования данных, они часто пренебрегают неудобными проблемами прямой и обратной совместимости.</li>
<li>Эффективность (время CPU на кодирование или декодирование, а также размер закодированной структуры) также часто является второстепенной. Например, встроенная сериализация Java печально известна своей низкой производительностью и раздутым кодированием.</li>
</ul>
<p>По этим причинам, как правило, плохая идея использовать встроенное в язык кодирование для чего-либо, кроме очень временных целей.</p>
<h2>JSON, XML и бинарные варианты</h2>
<p>При переходе на стандартизированные кодирования, которые могут быть записаны и прочитаны многими языками программирования, <strong>JSON</strong> и <strong>XML</strong> — очевидные претенденты. Они широко известны, широко поддерживаются и почти так же широко нелюбимы. <strong>XML</strong> часто критикуют за излишнюю многословность и ненужную сложность. Популярность <strong>JSON</strong> в основном связана с его встроенной поддержкой в веб-браузерах и простотой по сравнению с XML. <strong>CSV</strong> — ещё один популярный формат, независимый от языка, но он поддерживает только табличные данные без вложенности.</p>
<p><code>JSON</code>, <code>XML</code> и <code>CSV</code> являются текстовыми форматами и, таким образом, в какой-то степени человекочитаемыми (хотя синтаксис — популярная тема для дискуссий). Помимо поверхностных синтаксических проблем, у них есть также более тонкие недостатки:</p>
<ul>
<li>Существует много неоднозначностей вокруг кодирования чисел. В XML и CSV невозможно различить число и строку, состоящую из цифр (кроме как ссылаясь на внешнюю схему). JSON различает строки и числа, но не различает целые и числа с плавающей точкой и не указывает точность.<br />
Это становится проблемой при работе с большими числами; например, целые числа больше <strong style="font-size: revert;">2⁵³</strong><span style="font-size: revert;"> не могут быть точно представлены в числе с плавающей точкой двойной точности </span><strong style="font-size: revert;">IEEE 754</strong><span style="font-size: revert;">, поэтому такие числа становятся неточными при парсинге в языке, который использует числа с плавающей точкой, таком как JavaScript. Пример чисел больше </span><strong style="font-size: revert;">2⁵³</strong><span style="font-size: revert;"> встречается в X (ранее Twitter), где для идентификации каждого поста используется </span><strong style="font-size: revert;">64-битное число</strong><span style="font-size: revert;">. JSON, возвращаемый API, включает идентификаторы постов дважды: один раз как JSON-число и один раз как десятичную строку, чтобы обойти тот факт, что числа некорректно парсятся приложениями на JavaScript.</span></li>
</ul>
<ul>
<li><strong>JSON</strong> и <strong>XML</strong> имеют хорошую поддержку строк символов в Unicode (т. е. человекочитаемого текста), но они не поддерживают бинарные строки (последовательности байт без кодировки символов). Бинарные строки — полезная фича, поэтому люди обходят это ограничение, кодируя бинарные данные в текст с помощью Base64. Схема затем используется для указания, что значение должно интерпретироваться как Base64-кодированное. Это работает, но выглядит несколько костыльно и увеличивает размер данных на 33%.</li>
<li><strong>XML Schema</strong> и <strong>JSON Schema</strong> мощные, и поэтому довольно сложные для изучения и реализации. Так как правильная интерпретация данных (например, чисел и бинарных строк) зависит от информации в схеме, приложения, которые не используют схемы XML/JSON, потенциально должны хардкодить соответствующую логику кодирования/декодирования.</li>
<li>У <strong>CSV</strong> вообще нет схемы, поэтому приложение должно само определять значение каждой строки и каждого столбца. Если изменение в приложении добавляет новую строку или столбец, необходимо обрабатывать это изменение вручную. CSV также является довольно размытым форматом (что произойдёт, если значение содержит запятую или символ новой строки?). Хотя его правила экранирования формально задокументированы, не все парсеры корректно их реализуют.</li>
</ul>
<p>Несмотря на эти недостатки, <strong>JSON</strong>, <strong>XML</strong> и <strong>CSV</strong> достаточно хороши для многих целей. Скорее всего, они останутся популярными, особенно как форматы обмена данными (т. е. для отправки данных от одной организации другой). В таких ситуациях, пока стороны согласны относительно формата, часто не имеет значения, насколько красив или эффективен этот формат. Сложность заставить разные организации договориться хоть о чём-то перевешивает большинство других соображений.</p>
<h3>JSON Schema</h3>
<p><strong>JSON Schema</strong> получил широкое распространение как способ моделирования данных всякий раз, когда они обмениваются между системами или записываются в хранилище. Вы найдёте JSON-схемы в веб-сервисах (см. «Web services») как часть спецификации веб-сервисов OpenAPI, в регистрах схем, таких как <strong>Confluent Schema Registry</strong> и <strong>Red Hat Apicurio Registry</strong>, а также в базах данных, таких как расширение валидатора <code>pg_jsonschema</code> в <strong>PostgreSQL</strong> и синтаксис валидатора <code>$jsonSchema</code> в <strong>MongoDB</strong>.</p>
<p><strong>Спецификация JSON Schema</strong> предлагает ряд возможностей. Схемы включают стандартные примитивные типы, включая строки, числа, целые, объекты, массивы, булевы значения или null. Но <strong>JSON Schema</strong> также предоставляет отдельную спецификацию валидации, которая позволяет разработчикам накладывать ограничения на поля. Например, поле порта может иметь минимум 1 и максимум <code>65535</code>.</p>
<p><strong>JSON Schema</strong> может иметь либо открытую, либо закрытую модель содержимого. Открытая модель содержимого допускает существование любых полей, не определённых в схеме, с любыми типами данных, тогда как закрытая модель содержимого разрешает только явно определённые поля. Открытая модель содержимого в JSON Schema включена, когда <code>additionalProperties</code> установлено в <code>true</code>, что является значением по умолчанию. Таким образом, JSON Schema обычно является определением того, что не разрешено (а именно, недопустимые значения для любых определённых полей), а не того, что разрешено в схеме.</p>
<p>Открытые модели содержимого мощные, но могут быть сложными. Например, предположим, вы хотите определить отображение от целых чисел (например, ID) к строкам. JSON не имеет типа «map» или «dictionary», только тип «object», который может содержать строковые ключи и значения любого типа. Вы можете затем ограничить этот тип с помощью JSON Schema так, чтобы ключи могли содержать только цифры, а значения могли быть только строками, используя <code>patternProperties</code> и <code>additionalProperties</code>, как показано в Примере 5-1.</p>
<p><strong>Пример 5-1. Пример JSON Schema с целочисленными ключами и строковыми значениями.</strong> Целочисленные ключи представлены как строки, содержащие только целые числа, так как JSON Schema требует, чтобы все ключи были строками.</p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "patternProperties": {
    "^[0-9]+$": {
      "type": "string"
    }
  },
  "additionalProperties": false
}</pre><p>В дополнение к открытым и закрытым моделям содержимого и валидаторам, JSON Schema поддерживает условную if/else-логику схемы, именованные типы, ссылки на удалённые схемы и многое другое. Всё это делает язык схем очень мощным. Такие возможности также делают определения громоздкими. Может быть сложно разрешать удалённые схемы, рассуждать об условных правилах или эволюционировать схемы в направлении прямой или обратной совместимости. Подобные проблемы применимы и к XML Schema.</p>
<h3>Бинарное кодирование</h3>
<p><strong>JSON</strong> менее многословен, чем <strong>XML</strong>, но оба всё равно занимают много места по сравнению с бинарными форматами. Это наблюдение привело к разработке множества бинарных кодирований для <strong>JSON</strong> (<code>MessagePack</code>, <code>CBOR</code>, <code>BSON</code>, <code>BJSON</code>, <code>UBJSON</code>, <code>BISON</code>, <code>Hessian</code> и <code>Smile</code>, например) и для <strong>XML</strong> (<code>WBXML</code> и <code>Fast Infoset</code>, к примеру). Эти форматы были приняты в различных нишах, так как они более компактны и иногда быстрее парсятся, но ни один из них не получил такого широкого распространения, как текстовые версии <strong>JSON</strong> и <strong>XML</strong>.</p>
<p>Некоторые из этих форматов расширяют набор типов данных (например, различают целые числа и числа с плавающей точкой или добавляют поддержку бинарных строк), но в остальном они сохраняют модель данных JSON/XML неизменной. В частности, так как они не предписывают схему, они должны включать все имена полей объекта внутри закодированных данных. То есть, в бинарном кодировании JSON-документа из Примера 5-2 им придётся где-то включить строки <code>userName</code>, <code>favoriteNumber</code> и <code>interests</code>.</p>
<p><strong>Пример 5-2. Пример записи, которую мы будем кодировать в нескольких бинарных форматах в этой главе</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}</pre><p>Давайте посмотрим на пример <strong>MessagePack</strong>, бинарного кодирования для JSON. На рисунке 5-2 показана последовательность байт, которую вы получите, если закодируете JSON-документ из Примера 5-2 с помощью <strong>MessagePack</strong>. Первые несколько байт выглядят так:</p>
<ul>
<li>Первый байт, <code>0x83</code>, указывает, что далее идёт объект (старшие четыре бита = <code>0x80</code>) с тремя полями (младшие четыре бита = <code>0x03</code>). (Если вы задаётесь вопросом, что происходит, если объект содержит больше 15 полей, так что число полей не помещается в четыре бита, тогда используется другой индикатор типа, а число полей кодируется в двух или четырёх байтах.)</li>
<li>Второй байт, <code>0xa8</code>, указывает, что далее идёт строка (старшие четыре бита = <code>0xa0</code>), которая имеет длину восемь байт (младшие четыре бита = <code>0x08</code>).</li>
<li>Следующие восемь байт — это имя поля <code>userName</code> в <strong>ASCII</strong>. Так как длина была указана ранее, нет необходимости в каком-либо маркере, чтобы сказать нам, где строка заканчивается (или в экранировании).</li>
<li>Следующие семь байт кодируют шестибуквенное строковое значение <strong>Martin</strong> с префиксом <code>0xa6</code>, и так далее.</li>
</ul>
<p>Бинарное кодирование имеет длину 66 байт, что всего лишь немного меньше, чем 81 байт, занимаемый текстовым <strong>JSON-кодированием</strong> (с удалёнными пробелами). Все бинарные кодирования JSON похожи в этом отношении. Неясно, стоит ли такое небольшое сокращение пространства (и, возможно, ускорение парсинга) потери человекочитаемости.</p>
<p>В следующих разделах мы увидим, как можно добиться гораздо лучших результатов и закодировать ту же запись всего в 32 байта.</p>
<p><strong>Рисунок 5-2. Пример записи (Пример 5-2), закодированной с использованием MessagePack</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png"><img decoding="async" class="aligncenter size-full wp-image-2187" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png" alt="" width="2560" height="2162" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-300x253.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1024x865.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-768x649.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1536x1297.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-2048x1729.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-450x380.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-780x659.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1600x1351.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h2>Protocol Buffers</h2>
<p><strong>Protocol Buffers (protobuf)</strong> — это библиотека бинарного кодирования, разработанная в <strong>Google</strong>. Она похожа на <strong>Apache Thrift</strong>, который был изначально разработан <strong>Facebook</strong>; большинство того, что говорится в этом разделе о <strong>Protocol Buffers</strong>, также применимо к <strong>Thrift</strong>.</p>
<p><strong>Protocol Buffers</strong> требует наличия схемы для любых данных, которые кодируются. Чтобы закодировать данные из Примера 5-2 в <strong>Protocol Buffers</strong>, вы должны описать схему на языке определения интерфейсов (<strong>IDL</strong>) <strong>Protocol Buffers</strong> следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">syntax = "proto3";

message Person {
    string user_name = 1;
    int64 favorite_number = 2;
    repeated string interests = 3;
}</pre><p><strong>Protocol Buffers</strong> поставляется с инструментом генерации кода, который принимает определение схемы, подобное показанному здесь, и генерирует классы, реализующие схему на различных языках программирования. Код вашего приложения может вызывать этот сгенерированный код для кодирования или декодирования записей схемы. Язык схемы очень прост по сравнению с <strong>JSON Schema</strong>: он определяет только поля записей и их типы, но не поддерживает другие ограничения на возможные значения полей.<br />
Кодирование Примера 5-2 с использованием кодировщика <strong>Protocol Buffers</strong> требует 33 байт, как показано на рисунке 5-3.</p>
<p><strong>Рисунок 5-3. Пример записи, закодированной с использованием Protocol Buffers</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png"><img decoding="async" class="aligncenter size-full wp-image-2186" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png" alt="" width="2560" height="1578" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-300x185.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1024x631.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-768x473.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1536x947.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-2048x1262.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-450x277.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-780x481.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1600x986.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Аналогично рисунку 5-2, каждое поле имеет аннотацию типа (чтобы указать, является ли оно строкой, целым числом и т. д.) и, где необходимо, указание длины (например, длина строки). Строки, встречающиеся в данных («Martin», «daydreaming», «hacking»), также закодированы как <strong>ASCII</strong> (точнее, UTF-8), аналогично предыдущему примеру.</p>
<p>Большое отличие по сравнению с рисунком 5-2 заключается в том, что здесь нет имён полей (<code>userName</code>, <code>favoriteNumber</code>, <code>interests</code>). Вместо этого закодированные данные содержат теги полей, которые являются числами (1, 2 и 3). Это те самые числа, которые указаны в определении схемы. Теги полей подобны псевдонимам для полей — это компактный способ указать, о каком поле идёт речь, без необходимости явно указывать имя поля.</p>
<p>Как видно, <strong>Protocol Buffers</strong> экономит ещё больше места, упаковывая тип поля и номер тега в один байт. Используются целые числа переменной длины: число 1337 кодируется в два байта, при этом старший бит каждого байта используется для указания, есть ли ещё байты далее. Это означает, что числа от –64 до 63 кодируются в одном байте, числа от –8192 до 8191 — в двух байтах и т. д. Более крупные числа используют больше байт.</p>
<p>Protocol Buffers не имеет явного типа списка или массива. Вместо этого модификатор repeated у поля interests указывает, что поле содержит список значений, а не одно значение. В бинарном кодировании элементы списка представлены просто как повторяющиеся вхождения одного и того же тега поля внутри одной записи.</p>
<h3>Теги полей и эволюция схем</h3>
<p>Мы уже говорили, что схемы неизбежно должны меняться со временем. Мы называем это эволюцией схемы. Как Protocol Buffers обрабатывает изменения схем при сохранении обратной и прямой совместимости?</p>
<p>Как видно из примеров, <strong>закодированная запись</strong> — это просто конкатенация её закодированных полей. Каждое поле идентифицируется по своему номеру тега (числа 1, 2, 3 в примере схемы) и аннотируется типом данных (например, строка или целое число). Если значение поля не установлено, оно просто опускается из закодированной записи. Из этого видно, что теги полей критически важны для смысла закодированных данных. Вы можете изменить имя поля в схеме, так как закодированные данные никогда не ссылаются на имена полей, но вы не можете изменить тег поля, так как это сделает все существующие закодированные данные недействительными.<br />
Вы можете добавлять новые поля в схему, при условии, что вы присвоите каждому полю новый номер тега. Если старый код (который не знает о новых тегах, которые вы добавили) попытается прочитать данные, записанные новым кодом, включая новое поле с номером тега, который он не распознаёт, он может просто проигнорировать это поле. Аннотация типа данных позволяет парсеру определить, сколько байт нужно пропустить, и сохранить неизвестные поля, чтобы избежать проблемы, показанной на рисунке 5-1. Это сохраняет прямую совместимость: старый код может читать записи, которые были записаны новым кодом.</p>
<p><strong>А как насчёт обратной совместимости?</strong> Пока у каждого поля уникальный номер тега, новый код всегда может читать старые данные, потому что номера тегов всё ещё имеют то же значение. Если поле было добавлено в новой схеме, а вы читаете старые данные, которые ещё не содержат этого поля, оно заполняется значением по умолчанию (например, пустой строкой, если тип поля — строка, или нулём, если это число).</p>
<p><strong>Удаление поля</strong> — это то же самое, что добавление поля, только с обратными требованиями по обратной и прямой совместимости. Вы никогда не можете снова использовать тот же номер тега, так как где-то могут существовать данные, записанные с этим старым номером тега, и это поле должно игнорироваться новым кодом. Номера тегов, использовавшиеся в прошлом, можно зарезервировать в определении схемы, чтобы они не были забыты.</p>
<p><strong>А что насчёт изменения типа данных поля?</strong> Это возможно для некоторых типов — подробности см. в документации — но есть риск, что значения будут усечены. Например, допустим, вы изменяете 32-битное целое число на 64-битное. Новый код может легко читать данные, записанные старым кодом, так как парсер может дополнить отсутствующие биты нулями. Однако если старый код читает данные, записанные новым кодом, старый код всё ещё использует 32-битную переменную для хранения значения. Если декодированное 64-битное значение не помещается в 32 бита, оно будет усечено.</p>
<h2>Avro</h2>
<p><strong>Apache Avro</strong> — ещё один бинарный формат кодирования, который примечателен своей отличием от Protocol Buffers. Он был запущен в 2009 году как подпроект Hadoop в результате того, что Protocol Buffers плохо подходил для кейсов использования Hadoop.</p>
<p>Avro также использует схему для задания структуры кодируемых данных. У него есть два языка схем: один (Avro IDL), предназначенный для редактирования человеком, и один (основанный на JSON), который легче читается машиной. Как и в Protocol Buffers, этот язык схемы определяет только поля и их типы, но не сложные правила валидации, как в JSON Schema.</p>
<p>Наша примерная схема, написанная на Avro IDL, может выглядеть так:</p><pre class="urvanov-syntax-highlighter-plain-tag">record Person {
    string               userName;
    union { null, long } favoriteNumber = null;
    array&lt;string&gt;        interests;
}</pre><p>Эквивалентное JSON-представление этой схемы выглядит следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName",       "type": "string"},
        {"name": "favoriteNumber", "type": ["null", "long"], "default": null},
        {"name": "interests",      "type": {"type": "array", "items": "string"}}
    ]
}</pre><p>Прежде всего, обратите внимание, что в схеме нет номеров тегов. Если мы закодируем нашу примерную запись (Пример 5-2) с использованием этой схемы, бинарное кодирование Avro занимает всего 32 байта — самое компактное из всех рассмотренных нами кодирований. Подробное разбиение последовательности закодированных байтов показано на рисунке 5-4.<br />
Если вы изучите последовательность байтов, вы увидите, что там нет ничего, что идентифицировало бы поля или их типы данных. Кодировка просто состоит из значений, объединённых вместе. Строка — это просто префикс длины, за которым следуют байты UTF-8, но в закодированных данных нет ничего, что указывало бы, что это строка. Это вполне может быть целое число или что-то ещё. Целое число кодируется с использованием кодирования переменной длины.</p>
<p><strong>Рисунок 5-4. Пример записи, закодированной с использованием Avro</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2185" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png" alt="" width="2560" height="1689" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-300x198.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1024x676.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-768x507.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1536x1013.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-2048x1351.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-450x297.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-780x515.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1600x1056.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Чтобы разобрать бинарные данные, вы проходите по полям в том порядке, в котором они указаны в схеме, и используете схему, чтобы определить тип данных каждого поля. Это означает, что бинарные данные могут быть корректно декодированы только в том случае, если код, читающий данные, использует ту же самую схему, что и код, записывающий данные. Любое несоответствие в схеме между читателем и писателем приведёт к некорректно декодированным данным.<br />
Так каким же образом Avro поддерживает эволюцию схем?</p>
<h3>Схема писателя и схема читателя</h3>
<p>Когда приложение хочет закодировать какие-то данные (записать их в файл или базу данных, отправить по сети и т. д.), оно кодирует данные с использованием той версии схемы, о которой ему известно — например, эта схема может быть встроена в приложение. Это называется схемой писателя.</p>
<p>Когда приложение хочет декодировать какие-то данные (прочитать их из файла или базы данных, получить их из сети и т. д.), оно использует две схемы: схему писателя, которая идентична использованной для кодирования, и схему читателя, которая может отличаться. Это показано на рисунке 5-5. Схема читателя определяет поля каждой записи, которые ожидает код приложения, и их типы.</p>
<p><strong>Рисунок 5-5. В Protocol Buffers кодирование и декодирование могут использовать разные версии схемы.</strong> В Avro для декодирования используются две схемы: схема писателя должна быть идентична использованной при кодировании, но схема читателя может быть более старой или новой версией.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2184" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png" alt="" width="2560" height="1222" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-300x143.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1024x489.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-768x367.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1536x733.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-2048x978.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-450x215.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-780x372.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1600x764.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Если схема читателя и схема писателя совпадают, декодирование простое. Если они различаются, Avro устраняет расхождения, сравнивая схему писателя и схему читателя бок о бок и преобразуя данные из схемы писателя в схему читателя. Спецификация Avro точно определяет, как работает это согласование, и это показано на рисунке 5-6.<br />
Например, не проблема, если схема писателя и схема читателя имеют поля в разном порядке, потому что при согласовании полей они сопоставляются по имени. Если код, читающий данные, встречает поле, которое присутствует в схеме писателя, но отсутствует в схеме читателя, оно игнорируется. Если код, читающий данные, ожидает какое-то поле, но схема писателя не содержит поле с таким именем, оно заполняется значением по умолчанию, объявленным в схеме читателя.</p>
<p><strong>Рисунок 5-6. Читатель Avro устраняет различия между схемой писателя и схемой читателя</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2182" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png" alt="" width="2560" height="918" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-300x108.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1024x367.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-768x275.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1536x551.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-2048x735.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-450x161.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-780x280.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1600x574.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h3>Правила эволюции схем</h3>
<p>В Avro <strong>прямая совместимость (forward compatibility)</strong> означает, что в качестве писателя вы можете использовать новую версию схемы, а в качестве читателя — старую. <strong>Обратная совместимость (backward compatibility)</strong>, напротив, означает, что в качестве читателя вы можете использовать новую версию схемы, а в качестве писателя — старую.</p>
<p>Чтобы сохранить совместимость, вы можете добавлять или удалять только те поля, которые имеют значение по умолчанию. (Поле favoriteNumber в нашей Avro-схеме имеет значение по умолчанию null.) Например, допустим, вы добавляете поле со значением по умолчанию, так что это новое поле существует в новой схеме, но отсутствует в старой. Когда читатель, использующий новую схему, читает запись, созданную со старой схемой, для отсутствующего поля подставляется значение по умолчанию.</p>
<p>Если вы добавите поле без значения по умолчанию, новые читатели не смогут прочитать данные, созданные старыми писателями, и это нарушит обратную совместимость. Если вы удалите поле без значения по умолчанию, старые читатели не смогут прочитать данные, созданные новыми писателями, и это нарушит прямую совместимость.</p>
<p>В некоторых языках программирования null допустим в качестве значения по умолчанию для любой переменной, но в Avro это не так: если вы хотите, чтобы поле могло быть равно null, вы должны использовать объединённый тип (union type). Например,</p><pre class="urvanov-syntax-highlighter-plain-tag">union { null, long, string } field;</pre><p>указывает, что поле может быть числом, строкой или null. Вы можете использовать null в качестве значения по умолчанию только в том случае, если оно является первой ветвью объединения. Это немного более многословно, чем если бы все поля были допускающими null по умолчанию, но такая явность помогает предотвращать ошибки, точно определяя, что может, а что не может быть null.</p>
<p>Изменение типа данных поля возможно, при условии что Avro может преобразовать этот тип. Изменение имени поля также возможно, но немного сложнее: схема читателя может содержать псевдонимы (aliases) для имён полей, чтобы сопоставлять имена полей старой схемы писателя с этими псевдонимами. Это означает, что изменение имени поля совместимо с прошлыми версиями (backward compatible), но не с будущими (not forward compatible). Аналогично, добавление новой ветви в объединённый тип совместимо с прошлыми версиями, но не с будущими.</p>
<h3>Но что такое схема писателя?</h3>
<p>Здесь есть важный вопрос, который мы до сих пор обходили стороной: как читатель узнаёт схему писателя, с которой были закодированы конкретные данные? Мы не можем просто включать полную схему в каждую запись, потому что схема, скорее всего, будет намного больше самих закодированных данных, что сведёт на нет все преимущества экономии места при бинарном кодировании.</p>
<p>Ответ зависит от контекста, в котором используется Avro. Вот несколько примеров:</p>
<ul>
<li><strong>Большой файл с множеством записей</strong><br />
Распространённый вариант использования Avro — хранение большого файла, содержащего миллионы записей, все закодированные с одной и той же схемой. (Мы обсудим такую ситуацию далее.) В этом случае писатель файла может просто включить схему писателя один раз в начале файла. Avro определяет файловый формат (object container files) для этого.</li>
<li><strong>База данных с индивидуально записанными записями</strong><br />
В базе данных разные записи могут быть записаны в разное время с использованием разных схем писателей — нельзя предполагать, что у всех записей будет одинаковая схема. Самое простое решение — включать номер версии в начало каждой закодированной записи и хранить список версий схем в базе данных. Читатель может получить запись, извлечь номер версии, а затем получить схему писателя для этой версии из базы данных. Используя эту схему писателя, он может декодировать остальную часть записи. Например, реестр схем Confluent для Apache Kafka и Espresso от LinkedIn работают именно так.</li>
<li><strong>Передача записей по сетевому соединению</strong><br />
Когда два процесса обмениваются данными по двунаправленному сетевому соединению, они могут согласовать версию схемы при установке соединения, а затем использовать эту схему в течение всего времени соединения. Протокол RPC Avro (см. «Поток данных через сервисы: REST и RPC») работает именно так.</li>
</ul>
<p>База данных версий схем полезна в любом случае, так как она служит документацией и даёт возможность проверить совместимость схем. В качестве номера версии можно использовать простой увеличивающийся целочисленный идентификатор или хэш от самой схемы.</p>
<h3>Динамически генерируемые схемы</h3>
<p>Одним из преимуществ подхода Avro по сравнению с Protocol Buffers является то, что схема не содержит никаких тегов-полей (tag numbers). Но почему это важно? В чём проблема в том, чтобы хранить в схеме несколько чисел?</p>
<p>Разница в том, что Avro лучше подходит для динамически генерируемых схем. Например, представим, что у вас есть реляционная база данных, содержимое которой вы хотите выгрузить в файл, и вы хотите использовать бинарный формат, чтобы избежать упомянутых ранее проблем с текстовыми форматами (JSON, CSV, XML). Если вы используете Avro, то довольно легко можете сгенерировать Avro-схему (в JSON-представлении, которое мы видели ранее) из реляционной схемы и закодировать содержимое базы данных с использованием этой схемы, выгрузив всё это в объектный контейнерный файл Avro. Вы можете сгенерировать схему записи для каждой таблицы базы данных, и каждый столбец станет полем в этой записи. Имя столбца в базе данных сопоставляется с именем поля в Avro.</p>
<p>Теперь, если схема базы данных изменится (например, в таблицу добавят один столбец и удалят другой), вы можете просто сгенерировать новую Avro-схему из обновлённой схемы базы данных и экспортировать данные в новой Avro-схеме. Процесс экспорта данных не должен обращать внимание на изменения схемы — он может просто выполнять преобразование схемы каждый раз при запуске. Любой, кто будет читать новые файлы данных, увидит, что поля записи изменились, но так как поля идентифицируются по имени, обновлённая схема писателя всё равно сможет быть сопоставлена со старой схемой читателя.</p>
<p>Напротив, если бы вы использовали Protocol Buffers для этой цели, теги полей, скорее всего, пришлось бы назначать вручную: каждый раз, когда схема базы данных меняется, администратору пришлось бы вручную обновлять сопоставление между именами столбцов базы данных и тегами полей. (Теоретически это можно автоматизировать, но генератор схем должен был бы очень осторожно следить за тем, чтобы не назначить уже использовавшиеся ранее теги.) Подобные динамически генерируемые схемы просто не были целью проектирования Protocol Buffers, тогда как для Avro это было одной из задач.</p>
<h2>Преимущества схем</h2>
<p>Как мы видели, Protocol Buffers и Avro используют схему для описания формата бинарного кодирования. Их языки схем намного проще, чем XML Schema или JSON Schema, которые поддерживают гораздо более детализированные правила валидации (например, «строковое значение этого поля должно соответствовать этому регулярному выражению» или «целочисленное значение этого поля должно находиться в диапазоне от 0 до 100»). Так как Protocol Buffers и Avro проще реализовать и проще использовать, они получили широкую поддержку во множестве языков программирования.</p>
<p>Идеи, на которых основаны эти кодировки, отнюдь не новые. Например, у них много общего с ASN.1 — языком описания схем, впервые стандартизованным в 1984 году. Он использовался для определения различных сетевых протоколов, и его бинарное кодирование (DER) до сих пор используется, например, для кодирования SSL-сертификатов (X.509). ASN.1 поддерживает эволюцию схем с помощью тегов-полей, аналогично Protocol Buffers. Однако он также очень сложный и плохо документированный, поэтому ASN.1 вряд ли является хорошим выбором для новых приложений.</p>
<p>Многие системы данных также реализуют собственные проприетарные бинарные форматы кодирования для своих данных. Например, большинство реляционных баз данных имеют сетевой протокол, по которому вы можете отправлять запросы в базу данных и получать ответы. Эти протоколы, как правило, специфичны для конкретной базы данных, и вендор базы данных предоставляет драйвер (например, через API ODBC или JDBC), который декодирует ответы из сетевого протокола базы в структуры данных в памяти.</p>
<p>Таким образом, мы видим, что хотя текстовые форматы данных, такие как JSON, XML и CSV, широко распространены, бинарные кодировки, основанные на схемах, также являются жизнеспособным вариантом. У них есть ряд полезных свойств:</p>
<ul>
<li>Они могут быть гораздо более компактными, чем различные «бинарные JSON»-варианты, поскольку могут опускать имена полей из закодированных данных.</li>
<li>Схема является ценным видом документации, и так как схема требуется для декодирования, можно быть уверенным, что она актуальна (в то время как вручную поддерживаемая документация легко может разойтись с реальностью).</li>
<li>Ведение базы данных схем позволяет проверять прямую и обратную совместимость изменений схем до того, как что-либо будет развернуто.</li>
<li>Для пользователей статически типизированных языков программирования возможность генерировать код из схемы полезна, поскольку она позволяет выполнять проверку типов на этапе компиляции.</li>
</ul>
<p>В итоге, эволюция схем обеспечивает такую же гибкость, как и базы данных JSON без схемы/схемой-на-чтение (см. «Гибкость схемы в документной модели»), при этом предоставляя лучшие гарантии для ваших данных и лучшее инструментальное обеспечение.</p>
<h1>Режимы потоков данных</h1>
<p>В начале этой главы мы сказали, что всякий раз, когда вы хотите отправить какие-то данные другому процессу, с которым вы не разделяете память — например, когда вы хотите отправить данные по сети или записать их в файл, — вам нужно закодировать их в виде последовательности байтов. Затем мы обсудили различные способы кодирования для этого.<br />
Мы поговорили о прямой и обратной совместимости, которые важны для эволюционируемости (возможности легко вносить изменения, позволяя обновлять разные части вашей системы независимо и не вынуждая менять всё сразу). Совместимость — это отношение между одним процессом, который кодирует данные, и другим процессом, который их декодирует.</p>
<p>Это довольно абстрактная идея — существует множество способов, которыми данные могут перемещаться от одного процесса к другому. Кто кодирует данные, а кто их декодирует? В оставшейся части этой главы мы рассмотрим некоторые из наиболее распространённых способов, которыми данные передаются между процессами:</p>
<ul>
<li>Через базы данных (см. «Потоки данных через базы данных»)</li>
<li>Через вызовы сервисов (см. «Потоки данных через сервисы: REST и RPC»)</li>
<li>Через движки рабочих процессов (см. «Долговременное выполнение и рабочие процессы»)</li>
<li>Через асинхронные сообщения (см. «Архитектуры, управляемые событиями»)</li>
</ul>
<h2>Потоки данных через базы данных</h2>
<p>В базе данных процесс, который записывает данные в базу, кодирует их, а процесс, который считывает данные из базы, декодирует их. Может существовать всего один процесс, обращающийся к базе данных, и в этом случае читателем будет просто более поздняя версия того же самого процесса — тогда хранение чего-либо в базе можно рассматривать как отправку сообщения самому себе в будущем.<br />
Обратная совместимость здесь явно необходима; в противном случае вы сами в будущем не сможете декодировать то, что записали раньше.</p>
<p>В общем случае несколько различных процессов часто обращаются к базе данных одновременно. Эти процессы могут быть разными приложениями или сервисами, или же просто несколькими экземплярами одного и того же сервиса (работающими параллельно ради масштабируемости или отказоустойчивости). Так или иначе, в среде, где приложение изменяется, вероятно, что некоторые процессы, обращающиеся к базе данных, будут работать на более новой версии кода, а некоторые — на более старой (например, потому что новая версия развёртывается поэтапно, и часть экземпляров уже обновлена, а часть ещё нет).</p>
<p>Это означает, что значение в базе данных может быть записано более новой версией кода, а затем прочитано более старой версией кода, которая всё ещё работает. Таким образом, прямая совместимость также часто требуется для баз данных.</p>
<h3>Разные значения, записанные в разное время</h3>
<p>База данных в целом позволяет обновлять любое значение в любое время. Это означает, что в одной и той же базе у вас могут быть некоторые значения, записанные пять миллисекунд назад, и некоторые значения, записанные пять лет назад.<br />
Когда вы разворачиваете новую версию своего приложения (по крайней мере серверного приложения), вы можете полностью заменить старую версию на новую за несколько минут. Для содержимого базы данных это не так: пятилетние данные всё ещё будут там, в своём исходном кодировании, если вы их явно не переписали с тех пор. Это наблюдение иногда суммируют выражением «данные переживают код».</p>
<p>Переписать (мигрировать) данные в новую схему, безусловно, возможно, но это дорогостоящая операция на больших объёмах данных, поэтому большинство баз данных избегают её, если это возможно. Большинство реляционных баз данных позволяют выполнять простые изменения схемы, например добавлять новый столбец со значением null по умолчанию, без переписывания существующих данных. Когда старая строка считывается, база данных подставляет null для любых столбцов, которых нет в закодированных данных на диске. Таким образом, эволюция схем позволяет всей базе данных выглядеть так, будто она закодирована с помощью одной схемы, даже если в нижележащем хранилище содержатся записи, закодированные с использованием различных исторических версий схемы.</p>
<p>Более сложные изменения схемы — например, изменение однозначного атрибута на многозначный или перенос части данных в отдельную таблицу — всё ещё требуют переписывания данных, часто на уровне приложения. Поддержание прямой и обратной совместимости при таких миграциях остаётся исследовательской проблемой.</p>
<h3>Архивное хранилище</h3>
<p>Возможно, вы время от времени делаете снимок своей базы данных, скажем, для целей резервного копирования или для загрузки в хранилище данных (см. «Хранилище данных»). В этом случае дамп данных, как правило, будет закодирован с использованием последней схемы, даже если исходное кодирование в исходной базе содержало смесь версий схем разных эпох. Так как вы всё равно копируете данные, имеет смысл закодировать их копию последовательно.</p>
<p>Так как дамп данных записывается за один раз и впоследствии является неизменным, такие форматы, как объектные контейнерные файлы Avro, хорошо подходят. Это также хорошая возможность закодировать данные в аналитически-удобном колонко-ориентированном формате, таком как Parquet (см. «Сжатие колонок»).</p>
<h2>Потоки данных через сервисы: REST и RPC</h2>
<p>Когда у вас есть процессы, которым нужно обмениваться данными по сети, существует несколько способов организации такого взаимодействия. Наиболее распространённый вариант предполагает две роли: клиенты и серверы. Серверы предоставляют API по сети, а клиенты могут подключаться к серверам, чтобы делать запросы к этому API. API, предоставляемый сервером, называется сервисом.</p>
<p>Веб работает именно так: клиенты (веб-браузеры) делают запросы к веб-серверам, выполняя GET-запросы для загрузки HTML, CSS, JavaScript, изображений и т. д., и POST-запросы для отправки данных на сервер. API состоит из стандартизированного набора протоколов и форматов данных (HTTP, URL, SSL/TLS, HTML и т. д.). Поскольку веб-браузеры, веб-серверы и авторы сайтов в основном соглашаются с этими стандартами, вы можете использовать любой веб-браузер для доступа к любому сайту (по крайней мере, в теории!).</p>
<p>Веб-браузеры — не единственный тип клиентов. Например, нативные приложения, работающие на мобильных устройствах и настольных компьютерах, часто взаимодействуют с серверами, а клиентские JavaScript-приложения, работающие внутри веб-браузеров, также могут делать HTTP-запросы. В этом случае ответ сервера обычно не является HTML для отображения человеку, а представляет собой данные в кодировке, удобной для дальнейшей обработки клиентским приложением (чаще всего JSON). Хотя HTTP может использоваться как транспортный протокол, API, реализуемый поверх него, является специфичным для приложения, и клиент с сервером должны договориться о деталях этого API.</p>
<p>В некотором смысле сервисы похожи на базы данных: они обычно позволяют клиентам отправлять и запрашивать данные. Однако, в то время как базы данных позволяют выполнять произвольные запросы с использованием языков запросов, которые мы обсуждали в Главе 3, сервисы предоставляют специфичный для приложения API, который позволяет только те входы и выходы, которые заранее определены бизнес-логикой (кодом приложения) сервиса. Это ограничение обеспечивает определённую степень инкапсуляции: сервисы могут накладывать детальные ограничения на то, что клиенты могут и не могут делать.</p>
<p>Ключевая цель проектирования сервис-ориентированной/микросервисной архитектуры — упростить изменение и сопровождение приложения, сделав сервисы независимо развёртываемыми и эволюционируемыми. Общий принцип заключается в том, что каждый сервис должен находиться в ведении одной команды, и эта команда должна иметь возможность часто выпускать новые версии сервиса, не координируясь с другими командами. Следовательно, мы должны ожидать, что старые и новые версии серверов и клиентов будут работать одновременно, и поэтому кодировка данных, используемая серверами и клиентами, должна быть совместима между версиями API сервиса.</p>
<h3>Веб-сервисы</h3>
<p>Когда HTTP используется как базовый протокол для взаимодействия с сервисом, это называется веб-сервисом. Веб-сервисы обычно применяются при построении сервис-ориентированной или микросервисной архитектуры (обсуждалось ранее в «Микросервисы и serverless»). Термин «веб-сервис» — возможно, немного неточен, потому что веб-сервисы используются не только в вебе, но и в ряде других контекстов. Например:</p>
<ul>
<li>клиентское приложение, работающее на устройстве пользователя (например, нативное приложение на мобильном устройстве или веб-приложение на JavaScript в браузере), отправляющее запросы к сервису по HTTP. Эти запросы обычно идут через публичный интернет;</li>
<li>один сервис, отправляющий запросы другому сервису, принадлежащему той же организации, часто находящемуся в том же дата-центре, как часть сервис-ориентированной/микросервисной архитектуры;</li>
<li>один сервис, отправляющий запросы сервису, принадлежащему другой организации, обычно через интернет. Это используется для обмена данными между бэкенд-системами разных организаций. В эту категорию входят публичные API, предоставляемые онлайн-сервисами, такими как системы обработки кредитных карт или OAuth для совместного доступа к пользовательским данным.</li>
</ul>
<p>Наиболее популярная философия проектирования сервисов — REST, которая строится на принципах HTTP. Она делает акцент на простых форматах данных, использовании URL для идентификации ресурсов и использовании функций HTTP для управления кэшированием, аутентификацией и согласованием типа содержимого. API, спроектированный в соответствии с принципами REST, называется RESTful.</p>
<p>Код, которому нужно вызвать API веб-сервиса, должен знать, к какому HTTP-эндпоинту обращаться и какие форматы данных отправлять и ожидать в ответ. Даже если сервис использует принципы RESTful-дизайна, клиентам всё равно нужно каким-то образом узнавать эти детали. Разработчики сервисов часто используют язык описания интерфейсов (IDL), чтобы определить и задокументировать эндпоинты API своего сервиса и модели данных, а также эволюционировать их со временем. Другие разработчики могут затем использовать описание сервиса, чтобы понять, как делать к нему запросы. Два наиболее популярных IDL для сервисов — это <strong>OpenAPI</strong> (также известный как Swagger) и <strong>gRPC</strong>. OpenAPI используется для веб-сервисов, которые отправляют и принимают JSON-данные, в то время как gRPC-сервисы отправляют и принимают Protocol Buffers.</p>
<p>Разработчики обычно пишут описания сервисов OpenAPI в формате <strong>JSON</strong> или <strong>YAML</strong>; см. Пример 5-3. Определение сервиса позволяет разработчикам задавать эндпоинты сервиса, документацию, версии, модели данных и многое другое. Определения gRPC выглядят похоже, но задаются с использованием описаний сервисов на Protocol Buffers.</p>
<p><strong>Пример 5-3. Пример описания сервиса OpenAPI в YAML</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">openapi: 3.0.0
info:
  title: Ping, Pong
  version: 1.0.0
servers:
  - url: http://localhost:8080
paths:
  /ping:
    get:
      summary: Given a ping, returns a pong message
      responses:
        '200':
          description: A pong
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Pong!</pre><p>Даже если философия проектирования и IDL приняты, разработчикам всё равно нужно написать код, реализующий вызовы API их сервиса. Чтобы упростить эту задачу, часто используется фреймворк сервисов. Фреймворки сервисов, такие как Spring Boot, FastAPI и gRPC, позволяют разработчикам писать бизнес-логику для каждого API-эндпоинта, в то время как код фреймворка обрабатывает маршрутизацию, метрики, кэширование, аутентификацию и так далее. Пример 5-4 показывает пример реализации на Python сервиса, определённого в Примере 5-3.</p>
<p><strong>Пример 5-4. Пример сервиса FastAPI, реализующего определение из Примера 5-3</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="Ping, Pong", version="1.0.0")

class PongResponse(BaseModel):
    message: str = "Pong!"

@app.get("/ping", response_model=PongResponse,
         summary="Given a ping, returns a pong message")
async def ping():
    return PongResponse()</pre><p>Многие фреймворки связывают определения сервисов и серверный код. В некоторых случаях, например с популярным Python-фреймворком FastAPI, серверы пишутся в коде, а IDL генерируется автоматически. В других случаях, например с gRPC, сначала пишется определение сервиса, а затем генерируется каркас серверного кода. Оба подхода позволяют разработчикам генерировать клиентские библиотеки и SDK на различных языках из определения сервиса. Помимо генерации кода, инструменты IDL, такие как Swagger, могут генерировать документацию, проверять совместимость изменений схемы и предоставлять графический интерфейс для разработчиков для выполнения запросов и тестирования сервисов.</p>
<h3>Проблемы удалённых вызовов процедур (RPC)</h3>
<p><strong>Веб-сервисы</strong> — это лишь последняя инкарнация длинной линии технологий для выполнения API-запросов по сети, многие из которых получили много шума, но имеют серьёзные проблемы. Enterprise JavaBeans (EJB) и удалённые вызовы методов Java (RMI) ограничены только Java. Distributed Component Object Model (DCOM) ограничен платформами Microsoft. Common Object Request Broker Architecture (CORBA) чрезмерно сложен и не обеспечивает обратной или прямой совместимости. SOAP и фреймворк веб-сервисов WS-* нацелены на обеспечение взаимодействия между поставщиками, но также страдают от сложности и проблем совместимости.</p>
<p>Все они основаны на идее удалённого вызова процедуры (RPC), существующей с 1970-х годов. Модель RPC пытается сделать так, чтобы запрос к удалённому сетевому сервису выглядел так же, как вызов функции или метода в вашем языке программирования, внутри одного процесса (эта абстракция называется прозрачность расположения). Хотя RPC поначалу кажется удобным, подход фундаментально ошибочен. Сетевой запрос сильно отличается от локального вызова функции:</p>
<p><strong>Локальный вызов функции</strong> предсказуем и либо выполняется успешно, либо завершается сбоем, в зависимости только от параметров, которые находятся под вашим контролем. Сетевой запрос непредсказуем: запрос или ответ может быть потерян из-за сетевой проблемы, удалённая машина может быть медленной или недоступной, и такие проблемы полностью вне вашего контроля. Сетевые проблемы распространены, поэтому нужно предвидеть их, например, повторяя неудачный запрос.</p>
<p><strong>Локальный вызов функции</strong> либо возвращает результат, либо генерирует исключение, либо не возвращает вовсе (например, зацикливается или процесс падает). У сетевого запроса есть ещё один возможный исход: он может завершиться без результата из-за таймаута. В этом случае вы просто не знаете, что произошло: если вы не получили ответ от удалённого сервиса, у вас нет способа понять, дошёл ли запрос или нет.</p>
<p>Если вы повторите неудачный сетевой запрос, может оказаться, что предыдущий запрос всё-таки дошёл, но был потерян только ответ. В этом случае повтор приведёт к выполнению действия несколько раз, если только вы не встроите в протокол механизм дедупликации (идемпотентность) . У локальных вызовов функций такой проблемы нет.</p>
<p>Каждый раз при вызове локальной функции выполнение обычно занимает примерно одно и то же время. Сетевой запрос намного медленнее вызова функции, и его задержка также крайне вариативна: в хорошие моменты он может выполняться менее чем за миллисекунду, но при перегруженной сети или перегруженном удалённом сервисе выполнение того же самого может занять много секунд.</p>
<p>При вызове локальной функции вы можете эффективно передавать ей ссылки (указатели) на объекты в локальной памяти. При выполнении сетевого запроса все эти параметры нужно закодировать в последовательность байтов, которую можно передать по сети. Это допустимо, если параметры — неизменяемые примитивы вроде чисел или коротких строк, но это быстро становится проблематичным при больших объёмах данных и изменяемых объектах.</p>
<p>Клиент и сервис могут быть реализованы на разных языках программирования, поэтому фреймворк RPC должен преобразовывать типы данных из одного языка в другой. Это может выглядеть некрасиво, так как не все языки имеют одинаковые типы — вспомните, например, проблемы JavaScript с числами больше 2⁵³ (см. «JSON, XML и бинарные варианты»). Внутри одного процесса, написанного на одном языке, такой проблемы не существует.</p>
<p>Все эти факторы означают, что нет смысла пытаться сделать так, чтобы удалённый сервис выглядел слишком похожим на локальный объект в вашем языке программирования, потому что это принципиально разные вещи. Часть привлекательности REST заключается в том, что он рассматривает передачу состояния по сети как процесс, отличный от вызова функции.</p>
<h3>Балансировщики нагрузки, обнаружение сервисов и сервисные mesh</h3>
<p><strong>Все сервисы взаимодействуют по сети.</strong> По этой причине клиент должен знать адрес сервиса, к которому он подключается — эта задача известна как обнаружение сервисов. Самый простой подход — настроить клиент на подключение к IP-адресу и порту, на которых работает сервис. Такая конфигурация будет работать, но если сервер отключится, будет перенесён на новую машину или окажется перегружен, клиент придётся перенастраивать вручную.</p>
<p>Чтобы обеспечить более высокую доступность и масштабируемость, обычно запускается несколько экземпляров сервиса на разных машинах, и любой из них может обработать входящий запрос. Распределение запросов между этими экземплярами называется балансировкой нагрузки. Существует множество решений для балансировки нагрузки и обнаружения сервисов:</p>
<p><strong>Аппаратные балансировщики нагрузки</strong> — это специализированное оборудование, устанавливаемое в дата-центрах. Они позволяют клиентам подключаться к одному хосту и порту, а входящие соединения перенаправляются на один из серверов, на которых запущен сервис. Такие балансировщики выявляют сетевые сбои при подключении к downstream-серверу и переключают трафик на другие серверы.</p>
<p><strong>Программные балансировщики нагрузки</strong> работают почти так же, как аппаратные, но не требуют специализированного устройства. Программные балансировщики, такие как Nginx и HAProxy, представляют собой приложения, которые можно установить на стандартный сервер.</p>
<p><strong>Служба доменных имён (DNS)</strong> используется для разрешения доменных имён в Интернете, когда вы открываете веб-страницу. Она поддерживает балансировку нагрузки, позволяя привязать несколько IP-адресов к одному доменному имени. Клиенты могут быть настроены на подключение к сервису по доменному имени вместо IP-адреса, а сетевая подсистема клиента выбирает, какой IP-адрес использовать при подключении. Недостаток этого подхода в том, что DNS изначально спроектирован для распространения изменений с течением времени и кэширования записей. Если серверы часто запускаются, останавливаются или перемещаются, клиенты могут видеть устаревшие IP-адреса, на которых сервис больше не работает.</p>
<p>Системы обнаружения сервисов используют централизованный реестр вместо DNS для отслеживания доступных конечных точек сервиса. Когда запускается новый экземпляр сервиса, он регистрируется в системе обнаружения, указывая хост и порт, на которых слушает, а также соответствующие метаданные, такие как информация о шардировании, расположение дата-центра и другое. Затем сервис периодически отправляет heartbeat-сигнал системе обнаружения, подтверждая, что он всё ещё доступен.</p>
<p>Когда клиент хочет подключиться к сервису, он сначала запрашивает список доступных конечных точек у системы обнаружения, а затем подключается напрямую. По сравнению с DNS, системы обнаружения лучше подходят для динамичной среды, где экземпляры сервисов часто меняются. Кроме того, они дают клиентам больше метаданных о сервисе, что позволяет им принимать более разумные решения по балансировке нагрузки.</p>
<p><strong>Сервисные mesh</strong> — это сложная форма балансировки нагрузки, сочетающая программные балансировщики и системы обнаружения. В отличие от традиционных программных балансировщиков, работающих на отдельной машине, балансировщики в сервисной mesh обычно разворачиваются как встроенная клиентская библиотека или как процесс/«sidecar»-контейнер как на стороне клиента, так и на стороне сервера. Клиентские приложения подключаются к своему локальному балансировщику сервиса, который соединяется с балансировщиком на стороне сервера. Оттуда соединение маршрутизируется в локальный серверный процесс.</p>
<p>Хотя такая топология сложна, она имеет ряд преимуществ. Так как клиенты и серверы подключаются только через локальные соединения, шифрование соединений может полностью обрабатываться на уровне балансировщика. Это избавляет клиентов и серверы от необходимости разбираться в сложностях SSL-сертификатов и TLS. Mesh-системы также обеспечивают развитую наблюдаемость: они могут отслеживать, какие сервисы вызывают друг друга в реальном времени, выявлять сбои, фиксировать нагрузку трафика и многое другое.</p>
<p>Выбор подходящего решения зависит от потребностей организации. В очень динамичных средах с оркестратором, таким как Kubernetes, часто используют сервисные mesh, например Istio или Linkerd. Специализированная инфраструктура, такая как базы данных или системы обмена сообщениями, может требовать собственных специализированных балансировщиков. Более простые развёртывания лучше всего работают с программными балансировщиками нагрузки.</p>
<h3>Кодирование данных и эволюция для RPC</h3>
<p>Для эволюционности важно, чтобы RPC-клиенты и серверы могли изменяться и развёртываться независимо друг от друга. В отличие от потока данных через базы данных (описанных в предыдущем разделе), в случае потока данных через сервисы можно упростить задачу: разумно предположить, что сначала будут обновлены все серверы, а затем все клиенты. Таким образом, нужна только обратная совместимость для запросов и прямая совместимость для ответов.</p>
<p><strong>Свойства обратной и прямой совместимости схемы RPC наследуются от используемого формата кодирования:</strong></p>
<ul>
<li>gRPC (Protocol Buffers) и Avro RPC могут эволюционировать в соответствии с правилами совместимости их форматов кодирования.</li>
<li>RESTful API чаще всего используют JSON для ответов и JSON или URI-encoded/form-encoded параметры запроса для запросов. Добавление необязательных параметров запроса и новых полей в объекты ответов обычно считается изменениями, сохраняющими совместимость.</li>
</ul>
<p>Совместимость сервисов усложняется тем, что RPC часто используется для взаимодействия между организациями, и поставщик сервиса зачастую не имеет контроля над своими клиентами и не может заставить их обновиться. Поэтому совместимость должна сохраняться долго, возможно, бесконечно. Если требуется несовместимое изменение, поставщик сервиса часто вынужден поддерживать несколько версий API сервиса одновременно.</p>
<p>Нет единого соглашения о том, как должно работать версионирование API (то есть как клиент может указать, какую версию API он хочет использовать). Для RESTful API распространённые подходы — использовать номер версии в URL или в HTTP-заголовке Accept. Для сервисов, которые используют API-ключи для идентификации конкретного клиента, есть ещё один вариант: хранить запрашиваемую клиентом версию API на сервере и позволять обновлять этот выбор версии через отдельный административный интерфейс.</p>
<h2>Долговременное выполнение и рабочие процессы</h2>
<p>По определению, архитектуры на основе сервисов включают несколько сервисов, каждый из которых отвечает за разные части приложения. Рассмотрим приложение для обработки платежей, которое списывает деньги с кредитной карты и зачисляет средства на банковский счёт. Такая система, вероятно, будет иметь отдельные сервисы, отвечающие за обнаружение мошенничества, интеграцию с кредитными картами, интеграцию с банками и так далее.</p>
<p>Обработка одного платежа в нашем примере требует множества вызовов сервисов. Сервис процессинга платежей может вызвать сервис обнаружения мошенничества для проверки, затем вызвать сервис кредитных карт для списания средств, а затем вызвать банковский сервис для зачисления средств, как показано на Рисунке 5-7. Мы называем эту последовательность шагов рабочим процессом (workflow), а каждый шаг — задачей (task). Рабочие процессы обычно определяются как граф задач. Определения рабочих процессов могут быть написаны на языке общего назначения, на предметно-ориентированном языке (DSL) или на языке разметки, таком как Business Process Execution Language (BPEL).</p>
<h3>ЗАДАЧИ, АКТИВНОСТИ И ФУНКЦИИ</h3>
<p>Разные движки рабочих процессов используют разные названия для задач. Temporal, например, использует термин activity (активность). Другие называют задачи durable functions (долговременные функции). Хотя названия различаются, концепции остаются одинаковыми.</p>
<p><strong>Рисунок 5-7. Пример рабочего процесса, выраженного с использованием Business Process Model and Notation (BPMN) — графической нотации</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2183" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png" alt="" width="2560" height="693" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-300x81.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1024x277.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-768x208.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1536x416.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-2048x555.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-450x122.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-780x211.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1600x433.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Рабочие процессы запускаются или выполняются движком рабочих процессов. Движки рабочих процессов определяют, когда запускать каждую задачу, на какой машине задача должна выполняться, что делать, если задача завершилась сбоем (например, если машина вышла из строя во время выполнения задачи), сколько задач допускается выполнять параллельно и многое другое.</p>
<p>Обычно движки рабочих процессов состоят из оркестратора и исполнителя. Оркестратор отвечает за планирование задач для выполнения, а исполнитель отвечает за выполнение задач. Выполнение начинается, когда рабочий процесс запускается. Оркестратор инициирует сам рабочий процесс, если пользователи определили расписание, основанное на времени, например выполнение каждый час. Также запуск выполнения рабочего процесса могут инициировать внешние источники, такие как веб-сервис или даже человек. После запуска вызываются исполнители для выполнения задач.</p>
<p>Существует множество видов движков рабочих процессов, которые решают разные задачи. Некоторые, такие как Airflow, Dagster и Prefect, интегрируются с системами данных и оркестрируют ETL-задачи. Другие, такие как Camunda и Orkes, предоставляют графическую нотацию для рабочих процессов (например, BPMN, используемую на рисунке 5-7), чтобы не-инженеры могли проще определять и выполнять рабочие процессы. Третьи, такие как Temporal и Restate, обеспечивают долговременное выполнение.</p>
<h3>Долговременное выполнение</h3>
<p>Фреймворки долговременного выполнения стали популярным способом построения архитектур на основе сервисов, которым требуется транзакционность. В нашем примере с платежами мы хотим обработать каждый платёж ровно один раз. Сбой во время выполнения рабочего процесса может привести к списанию с кредитной карты без соответствующего зачисления средств на банковский счёт. В архитектуре на основе сервисов мы не можем просто обернуть эти две задачи в транзакцию базы данных. Более того, мы можем взаимодействовать с внешними платёжными шлюзами, над которыми у нас ограниченный контроль.</p>
<p><strong>Фреймворки долговременного выполнения</strong> — это способ обеспечить семантику «ровно один раз» для рабочих процессов. Если задача завершается сбоем, фреймворк перезапустит задачу, но пропустит любые RPC-вызовы или изменения состояния, которые задача успешно выполнила до сбоя. Вместо этого фреймворк «притворится», что совершает вызов, но вернёт результаты из предыдущего вызова. Это возможно потому, что фреймворки долговременного выполнения записывают все RPC и изменения состояния в надёжное хранилище, например в журнал предварительной записи (write-ahead log).</p>
<p><strong>Пример 5-5. Фрагмент определения рабочего процесса в Temporal для платёжного процесса, показанного на рисунке 5-7.</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">@workflow.defn
class PaymentWorkflow:
    @workflow.run
    async def run(self, payment: PaymentRequest) -&gt; PaymentResult:
        is_fraud = await workflow.execute_activity(
            check_fraud,
            payment,
            start_to_close_timeout=timedelta(seconds=15),
        )
        if is_fraud:
            return PaymentResultFraudulent
        credit_card_response = await workflow.execute_activity(
            debit_credit_card,
            payment,
            start_to_close_timeout=timedelta(seconds=15),
        )
        # ...</pre><p>Фреймворки, такие как Temporal, не лишены своих сложностей. Внешние сервисы, такие как сторонний платёжный шлюз в нашем примере, всё равно должны предоставлять идемпотентный API. Разработчики должны помнить о необходимости использовать уникальные идентификаторы для этих API, чтобы предотвратить повторное выполнение. И поскольку фреймворки долговременного выполнения логируют каждый RPC-вызов по порядку, они ожидают, что последующее выполнение будет совершать те же RPC-вызовы в том же порядке. Это делает изменения в коде хрупкими. Вы можете внести неопределённое поведение просто изменив порядок вызовов функций.</p>
<p>Аналогично, поскольку фреймворки долговременного выполнения ожидают воспроизведения всего кода детерминированным образом (одни и те же входные данные дают одни и те же выходные), недетерминированный код, такой как генераторы случайных чисел или системные часы, является проблемой. Фреймворки часто предоставляют собственные, детерминированные реализации таких библиотечных функций, но нужно помнить, чтобы именно их использовать. В некоторых случаях, таких как инструмент workflowcheck в Temporal, фреймворки предоставляют статические анализаторы, чтобы определить, введено ли недетерминированное поведение.</p>
<hr />
<p><strong>ПРИМЕЧАНИЕ</strong><br />
Сделать код детерминированным — мощная идея, но трудно реализуемая надёжным образом.</p>
<hr />
<h2>Архитектуры, управляемые событиями</h2>
<p>В этом заключительном разделе мы кратко рассмотрим архитектуры, управляемые событиями, которые представляют собой ещё один способ передачи закодированных данных от одного процесса к другому. Запрос называется событием или сообщением; в отличие от RPC, отправитель обычно не ждёт, пока получатель обработает событие. Более того, события, как правило, не отправляются получателю через прямое сетевое соединение, а проходят через посредника, называемого брокером сообщений (также event broker, message queue или message-oriented middleware), который временно сохраняет сообщение.</p>
<p><strong>Использование брокера сообщений имеет несколько преимуществ по сравнению с прямым RPC:</strong></p>
<ul>
<li>Он может выступать в роли буфера, если получатель недоступен или перегружен, тем самым повышая надёжность системы.</li>
<li>Он может автоматически повторно доставлять сообщения процессу, который вышел из строя, и тем самым предотвращает их потерю.</li>
<li>Он устраняет необходимость в обнаружении сервисов, так как отправителям не нужно напрямую подключаться к IP-адресу получателя.</li>
<li>Он позволяет отправить одно и то же сообщение нескольким получателям.</li>
<li>Он логически отделяет отправителя от получателя (отправитель просто публикует сообщения и не заботится о том, кто их потребляет).</li>
</ul>
<p>Общение через брокера сообщений является асинхронным: отправитель не ждёт доставки сообщения, а просто отправляет его и забывает. Однако можно реализовать синхронную модель, похожую на RPC, если заставить отправителя ждать ответа на отдельном канале.</p>
<h3>Брокеры сообщений</h3>
<p>В прошлом рынок брокеров сообщений был под контролем коммерческого корпоративного ПО от компаний, таких как TIBCO, IBM WebSphere и webMethods, прежде чем популярность приобрели открытые реализации, такие как RabbitMQ, ActiveMQ, HornetQ, NATS и Apache Kafka. Совсем недавно получили распространение облачные сервисы, такие как Amazon Kinesis, Azure Service Bus и Google Cloud Pub/Sub.</p>
<p>Подробная семантика доставки зависит от реализации и конфигурации, но в целом чаще всего используются два шаблона распределения сообщений:</p>
<p>Один процесс добавляет сообщение в именованную очередь, и брокер доставляет это сообщение потребителю этой очереди. Если потребителей несколько, сообщение получает один из них.</p>
<p>Один процесс публикует сообщение в именованную тему, и брокер доставляет это сообщение всем подписчикам этой темы. Если подписчиков несколько, сообщение получает каждый.</p>
<p>Брокеры сообщений, как правило, не навязывают какой-либо конкретной модели данных — сообщение представляет собой просто последовательность байтов с некоторыми метаданными, поэтому можно использовать любой формат кодирования. Общий подход — использовать Protocol Buffers, Avro или JSON, а вместе с брокером сообщений развёртывать реестр схем, чтобы хранить все допустимые версии схем и проверять их совместимость. Также можно использовать AsyncAPI, эквивалент OpenAPI для обмена сообщениями, чтобы задавать схему сообщений.</p>
<p>Брокеры сообщений различаются по степени долговечности хранения сообщений. Многие записывают сообщения на диск, чтобы они не были потеряны в случае сбоя брокера или необходимости его перезапуска. В отличие от баз данных, многие брокеры сообщений автоматически удаляют сообщения после того, как они были потреблены. Некоторые брокеры можно настроить на хранение сообщений бессрочно — это требуется, если вы хотите использовать event sourcing (см. “Event Sourcing and CQRS”).</p>
<p>Если потребитель публикует сообщения повторно в другую тему, возможно, потребуется позаботиться о сохранении неизвестных полей, чтобы предотвратить проблему, описанную ранее в контексте баз данных (рисунок 5-1).</p>
<h3>Распределённые акторные фреймворки</h3>
<p><strong>Модель акторов</strong> — это модель программирования для организации параллелизма в рамках одного процесса. Вместо того чтобы работать напрямую с потоками (и связанными с ними проблемами гонок, блокировок и взаимоблокировок), логика инкапсулируется в акторах. Каждый актор обычно представляет одного клиента или сущность, может иметь некоторое локальное состояние (которое не разделяется ни с какими другими акторами) и обменивается сообщениями с другими акторами посредством отправки и получения асинхронных сообщений. Доставка сообщений не гарантируется: в определённых сценариях ошибок сообщения будут потеряны. Так как каждый актор обрабатывает только одно сообщение за раз, ему не нужно беспокоиться о потоках, и каждый актор может планироваться независимо фреймворком.</p>
<p>В распределённых акторных фреймворках, таких как Akka, Orleans и Erlang/OTP, эта модель программирования используется для масштабирования приложения на несколько узлов. Механизм передачи сообщений используется тот же, независимо от того, находятся ли отправитель и получатель на одном узле или на разных. Если они находятся на разных узлах, сообщение прозрачно кодируется в последовательность байтов, отправляется по сети и декодируется на другой стороне.</p>
<p>Прозрачность расположения работает лучше в акторной модели, чем в RPC, потому что акторная модель уже предполагает, что сообщения могут быть потеряны даже в пределах одного процесса. Хотя задержка по сети, вероятно, выше, чем в пределах одного процесса, при использовании акторной модели существует меньше фундаментального несоответствия между локальной и удалённой коммуникацией.</p>
<p>Распределённый акторный фреймворк по сути объединяет брокер сообщений и модель акторов в единый фреймворк. Однако если вы хотите выполнять пошаговые обновления вашего акторного приложения, вам всё равно нужно учитывать прямую и обратную совместимость, поскольку сообщения могут отправляться с узла, работающего на новой версии, на узел со старой версией, и наоборот. Это можно реализовать с помощью одного из форматов кодирования, рассмотренных в этой главе.</p>
<h1>Резюме по главе 5</h1>
<p>В этой главе мы рассмотрели несколько способов преобразования структур данных в байты в сети или байты на диске. Мы увидели, что детали этих кодировок влияют не только на их эффективность, но, что более важно, на архитектуру приложений и ваши возможности по их развитию.</p>
<p>В частности, многим сервисам необходимо поддерживать пошаговые обновления, при которых новая версия сервиса развёртывается постепенно на нескольких узлах, а не сразу на всех. Пошаговые обновления позволяют выпускать новые версии сервиса без простоев (тем самым поощряя частые небольшие релизы вместо редких крупных) и делают развёртывания менее рискованными (так как ошибочные релизы могут быть обнаружены и откатаны до того, как они затронут большое количество пользователей). Эти свойства чрезвычайно полезны для эволюционности — простоты внесения изменений в приложение.</p>
<p>Во время пошаговых обновлений или по разным другим причинам мы должны предполагать, что разные узлы запускают разные версии кода нашего приложения. Таким образом, важно, чтобы все данные, циркулирующие в системе, кодировались таким образом, чтобы обеспечивать обратную совместимость (новый код может читать старые данные) и прямую совместимость (старый код может читать новые данные).</p>
<p>Мы обсудили несколько форматов кодирования данных и их свойства совместимости:</p>
<ul>
<li>Зависимые от языка программирования кодировки ограничены одним языком и часто не обеспечивают прямой и обратной совместимости.</li>
<li><strong>Текстовые форматы</strong> вроде JSON, XML и CSV широко распространены, и их совместимость зависит от того, как именно вы их используете. У них есть необязательные языки схем, которые иногда помогают, а иногда мешают. Эти форматы несколько расплывчаты в отношении типов данных, поэтому нужно быть осторожным с числами и бинарными строками.</li>
<li><strong>Бинарные форматы</strong>, управляемые схемами, такие как Protocol Buffers и Avro, позволяют выполнять компактное и эффективное кодирование с чётко определёнными правилами прямой и обратной совместимости. Схемы могут быть полезны для документации и генерации кода в статически типизированных языках. Однако у этих форматов есть недостаток: данные нужно декодировать, прежде чем они станут читаемыми человеком.</li>
</ul>
<p>Мы также обсудили несколько моделей передачи данных, показывающих различные сценарии, в которых кодировки данных имеют значение:</p>
<ul>
<li>Базы данных, где процесс записи в базу кодирует данные, а процесс чтения из базы их декодирует.</li>
<li>RPC и REST API, где клиент кодирует запрос, сервер декодирует запрос и кодирует ответ, а клиент в конце концов декодирует ответ.</li>
<li>Архитектуры, управляемые событиями (с использованием брокеров сообщений или акторов), где узлы обмениваются сообщениями, закодированными отправителем и декодированными получателем.</li>
</ul>
<p>Мы можем заключить, что при должном внимании обратная/прямая совместимость и пошаговые обновления вполне достижимы. Пусть эволюция вашего приложения будет быстрой, а развёртывания — частыми.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-5-encoding-and-evolution/">Глава 5. Кодирование и Эволюция (Encoding and Evolution)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-5-encoding-and-evolution/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 4. Хранение и извлечение</title>
		<link>https://datatalks.ru/chapter-4-storage-and-retrieval/</link>
					<comments>https://datatalks.ru/chapter-4-storage-and-retrieval/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 31 Aug 2025 10:44:35 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1956</guid>

					<description><![CDATA[<p>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта Глава 4. Хранение и извлечение данных Одна из бед жизни в том, что все называют вещи чуть-чуть неправильно. И из-за этого всё в мире оказывается чуть труднее понять, чем могло бы быть, если бы названия были другими. Компьютер в первую очередь не вычисляет в [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-4-storage-and-retrieval/">Глава 4. Хранение и извлечение</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта</em></p>
<h1>Глава 4. Хранение и извлечение данных</h1>
<blockquote><p>Одна из бед жизни в том, что все называют вещи чуть-чуть неправильно. И из-за этого всё в мире оказывается чуть труднее понять, чем могло бы быть, если бы названия были другими. Компьютер в первую очередь не вычисляет в смысле арифметики. В первую очередь это системы хранения.</p>
<p><strong>Ричард Фейнман, семинар Idiosyncratic Thinking (1985)</strong></p></blockquote>
<hr />
<p>На самом базовом уровне база данных должна уметь делать две вещи: когда вы ей даёте данные — она должна их сохранить, а когда вы попросите их позже — она должна их вам вернуть.</p>
<p>В Главе 3 мы обсуждали модели данных и языки запросов, то есть формат, в котором вы отдаёте базе свои данные, и интерфейс, через который вы их потом извлекаете. В этой главе мы посмотрим на те же вещи с точки зрения базы данных: как она может хранить переданные данные и как находить их снова по запросу.</p>
<p><strong>Почему вам, как разработчику приложений, стоит заботиться о том, как база данных реализует хранение и поиск внутри себя?</strong> Вы вряд ли будете писать собственный сторедж-энджин с нуля, но вам придётся выбрать движок хранения, подходящий для вашего приложения, из множества доступных. Чтобы правильно сконфигурировать движок хранения под вашу нагрузку, необходимо хотя бы примерно понимать, что он делает «под капотом».</p>
<p>В частности, есть большая разница между движками, <strong>оптимизированными под транзакционные нагрузки (OLTP)</strong>, и <strong>теми, что оптимизированы под аналитику (OLAP)</strong>. Эта глава начнётся с рассмотрения двух семейств движков для OLTP: <strong>лог-структурированных движков</strong>, которые пишут неизменяемые файловые сегменты, и <strong>движков на основе B-деревьев</strong>, которые обновляют данные на месте. Эти структуры используются как для key-value хранения, так и для вторичных индексов.</p>
<p>Позже, в разделе «Хранение данных для аналитики», мы рассмотрим семейство движков, оптимизированных для аналитики, а в разделе «Многомерные и полнотекстовые индексы» — кратко заглянем в индексы для более сложных запросов, например полнотекстового поиска.</p>
<h1>Хранение и индексация для OLTP</h1>
<p>Представим себе самую простую базу данных в мире, реализованную в виде двух функций на Bash:</p><pre class="urvanov-syntax-highlighter-plain-tag">#!/bin/bash

db_set () {
    echo "$1,$2" &gt;&gt; database
}

db_get () {
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}</pre><p><strong>Эти две функции реализуют key-value storage.</strong> Можно вызвать <code>db_set key value</code>, и это сохранит ключ и значение в базе. Ключ и значение могут быть (почти) любыми — например, значением может быть JSON-документ. Затем можно вызвать <code>db_get key</code>, чтобы получить последнее значение, связанное с данным ключом.</p>
<p><strong>И это работает:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">$ db_set 12 '{"name":"London","attractions":["Big Ben","London Eye"]}'

$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'

$ db_get 42
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}</pre><p>Формат хранения очень простой: текстовый файл, где каждая строка содержит пару ключ-значение, разделённую запятой (примерно как CSV, если не учитывать экранирование). Каждый вызов <code>db_set</code> просто дописывает строку в конец файла. Если вы обновляете один и тот же ключ несколько раз, старые значения не перезаписываются — чтобы найти последнее значение, нужно взять последнюю строку с этим ключом в файле (поэтому в <code>db_get</code> используется <code>tail -n 1</code>):</p><pre class="urvanov-syntax-highlighter-plain-tag">$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'

$ db_get 42
{"name":"San Francisco","attractions":["Exploratorium"]}

$ cat database
12,{"name":"London","attractions":["Big Ben","London Eye"]}
42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
42,{"name":"San Francisco","attractions":["Exploratorium"]}</pre><p>Функция <code>db_set</code> на самом деле работает довольно быстро для такой простой реализации, потому что дописывание в конец файла — операция обычно очень эффективная. Подобно этому, многие базы данных внутри используют лог — файл с данными только для добавления. В реальных базах приходится решать больше проблем (например, обработка конкурентных записей, освобождение дискового пространства, чтобы лог не рос бесконечно, и восстановление после краша при частично записанных данных), но базовый принцип тот же. Логи невероятно полезны, и мы будем сталкиваться с ними ещё не раз в этой книге.</p>
<hr />
<p><strong>Примечание</strong></p>
<p>Слово <strong>лог</strong> часто употребляется для обозначения <code>application logs</code> — текстовых сообщений приложения о происходящем. В этой книге <strong>лог</strong> понимается шире: как последовательность записей на диске, доступная только для добавления. Он может вовсе не быть читаемым человеком; это может быть бинарный формат, предназначенный только для внутреннего использования СУБД.</p>
<hr />
<p>С другой стороны, <code>db_get</code> имеет ужасную производительность, если в базе много записей. Каждый раз при поиске ключа <code>db_get</code> должен просканировать весь файл базы от начала до конца, находя все вхождения ключа. В терминах алгоритмов, стоимость поиска — <strong>O(n)</strong>: если удвоить количество записей n в базе, поиск займёт в два раза больше времени. Это плохо.</p>
<p>Чтобы эффективно находить значение по конкретному ключу, нужна другая структура данных — <strong>индекс</strong>. В этой главе мы рассмотрим разные структуры индексов и сравним их; общая идея в том, чтобы структурировать данные особым образом (например, отсортировать по ключу), что позволяет быстрее находить нужное. Если нужно искать одни и те же данные разными способами, могут потребоваться несколько индексов по разным частям данных.</p>
<p><strong>Индекс</strong> — это дополнительная структура, производная от основных данных. Многие базы позволяют добавлять и удалять индексы, и это не влияет на содержимое базы, только на производительность запросов. Поддержка дополнительных структур создаёт накладные расходы, особенно на запись. Для записи очень трудно превзойти производительность простого дописывания в файл, потому что это самая простая операция записи. Любой индекс почти всегда замедляет запись, так как при каждой записи данных необходимо обновлять и индекс.</p>
<p>Это важный компромисс в системах хранения: правильно выбранные индексы ускоряют чтение, но каждый индекс потребляет дополнительное место на диске и замедляет запись, иногда значительно. Поэтому базы обычно не индексируют всё подряд по умолчанию, а требуют от вас — разработчика приложения или администратора БД — выбирать индексы вручную, исходя из типичных паттернов запросов. Так вы сможете подобрать индексы, которые дадут максимальный выигрыш для вашего приложения, не внося лишней нагрузки на операции записи.</p>
<h2>Лог-структурированное хранение</h2>
<p>Начнём с предположения, что вы хотите продолжать хранить данные в <strong>append-only</strong> файле, который пишет <code>db_set</code>, но ускорить чтение. Один из способов — хранить в памяти <strong>hash map</strong>, где каждому ключу сопоставлен байтовый <strong>offset</strong> в файле, по которому лежит последнее значение для этого ключа, как показано на рисунке 4-1.</p>
<p><strong>Рисунок 4-1. Хранение лога пар ключ-значение в формате, похожем на CSV, индексированного с помощью хэш-таблицы в памяти</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2086" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1.png" alt="" width="1772" height="872" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-300x148.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-1024x504.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-768x378.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-1536x756.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-450x221.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-780x384.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_1-1600x787.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>Каждый раз, когда вы дописываете новую пару <strong>ключ-значение</strong> в файл, вы также обновляете <strong>хэш-таблицу</strong> так, чтобы она отражала смещение данных, только что записанных. Когда вы хотите получить значение, вы используете хэш-таблицу, чтобы найти смещение в лог-файле, перемещаетесь к этой позиции и читаете значение. Если эта часть файла данных уже находится в файловом кэше, чтение вообще не требует дисковых операций ввода-вывода.</p>
<p>Такой подход намного быстрее, но у него всё же <strong>есть несколько проблем</strong>:</p>
<ul>
<li>Никогда не освобождается место на диске, занимаемое старыми лог-записями, которые были перезаписаны; если продолжать писать в базу, место на диске может закончиться.</li>
<li>Хэш-таблица не сохраняется, поэтому её приходится перестраивать при перезапуске базы — например, сканированием всего лог-файла, чтобы найти последнее смещение для каждого ключа. Это делает перезапуск очень медленным, если данных много.</li>
<li>Хэш-таблица должна помещаться в память. Теоретически можно было бы хранить её на диске, но, к сожалению, сделать дисковую хэш-таблицу производительной очень трудно. Она требует большого количества случайных операций ввода-вывода, дорого обходится при увеличении размера и сталкивается с коллизиями хэшей, для которых нужна сложная логика.</li>
<li>Диапазонные запросы неэффективны. Например, нельзя просто просканировать все ключи между 10000 и 19999 — придётся искать каждый ключ отдельно в хэш-таблице.</li>
</ul>
<h3>Формат файла SSTable</h3>
<p>На практике хэш-таблицы для индексов баз данных используются не очень часто; вместо этого гораздо чаще данные хранятся в структуре, отсортированной по ключу. Пример такой структуры — <strong>Sorted String Table, или сокращённо SSTable</strong>, как показано на рисунке 4-2. Этот формат файла также хранит пары ключ-значение, но гарантирует, что они отсортированы по ключу, и каждый ключ встречается в файле только один раз.</p>
<p><strong>Рисунок 4-2. SSTable с разреженным индексом, позволяющим запросам сразу переходить к нужному блоку</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2087" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-scaled.png" alt="" width="2560" height="1184" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-300x139.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-1024x473.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-768x355.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-1536x710.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-2048x947.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-450x208.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-780x361.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_2-1600x740.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Теперь нет необходимости держать все ключи в памяти: пары ключ-значение внутри SSTable можно сгруппировать в блоки размером в несколько килобайт, а затем сохранить в индексе только первый ключ каждого блока. <strong>Такой индекс, который хранит лишь часть ключей, называется разреженным.</strong> Этот индекс хранится в отдельной части SSTable, например, с использованием неизменяемого <strong>B-дерева</strong>, <strong>префиксного дерева (trie)</strong> или другой структуры данных, позволяющей быстро искать ключ.</p>
<p>Например, на рисунке 4-2 первый ключ одного блока — <strong>handbag</strong>, а первого ключа следующего блока — <strong>handsome</strong>. Теперь допустим, вы ищете ключ <strong>handiwork</strong>, которого нет в разреженном индексе. Благодаря сортировке вы знаете, что <strong>handiwork</strong> должен находиться между <strong>handbag</strong> и <strong>handsome</strong>. Это значит, что можно перейти к смещению для <strong>handbag</strong> и просканировать файл оттуда, пока не будет найден <strong>handiwork</strong> (или до конца блока, если ключа нет в файле). Блок в несколько килобайт можно просканировать очень быстро.</p>
<p>Кроме того, каждый блок записей может быть сжат (что показано затемнённой областью на рисунке 4-2). Помимо экономии места на диске, сжатие также уменьшает использование пропускной способности ввода-вывода, ценой небольшой дополнительной загрузки CPU.</p>
<h3>Построение и слияние SSTable</h3>
<p><strong>Формат файла SSTable лучше подходит для чтения, чем лог с дозаписью, но он усложняет запись.</strong> Мы не можем просто дописать новый ключ в конец, потому что файл перестанет быть отсортированным (если только ключи случайно не приходят в возрастающем порядке). Если бы нам приходилось переписывать весь SSTable при каждой вставке ключа где-то в середину, то записи стали бы слишком дорогими.</p>
<p>Эта проблема решается <strong>лог-структурированным подходом</strong> — гибридом между логом только для добавления и отсортированным файлом:</p>
<ul>
<li>Когда приходит запись, она добавляется в <strong>упорядоченную in-memory структуру данных</strong>, такую как <strong>красно-чёрное дерево</strong>, <strong>skip list</strong> или <strong>trie</strong>. В этих структурах можно вставлять ключи в любом порядке, эффективно их искать и извлекать обратно в отсортированном порядке. Эта структура в памяти называется <code>memtable</code>.</li>
<li>Когда <code>memtable</code> вырастает больше некоторого порога — обычно несколько мегабайт — её сбрасывают на диск в отсортированном виде как файл <strong>SSTable</strong>. Такой новый SSTable называется <strong>самым свежим сегментом базы данных</strong> и хранится как отдельный файл рядом со старыми сегментами. У каждого сегмента свой собственный индекс содержимого. Пока новый сегмент пишется на диск, база может продолжать работать с новым экземпляром <strong>memtable</strong>, а память старого <strong>memtable</strong> освобождается после завершения записи <strong>SSTable</strong>.</li>
<li>Чтобы прочитать значение для ключа, сначала ищут ключ в memtable и в самом новом сегменте на диске. Если его там нет — ищут в следующем, более старом сегменте, и так далее, пока либо ключ не будет найден, либо не будет достигнут самый старый сегмент. Если ключ не встречается ни в одном сегменте, его нет в базе.</li>
<li>Время от времени в фоне запускается процесс слияния и уплотнения сегментов, чтобы объединять файлы сегментов и удалять перезаписанные или удалённые значения.</li>
</ul>
<p>Слияние сегментов работает аналогично <strong>алгоритму mergesort</strong>. Этот процесс показан на рисунке 4-3: начинаем чтение входных файлов параллельно, смотрим на первый ключ в каждом файле, копируем в выходной файл наименьший ключ (согласно порядку сортировки) и повторяем. Если один и тот же ключ встречается в нескольких входных файлах, сохраняется только самое свежее значение. В результате получается новый объединённый сегментный файл, также отсортированный по ключу, с одним значением на ключ. При этом используется минимальное количество памяти, так как мы можем итерироваться по SSTable построчно, по одному ключу за раз.</p>
<p><strong>Рисунок 4-3. Слияние нескольких сегментов SSTable с сохранением только самого свежего значения для каждого ключа</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2088" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-scaled.png" alt="" width="2560" height="1707" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-300x200.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-1024x683.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-768x512.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-1536x1024.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-2048x1365.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-450x300.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-780x520.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_3-1600x1067.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Чтобы гарантировать, что данные в <strong>memtable</strong> не будут потеряны при сбое базы, сторедж-энджин ведёт отдельный лог на диске, куда каждая запись немедленно дописывается. Этот лог не отсортирован по ключу, но это и не нужно, так как его единственная цель — восстановить <strong>memtable</strong> после сбоя. Каждый раз, когда <strong>memtable</strong> был сброшен в <strong>SSTable</strong>, соответствующая часть лога может быть удалена.</p>
<p>Если нужно удалить ключ и его значение, в файл данных добавляется специальная запись удаления, называемая <strong>tombstone</strong>. При слиянии сегментов <strong>tombstone</strong> сообщает процессу слияния удалить все предыдущие значения для удалённого ключа. Как только tombstone попадёт в самый старый сегмент, он тоже может быть удалён.</p>
<p>Алгоритм, описанный здесь, по сути используется в <strong>RocksDB</strong>, <strong>Cassandra</strong>, <strong>Scylla</strong> и <strong>HBase</strong>, все они были вдохновлены статьёй <strong>Google о Bigtable</strong> (в которой были введены термины <strong>SSTable</strong> и <strong>memtable</strong>).</p>
<p>Впервые этот алгоритм был опубликован в 1996 году под названием <strong>Log-Structured Merge-Tree</strong> или <strong>LSM-Tree</strong>, опираясь на более ранние работы о лог-структурированных файловых системах. Поэтому движки хранения, основанные на принципе слияния и уплотнения отсортированных файлов, часто называют <strong>LSM storage engines</strong>.</p>
<p>В <strong>LSM-движках</strong> сегментный файл пишется за один проход (либо при записи memtable, либо при слиянии существующих сегментов), и после этого он неизменяемый. Слияние и уплотнение сегментов могут выполняться в фоне, и пока это происходит, можно продолжать обслуживать чтения, используя старые сегментные файлы. Когда процесс слияния завершён, запросы на чтение переключаются на новый объединённый сегмент, а старые сегментные файлы могут быть удалены.</p>
<p><strong>Сегментные файлы</strong> не обязательно хранить на локальном диске: они также хорошо подходят для записи в объектное хранилище. Например, такой подход используют <strong>SlateDB</strong> и <strong>Delta Lake</strong>.</p>
<p>Наличие неизменяемых сегментных файлов также упрощает восстановление после сбоев: если сбой произошёл во время записи <strong>memtable</strong> или во время слияния сегментов, база может просто удалить незаконченную <strong>SSTable</strong> и начать заново. Лог, фиксирующий записи в <strong>memtable</strong>, может содержать неполные записи, если сбой произошёл в середине записи или если диск был переполнен; такие ситуации обычно выявляются с помощью контрольных сумм в логе, и повреждённые или неполные записи отбрасываются.</p>
<h3><strong>Фильтры Блума</strong></h3>
<p>При использовании LSM-хранилища может быть медленно читать ключ, который был обновлён очень давно, или которого не существует, поскольку движку хранения нужно проверять несколько файлов сегментов. Чтобы ускорить такие чтения, LSM-движки хранения часто включают в каждый сегмент фильтр Блума, который обеспечивает быстрый, но приближённый способ проверки того, встречается ли конкретный ключ в конкретной SSTable.</p>
<p>Рисунок 4-4 показывает пример фильтра Блума, содержащего два ключа и 16 бит (на самом деле он содержал бы больше ключей и больше бит). Для каждого ключа в SSTable мы вычисляем хэш-функцию, которая выдаёт набор чисел, интерпретируемых как индексы в массиве битов. Биты по этим индексам устанавливаются в 1, остальные остаются равными 0. Например, ключ handbag хэшируется в числа (2, 9, 4), поэтому мы устанавливаем 2-й, 9-й и 4-й биты в 1. Получившаяся битовая карта сохраняется как часть SSTable вместе с разреженным индексом ключей. Это занимает немного дополнительного места, но фильтр Блума обычно очень мал по сравнению с остальной частью SSTable.</p>
<p><strong>Рисунок 4-4. Фильтр Блума обеспечивает быстрый вероятностный способ проверить, существует ли конкретный ключ в конкретной SSTable</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2090" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-scaled.png" alt="" width="2560" height="1095" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-300x128.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-1024x438.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-768x329.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-1536x657.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-2048x876.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-450x192.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-780x334.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_4-1600x684.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Когда мы хотим узнать, содержится ли ключ в SSTable, мы вычисляем тот же хэш этого ключа, что и раньше, и проверяем биты по соответствующим индексам. Например, на рисунке 4-4 мы ищем ключ handheld, который хэшируется в (6, 11, 2). Один из этих битов равен 1 (а именно бит номер 2), а два других равны 0. Эти проверки можно выполнять чрезвычайно быстро с использованием побитовых операций, которые поддерживаются всеми CPU.</p>
<p>Если хотя бы один из битов равен 0, мы точно знаем, что ключ в SSTable отсутствует. Если же все биты равны 1, скорее всего, ключ есть в SSTable, но возможно, что случайно все эти биты были установлены другими ключами. Этот случай, когда кажется, что ключ присутствует, хотя на самом деле его нет, называется ложноположительным результатом.</p>
<p>Вероятность ложноположительных результатов зависит от количества ключей, количества бит, установленных на каждый ключ, и общего количества бит в фильтре Блума. Для подбора параметров под ваше приложение можно использовать онлайн-калькулятор. В качестве правила большого пальца: необходимо выделять 10 бит пространства фильтра Блума на каждый ключ в SSTable, чтобы вероятность ложноположительного результата составляла 1%; при этом вероятность уменьшается в 10 раз за каждые дополнительные 5 бит на ключ.</p>
<p>В контексте LSM-движков хранения ложноположительные результаты не являются проблемой:</p>
<p>Если фильтр Блума говорит, что ключ отсутствует, мы можем безопасно пропустить эту SSTable, так как можем быть уверены, что она не содержит ключ.</p>
<p>Если фильтр Блума говорит, что ключ присутствует, нужно обратиться к разреженному индексу и декодировать блок пар ключ-значение, чтобы проверить, действительно ли ключ там есть. Если это был ложноположительный результат, мы просто сделали немного лишней работы, но никакого вреда — продолжаем поиск в следующем, более старом сегменте.</p>
<h3><strong>Стратегии уплотнения (Compaction strategies)</strong></h3>
<p>Важная деталь заключается в том, как LSM-хранилище выбирает момент для выполнения уплотнения и какие SSTable включать в compaction. Многие системы на основе LSM позволяют настраивать стратегию уплотнения, и некоторые из распространённых вариантов такие:</p>
<ul>
<li><strong>Уплотнение по размеру (size-tiered compaction).</strong><br />
Более новые и меньшие SSTable последовательно сливаются в более старые и большие SSTable. SSTable, содержащие старые данные, могут становиться очень большими, и их слияние требует большого объёма временного дискового пространства. Преимущество этой стратегии в том, что она может выдерживать очень высокую скорость записи.</li>
<li><strong>Уплотнение по уровням (leveled compaction).</strong><br />
Диапазон ключей делится на меньшие SSTable, а старые данные перемещаются в отдельные «уровни», что позволяет выполнять уплотнение более постепенно и использовать меньше места на диске, чем при стратегии size-tiered. Эта стратегия эффективнее для чтения, чем уплотнение по размеру, потому что движку хранения нужно читать меньше SSTable, чтобы проверить, содержат ли они ключ.</li>
</ul>
<p>В качестве правила большого пальца: уплотнение по размеру работает лучше, если у вас в основном записи и мало чтений, а уплотнение по уровням работает лучше, если нагрузка в основном читающая. Если вы часто записываете небольшое количество ключей и редко — большое количество ключей, то уплотнение по уровням также может быть выгодной.</p>
<p>Хотя существует много нюансов, базовая идея LSM-деревьев — хранить каскад SSTable, которые сливаются в фоне — проста и эффективна. Мы подробнее рассмотрим их характеристики производительности в разделе «Сравнение B-деревьев и LSM-деревьев».</p>
<h3><strong>Встроенные движки хранения</strong></h3>
<p>Многие базы данных работают как сервис, принимающий запросы по сети, но существуют и встроенные базы данных, которые не предоставляют сетевой API. Вместо этого это библиотеки, работающие в том же процессе, что и ваш код приложения, обычно читающие и пишущие файлы на локальном диске, и вы взаимодействуете с ними через обычные вызовы функций. Примеры встроенных движков хранения: RocksDB, SQLite, LMDB, DuckDB и KùzuDB.</p>
<p>Встроенные базы данных очень часто используются в мобильных приложениях для хранения локальных пользовательских данных. На бэкенде они могут быть подходящим выбором, если данных достаточно мало, чтобы уместиться на одной машине, и если не слишком много конкурентных транзакций. Например, в многопользовательской системе, где каждый арендатор достаточно мал и полностью изолирован от других (т.е. вам не нужно выполнять запросы, объединяющие данные разных арендаторов), можно потенциально использовать отдельный экземпляр встроенной базы данных для каждого арендатора.</p>
<p>Методы хранения и извлечения, обсуждаемые в этой главе, применяются как во встроенных, так и в клиент-серверных базах данных. В главах 6 и 7 мы обсудим техники масштабирования базы данных на несколько машин.</p>
<h2>B-деревья</h2>
<p>Лог-структурированный подход популярен, но это не единственная форма key-value хранения. Наиболее широко используемая структура для чтения и записи записей базы данных по ключу — это B-дерево.</p>
<p>Впервые представленное в 1970 году и названное «повсеместным» менее чем через 10 лет, B-дерево отлично выдержало испытание временем. Оно остаётся стандартной реализацией индекса почти во всех реляционных базах данных, и многие нереляционные базы используют их тоже.</p>
<p>Как и SSTable, B-деревья хранят пары ключ-значение, отсортированные по ключу, что позволяет эффективно выполнять поиск по ключу и диапазонные запросы. Но на этом сходство заканчивается: B-деревья имеют совершенно иную философию проектирования.</p>
<p>Ранее рассмотренные лог-структурированные индексы разбивают базу данных на сегменты переменного размера, обычно несколько мегабайт или больше, которые записываются один раз и затем становятся неизменяемыми. Напротив, B-деревья разбивают базу на блоки или страницы фиксированного размера и могут перезаписывать страницу «на месте». Страница традиционно имеет размер 4 КиБ, но PostgreSQL сейчас использует 8 КиБ, а MySQL по умолчанию — 16 КиБ.</p>
<p>Каждая страница может быть идентифицирована с помощью номера страницы, что позволяет одной странице ссылаться на другую — подобно указателю, но на диске вместо памяти. Если все страницы хранятся в одном файле, то умножив номер страницы на размер страницы, можно получить смещение в байтах в файле, где расположена страница. Эти ссылки на страницы можно использовать для построения дерева страниц, как показано на рисунке 4-5.</p>
<p><strong>Рисунок 4-5. Поиск ключа 251 с использованием индекса B-дерева. От корневой страницы мы сначала переходим по ссылке на страницу для ключей 200–300, затем на страницу для ключей 250–270</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2091" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-scaled.png" alt="" width="2560" height="1418" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-300x166.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-1024x567.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-768x425.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-1536x851.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-2048x1134.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-450x249.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-780x432.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_5-1600x886.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Одна страница назначается корнем B-дерева; каждый раз, когда вы хотите найти ключ в индексе, вы начинаете отсюда. Страница содержит несколько ключей и ссылки на дочерние страницы. Каждый дочерний элемент отвечает за непрерывный диапазон ключей, а ключи между ссылками указывают, где проходят границы между этими диапазонами. (Эта структура иногда называется B+ деревом, но нам не нужно отличать её от других вариантов B-дерева.)</p>
<p>В примере на рисунке 4-5 мы ищем ключ 251, поэтому знаем, что нужно следовать по ссылке на страницу между границами 200 и 300. Это приводит нас на похожую страницу, которая дальше разбивает диапазон 200–300 на поддиапазоны. В итоге мы доходим до страницы, содержащей отдельные ключи (листовой страницы), которая либо содержит значение для каждого ключа inline, либо содержит ссылки на страницы, где можно найти значения.</p>
<p>Количество ссылок на дочерние страницы в одной странице B-дерева называется фактором ветвления. Например, на рисунке 4-5 фактор ветвления равен шести. На практике он зависит от объёма места, необходимого для хранения ссылок на страницы и границ диапазонов, но обычно составляет несколько сотен.</p>
<p>Если нужно обновить значение для существующего ключа в B-дереве, вы находите листовую страницу, содержащую этот ключ, и перезаписываете эту страницу на диске новой версией с обновлённым значением. Если нужно добавить новый ключ, необходимо найти страницу, чей диапазон охватывает этот ключ, и добавить его туда. Если на странице недостаточно свободного места, чтобы вместить новый ключ, страница делится на две наполовину заполненные страницы, и родительская страница обновляется, чтобы учесть новое деление диапазонов ключей.</p>
<p><strong>Рисунок 4-6. Рост B-дерева путём разделения страницы по граничному ключу 337. Родительская страница обновляется так, чтобы ссылаться на оба дочерних элемента</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2092" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-scaled.png" alt="" width="2560" height="1529" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-300x179.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-1024x612.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-768x459.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-1536x918.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-2048x1224.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-450x269.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-780x466.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_6-1600x956.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>В примере на рисунке 4-6 мы хотим вставить ключ 334, но страница для диапазона 333–345 уже заполнена. Поэтому мы делим её на страницу для диапазона 333–337 (включая новый ключ) и страницу для диапазона 337–344. Мы также должны обновить родительскую страницу, чтобы она содержала ссылки на оба дочерних элемента, с граничным значением 337 между ними. Если в родительской странице недостаточно места для новой ссылки, её тоже придётся разделить, и разделения могут продолжаться вплоть до корня дерева. Когда корень делится, мы создаём над ним новый корень. Удаление ключей (которое может требовать слияния узлов) более сложно.</p>
<p>Этот алгоритм гарантирует, что дерево остаётся сбалансированным: B-дерево с n ключами всегда имеет глубину O(log n). Большинство баз данных можно уместить в B-дерево глубиной три или четыре уровня, так что не приходится проходить много ссылок, чтобы найти нужную страницу. (Четырёхуровневое дерево из страниц по 4 КиБ с фактором ветвления 500 может хранить до 250 ТБ.)</p>
<h3><strong>Надёжность B-деревьев</strong></h3>
<p>Базовая операция записи в B-дереве заключается в перезаписи страницы на диске новыми данными. Предполагается, что перезапись не изменяет расположение страницы; т.е. все ссылки на эту страницу остаются корректными после её перезаписи. Это резко контрастирует с индексами на основе журнала, такими как LSM-деревья, которые только добавляют данные в файлы (и в конечном счёте удаляют устаревшие файлы), но никогда не изменяют файлы на месте.<br />
Перезапись нескольких страниц одновременно, как при разделении страницы, — это опасная операция: если база данных аварийно завершится после того, как только часть страниц была записана, в итоге получится повреждённое дерево (например, может появиться «осиротевшая» страница, которая не является дочерней ни для одного родителя).<br />
Чтобы сделать базу данных устойчивой к сбоям, в реализации B-деревьев обычно добавляется дополнительная структура данных на диске: журнал предзаписи (WAL, Write-Ahead Log). Это файл только для добавления, в который каждая модификация B-дерева должна быть записана перед тем, как её можно будет применить к страницам самого дерева. Когда база данных запускается после сбоя, этот журнал используется для восстановления B-дерева в согласованное состояние. В файловых системах эквивалентный механизм называется журналированием.<br />
Для повышения производительности реализации B-деревьев, как правило, не записывают сразу каждую изменённую страницу на диск, а сначала некоторое время буферизуют страницы B-дерева в памяти. Тогда журнал предзаписи также гарантирует, что данные не будут потеряны в случае сбоя: пока данные были записаны в WAL и сброшены на диск с помощью системного вызова fsync(), данные будут надёжными, так как база данных сможет восстановить их после сбоя.</p>
<h3><strong>Варианты B-деревьев</strong></h3>
<p>Так как B-деревья существуют уже очень давно, за эти годы было разработано множество их вариантов. Упомянем лишь некоторые:</p>
<ul>
<li>Вместо перезаписи страниц и поддержки WAL для восстановления после сбоев некоторые базы данных (например, LMDB) используют схему «copy-on-write». Изменённая страница записывается в другое место, и создаётся новая версия родительских страниц дерева, указывающая на новое местоположение. Такой подход также полезен для управления параллелизмом, как мы увидим в разделе «Изоляция снимков и повторяемое чтение».</li>
<li>Можно экономить место на страницах, не сохраняя полный ключ, а сокращая его. Особенно на внутренних страницах дерева ключи должны содержать только достаточно информации, чтобы служить границами между диапазонами ключей. Упаковка большего количества ключей в страницу позволяет дереву иметь больший коэффициент ветвления и, следовательно, меньше уровней.</li>
<li>Чтобы ускорить сканирование диапазона ключей в отсортированном порядке, некоторые реализации B-деревьев пытаются расположить дерево так, чтобы страницы-листья находились на диске в последовательном порядке, уменьшая количество обращений к диску. Однако поддерживать этот порядок по мере роста дерева трудно.</li>
<li>В дерево добавляются дополнительные указатели. Например, каждая страница-лист может иметь ссылки на свои соседние страницы слева и справа, что позволяет сканировать ключи по порядку без возврата к родительским страницам.</li>
</ul>
<h2>Сравнение B-деревьев и LSM-деревьев</h2>
<p>Как правило, LSM-деревья лучше подходят для приложений с интенсивной записью, тогда как B-деревья быстрее при чтении. Однако результаты тестов часто зависят от деталей нагрузки. Чтобы провести корректное сравнение, необходимо тестировать системы именно с вашей нагрузкой. Более того, это не строгое «или/или» между LSM и B-деревьями: движки хранения иногда сочетают характеристики обоих подходов, например, имеют несколько B-деревьев и объединяют их в стиле LSM. В этом разделе мы кратко обсудим несколько вещей, которые стоит учитывать при измерении производительности движка хранения.</p>
<h3><strong>Производительность чтения</strong></h3>
<p>В B-дереве поиск ключа включает чтение одной страницы на каждом уровне B-дерева. Так как количество уровней обычно невелико, это означает, что чтение из B-дерева обычно быстрое и имеет предсказуемую производительность. В LSM-движке чтения часто должны проверять несколько разных SSTable на разных стадиях уплотнения, но фильтры Блума помогают сократить количество фактических операций ввода-вывода. Оба подхода могут работать хорошо, и то, какой быстрее, зависит от деталей движка хранения и нагрузки.<br />
Диапазонные запросы просты и быстры в B-деревьях, так как они используют отсортированную структуру дерева. В LSM-хранилищах диапазонные запросы также могут использовать сортировку в SSTable, но им нужно сканировать все сегменты параллельно и объединять результаты. Фильтры Блума не помогают при диапазонных запросах (так как пришлось бы вычислять хеш для каждого возможного ключа внутри диапазона, что непрактично), поэтому диапазонные запросы обходятся дороже, чем точечные запросы в подходе LSM.<br />
Высокая пропускная способность записей может вызвать всплески задержек в движке хранения на основе журнала, если memtable заполняется. Это происходит, если данные не могут достаточно быстро записываться на диск, возможно, потому что процесс уплотнения не успевает за входящими записями. Многие движки хранения, включая RocksDB, используют механизм обратного давления в этой ситуации: они приостанавливают все чтения и записи до тех пор, пока memtable не будет выгружена на диск.<br />
Что касается пропускной способности чтения, современные SSD (и особенно NVMe) могут выполнять множество независимых запросов на чтение параллельно. И LSM-деревья, и B-деревья способны обеспечивать высокую пропускную способность чтения, но движки хранения должны быть тщательно спроектированы, чтобы использовать это параллельное выполнение.</p>
<h3><strong>Последовательные и случайные записи</strong></h3>
<p>В B-дереве, если приложение записывает ключи, разбросанные по всему пространству ключей, то и операции на диске также оказываются случайно разбросанными, так как страницы, которые нужно перезаписать движку хранения, могут находиться где угодно на диске. С другой стороны, движок хранения на основе журнала записывает целые файлы сегментов за раз (либо выгружая memtable, либо при уплотнении существующих сегментов), которые намного больше страницы в B-дереве.<br />
Шаблон множества мелких, разбросанных записей (как в B-деревьях) называется случайными записями, в то время как шаблон меньшего количества крупных записей (как в LSM-деревьях) называется последовательными записями. Диски в целом имеют более высокую пропускную способность для последовательных записей, чем для случайных, что означает, что движок хранения на основе журнала обычно способен выдерживать более высокую нагрузку на запись на том же оборудовании, чем B-дерево. Эта разница особенно велика на жёстких дисках (HDD); на твердотельных накопителях (SSD), которые используются в большинстве баз данных сегодня, разница меньше, но всё же заметна (см. «Последовательные и случайные записи на SSD»).</p>
<p><strong>ПОСЛЕДОВАТЕЛЬНЫЕ И СЛУЧАЙНЫЕ ЗАПИСИ НА SSD</strong></p>
<p>На жёстких дисках (HDD) последовательные записи намного быстрее случайных: случайная запись должна механически переместить головку диска в новое положение и подождать, пока нужная часть пластины пройдёт под головкой диска, что занимает несколько миллисекунд — вечность по меркам вычислительных масштабов времени. Однако SSD (твердотельные накопители), включая NVMe (Non-Volatile Memory Express, т.е. флеш-память, подключённая к шине PCI Express), теперь обогнали HDD для многих сценариев использования, и они не подвержены таким механическим ограничениям.<br />
Тем не менее, SSD также имеют большую пропускную способность для последовательных записей, чем для случайных. Причина в том, что флеш-память можно читать или записывать по одной странице (обычно 4 КиБ) за раз, но стирать можно только блоками (обычно 512 КиБ) за раз. Некоторые страницы в блоке могут содержать актуальные данные, а другие — данные, которые больше не нужны. Перед тем как стереть блок, контроллер должен сначала переместить страницы с актуальными данными в другие блоки; этот процесс называется сборкой мусора (GC, Garbage Collection).<br />
Последовательная нагрузка записи записывает большие куски данных за раз, поэтому велика вероятность, что весь блок в 512 КиБ принадлежит одному файлу; когда этот файл позже удаляется, весь блок можно стереть без выполнения GC. С другой стороны, при случайной нагрузке записи более вероятно, что блок содержит смесь страниц с актуальными и неактуальными данными, поэтому GC приходится выполнять больше работы, прежде чем блок можно стереть.<br />
Пропускная способность записи, потребляемая GC, недоступна для приложения. Более того, дополнительные записи, выполняемые GC, способствуют износу флеш-памяти; поэтому случайные записи изнашивают накопитель быстрее, чем последовательные.</p>
<h3><strong>Усиление записи (Write amplification)</strong></h3>
<p>В любом типе движка хранения один запрос на запись от приложения превращается во множество операций ввода-вывода на нижележащем диске. В LSM-деревьях значение сначала записывается в журнал для обеспечения надежности, затем снова, когда memtable сбрасывается на диск, и снова каждый раз, когда пара ключ-значение участвует в процессах уплотнения. (Если значения значительно больше ключей, эти накладные расходы можно сократить, сохраняя значения отдельно от ключей и выполняя уплотнение только над SSTable, содержащими ключи и ссылки на значения.)</p>
<p>Индекс B-дерева должен записывать каждый кусок данных как минимум дважды: один раз в журнал предварительной записи и один раз в страницу дерева. Кроме того, иногда необходимо записывать целую страницу, даже если в ней изменилось всего несколько байтов, чтобы гарантировать возможность корректного восстановления B-дерева после сбоя или отключения питания.</p>
<p>Если взять общее количество байтов, записанных на диск при некоторой нагрузке, и разделить на количество байтов, которые пришлось бы записать при использовании простого только-добавляемого журнала без индекса, получится коэффициент усиления записи. (Иногда усиление записи определяется в терминах операций ввода-вывода, а не байтов.) В приложениях с интенсивной записью узким местом может оказаться скорость, с которой база данных способна записывать на диск. В этом случае чем выше усиление записи, тем меньшее количество записей в секунду система может обработать в рамках доступной пропускной способности диска.</p>
<p>Усиление записи является проблемой как в LSM-деревьях, так и в B-деревьях. Какой вариант лучше — зависит от различных факторов, таких как длина ключей и значений, а также частота перезаписи существующих ключей по сравнению с добавлением новых. Для типичных нагрузок LSM-деревья, как правило, имеют меньшее усиление записи, потому что им не нужно переписывать целые страницы, и они могут сжимать блоки SSTable. Это еще один фактор, делающий движки хранения на основе LSM хорошо подходящими для нагрузок с интенсивной записью.</p>
<p>Помимо влияния на пропускную способность, усиление записи также имеет значение для износа SSD: движок хранения с меньшим усилением записи будет изнашивать SSD медленнее.</p>
<p>При измерении пропускной способности записи движка хранения важно запускать эксперимент достаточно долго, чтобы стали заметны эффекты усиления записи. При записи в пустое LSM-дерево уплотнения еще не происходят, поэтому вся пропускная способность диска доступна для новых записей. По мере роста базы данных новые записи должны делить пропускную способность диска с уплотнением.</p>
<h3><strong>Использование дискового пространства (Disk space usage)</strong></h3>
<p>Со временем B-деревья могут становиться фрагментированными: например, если удалить большое количество ключей, файл базы данных может содержать множество страниц, больше не используемых B-деревом. Последующие добавления в B-дерево могут использовать эти свободные страницы, но их нелегко вернуть операционной системе, так как они находятся в середине файла, и поэтому продолжают занимать место в файловой системе. Поэтому базам данных требуется фоновый процесс, который перемещает страницы для их лучшего размещения, например процесс vacuum в PostgreSQL.</p>
<p>Фрагментация является меньшей проблемой в LSM-деревьях, поскольку процесс уплотнения периодически переписывает файлы данных, и SSTable не имеют страниц с неиспользованным пространством. Более того, блоки пар ключ-значение в SSTable лучше поддаются сжатию, и поэтому часто создают меньшие по размеру файлы на диске, чем B-деревья. Ключи и значения, которые были перезаписаны, продолжают занимать место, пока не будут удалены уплотнением (compaction), но эти накладные расходы достаточно низкие при использовании уровневого уплотнения. Уплотнение по уровням использует больше дискового пространства, особенно временно во время самого уплотнения.</p>
<p>Наличие нескольких копий одних и тех же данных на диске также может быть проблемой, если необходимо удалить какие-то данные и быть уверенным, что они действительно удалены (например, для соблюдения требований законодательства о защите данных). Например, в большинстве движков хранения на основе LSM удаленная запись может по-прежнему существовать на верхних уровнях, пока маркер удаления (tombstone), представляющий удаление, не пройдет через все уровни уплотнения, что может занять много времени. Специализированные архитектуры движков хранения могут распространять удаления быстрее.</p>
<p>С другой стороны, неизменяемая природа файлов сегментов SSTable полезна, если необходимо сделать снимок базы данных в определенный момент времени (например, для резервного копирования или создания копии базы данных для тестирования): можно сбросить memtable и зафиксировать, какие файлы сегментов существовали в тот момент времени. Пока файлы, входящие в снимок, не удаляются, их не нужно фактически копировать. В B-дереве, страницы которого перезаписываются, сделать такой снимок эффективно гораздо сложнее.</p>
<h1>Множественные и вторичные индексы (Multi-Column and Secondary Indexes)</h1>
<p>До этого мы рассматривали только индексы по ключу-значению, которые похожи на индекс по первичному ключу в реляционной модели. Первичный ключ однозначно идентифицирует одну строку в реляционной таблице, один документ в документной базе данных или одну вершину в графовой базе данных. Другие записи в базе могут ссылаться на эту строку/документ/вершину по её первичному ключу (или <code>ID</code>), и индекс используется для разрешения таких ссылок.</p>
<p>Также очень часто используются вторичные индексы. В реляционных базах данных можно создать несколько вторичных индексов на одной и той же таблице с помощью команды CREATE INDEX, что позволяет выполнять поиск по столбцам, отличным от первичного ключа. Например, на Рисунке 3-1 в Главе 3 у вас, скорее всего, будет вторичный индекс по столбцам <code>user_id</code>, чтобы можно было находить все строки, принадлежащие одному пользователю, в каждой из таблиц.</p>
<p>Вторичный индекс легко построить из индекса по ключу-значению. Основное отличие состоит в том, что во вторичном индексе индексируемые значения не обязательно уникальны; то есть под одной индексной записью может находиться множество строк (документов, вершин). Это можно решить двумя способами: либо сделать каждое значение в индексе списком идентификаторов совпадающих строк (как список вхождений в полнотекстовом индексе), либо сделать каждую запись уникальной, добавив к ней идентификатор строки. Для реализации индекса могут использоваться как движки хранения с обновлением на месте (например, B-деревья), так и журнально-структурированное хранение.</p>
<h2>Хранение значений внутри индекса (Storing values within the index)</h2>
<p><strong>Ключ в индексе</strong> — это то, по чему выполняется поиск в запросах, но значение может быть одним из нескольких вариантов:</p>
<ul>
<li>Если фактические данные (строка, документ, вершина) хранятся непосредственно внутри структуры индекса, это называется кластерным индексом. Например, в движке хранения InnoDB в MySQL первичный ключ таблицы всегда является кластерным индексом, а в SQL Server можно указать один кластерный индекс для каждой таблицы.</li>
<li>Альтернативно, значением может быть ссылка на фактические данные: либо первичный ключ соответствующей строки (InnoDB делает так для вторичных индексов), либо прямое указание на расположение на диске. В последнем случае место, где хранятся строки, называется heap file (кучей), и оно хранит данные без какого-либо определенного порядка (оно может быть только-добавляемым, либо может отслеживать удаленные строки, чтобы позже перезаписывать их новыми данными). Например, Postgres использует подход с heap file.</li>
<li>Промежуточный вариант между двумя подходами — это покрывающий индекс (covering index) или индекс с включенными столбцами, который хранит некоторые столбцы таблицы внутри индекса, в дополнение к хранению полной строки в куче или в кластерном индексе по первичному ключу. Это позволяет некоторым запросам выполняться только с использованием индекса, без необходимости обращаться к первичному ключу или искать в куче (в таком случае говорят, что индекс «покрывает» запрос). Это может сделать некоторые запросы быстрее, но дублирование данных означает, что индекс занимает больше места на диске и замедляет операции записи.</li>
</ul>
<p>Обсужденные до сих пор индексы сопоставляют только один ключ со значением. Если необходимо выполнять запрос по нескольким столбцам таблицы (или нескольким полям документа) одновременно.</p>
<p>При обновлении значения без изменения ключа подход с <strong>heap file</strong> позволяет перезаписывать запись на месте, при условии, что новое значение не больше старого. Ситуация усложняется, если новое значение больше, так как его, вероятно, придется переместить в новое место в куче, где есть достаточно пространства. В таком случае либо все индексы должны быть обновлены, чтобы указывать на новое место записи в куче, либо в старом месте оставляется <strong>пересылающий указатель (forwarding pointer)</strong>.</p>
<h2>Хранение всего в памяти (Keeping everything in memory)</h2>
<p>Все структуры данных, рассмотренные до этого момента в этой главе, были ответами на ограничения дисков. По сравнению с основной памятью, диски неудобны в работе. И с магнитными дисками, и с SSD данные на диске должны быть тщательно размещены, если нужна хорошая производительность при чтении и записи. Однако мы миримся с этими неудобствами, потому что диски имеют два значительных преимущества: они надежны (их содержимое не теряется при отключении питания) и у них ниже стоимость за гигабайт, чем у оперативной памяти.</p>
<p>По мере того как RAM дешевеет, аргумент о стоимости за гигабайт теряет силу. Многие наборы данных просто не такие большие, поэтому вполне реально держать их полностью в памяти, возможно, распределив между несколькими машинами. Это привело к разработке in-memory баз данных.</p>
<p>Некоторые хранилища ключ-значение в памяти, такие как Memcached, предназначены исключительно для использования в качестве кэша, где приемлемо, что данные будут потеряны при перезапуске машины. Но другие базы данных в памяти ориентированы на надежность, которую можно достичь с помощью специального оборудования (например, RAM с питанием от батареи), записи журнала изменений на диск, периодических снимков на диск или репликации состояния памяти на другие машины.</p>
<p>Когда база данных в памяти перезапускается, ей нужно заново загрузить свое состояние — либо с диска, либо по сети от реплики (если только не используется специальное оборудование). Несмотря на запись на диск, это по-прежнему база данных в памяти, потому что диск используется только как журнал в режиме append-only для надежности, а чтение всегда выполняется из памяти. Запись на диск также имеет эксплуатационные преимущества: файлы на диске легко резервировать, просматривать и анализировать внешними утилитами.</p>
<p>Продукты такие как <strong>VoltDB</strong>, <strong>SingleStore</strong> и <strong>Oracle TimesTen</strong> — это базы данных в памяти с реляционной моделью, и их производители утверждают, что они могут предложить значительный прирост производительности за счет устранения всех накладных расходов, связанных с управлением структурами данных на диске. <strong>RAMCloud</strong> — это открытая in-memory база данных ключ-значение с надежностью (использует журнальную структуру как для данных в памяти, так и для данных на диске). <strong>Redis</strong> и <strong>Couchbase</strong> обеспечивают слабую надежность, записывая данные на диск асинхронно.</p>
<p>Против интуиции, преимущество производительности баз данных в памяти связано не с тем, что им не нужно читать с диска. Даже движок хранения на диске может никогда не читать с диска, если у вас достаточно памяти, потому что операционная система все равно кэширует недавно использованные блоки диска в памяти. Скорее, они быстрее потому, что могут избежать накладных расходов, связанных с кодированием структур данных в памяти в форму, которую можно записать на диск.</p>
<p>Помимо производительности, еще одна интересная область для баз данных в памяти — это предоставление моделей данных, которые трудно реализовать с использованием индексов на диске. Например, <strong>Redis</strong> предлагает интерфейс, похожий на базу данных, к различным структурам данных, таким как очереди с приоритетами и множества. Поскольку все данные хранятся в памяти, его реализация сравнительно проста.</p>
<h1>Хранение данных для аналитики (Data Storage for Analytics)</h1>
<p>Модель данных хранилища данных чаще всего является реляционной, потому что SQL, как правило, хорошо подходит для аналитических запросов. Существует множество графических инструментов анализа данных, которые генерируют SQL-запросы, визуализируют результаты и позволяют аналитикам исследовать данные (через такие операции, как <strong>детализация (drill-down)</strong> и <strong>разбиение/комбинирование (slicing and dicing)</strong>).</p>
<p>На первый взгляд хранилище данных и реляционная <strong>OLTP-база</strong> данных выглядят схоже, потому что обе имеют интерфейс запросов SQL. Однако внутренняя организация систем может сильно различаться, поскольку они оптимизированы для совершенно разных шаблонов запросов. Многие поставщики баз данных теперь концентрируются на поддержке либо транзакционной обработки, либо аналитических нагрузок, но не обеих одновременно.</p>
<p>Некоторые базы данных, такие как <strong>Microsoft SQL Server</strong>, <strong>SAP HANA</strong> и <strong>SingleStore</strong>, поддерживают как транзакционную обработку, так и хранилище данных в одном продукте. Однако эти базы данных для <strong>гибридной транзакционной и аналитической обработки (HTAP, Hybrid Transactional and Analytical Processing)</strong>, упомянутые в разделе «Хранилища данных», всё чаще становятся двумя отдельными движками хранения и запросов, которые, тем не менее, доступны через общий SQL-интерфейс.</p>
<h2>Облачные хранилища данных (Cloud Data Warehouses)</h2>
<p>Поставщики хранилищ данных, такие как <strong>Teradata</strong>, <strong>Vertica</strong> и <strong>SAP HANA</strong>, продают как локальные хранилища по коммерческим лицензиям, так и облачные решения. Но поскольку многие их клиенты переходят в облако, новые облачные хранилища данных, такие как <strong>Google Cloud BigQuery</strong>, <strong>Amazon Redshift</strong> и <strong>Snowflake</strong>, также получили широкое распространение. В отличие от традиционных хранилищ данных, облачные хранилища используют масштабируемую облачную инфраструктуру, такую как объектное хранилище и безсерверные вычислительные платформы.</p>
<p>Облачные хранилища данных, как правило, лучше интегрируются с другими облачными сервисами и более эластичны. Например, многие облачные хранилища поддерживают автоматическую загрузку логов и предлагают простую интеграцию с фреймворками обработки данных, такими как <strong>Google Cloud Dataflow</strong> или <strong>Amazon Web Services Kinesis</strong>. Эти хранилища также более эластичны, потому что они разделяют вычисления для запросов и слой хранения. Данные сохраняются в объектном хранилище, а не на локальных дисках, что позволяет легко независимо масштабировать емкость хранения и вычислительные ресурсы для запросов, как мы уже видели в разделе <strong>«Архитектура облачных систем» (Cloud-Native System Architecture)</strong>.</p>
<p>Открытые хранилища данных, такие как <strong>Apache Hive</strong>, <strong>Trino</strong> и <strong>Apache Spark</strong>, также развивались вместе с облаком. По мере того как хранение данных для аналитики переместилось в озёра данных (data lakes) на объектных хранилищах, открытые хранилища начали разделяться на отдельные компоненты. Следующие части, которые ранее были интегрированы в единую систему, такую как Apache Hive, теперь часто реализуются как отдельные компоненты:</p>
<p><strong>Движок запросов (Query engine)</strong></p>
<p>Движки запросов, такие как <strong>Trino</strong>, <strong>Apache DataFusion</strong> и <strong>Presto</strong>, разбирают SQL-запросы, оптимизируют их в планы выполнения и исполняют их над данными. Выполнение обычно требует параллельных распределённых задач обработки данных. Некоторые движки запросов предоставляют встроенное выполнение задач, в то время как другие используют сторонние фреймворки выполнения, такие как <strong>Apache Spark</strong> или <strong>Apache Flink</strong>.</p>
<p><strong>Формат хранения (Storage format)</strong></p>
<p>Формат хранения определяет, как строки таблицы кодируются в байты в файле, который затем обычно хранится в объектном хранилище или распределённой файловой системе. Эти данные могут быть доступны движку запросов, но также и другим приложениям, использующим озеро данных. Примеры таких форматов хранения: Parquet, ORC, Lance или Nimble, и о них мы узнаем больше в следующем разделе.</p>
<p><strong>Формат таблицы (Table format)</strong></p>
<p>Файлы, записанные в <strong>Apache Parquet</strong> и аналогичных форматах хранения, обычно являются неизменяемыми после записи. Для поддержки вставок и удалений строк используется формат таблицы, такой как <strong>Apache Iceberg</strong> или формат <strong>Delta</strong> от <strong>Databricks</strong>. Форматы таблиц определяют файловый формат, который указывает, какие файлы составляют таблицу, вместе со схемой таблицы. Такие форматы также предлагают расширенные возможности, такие как <strong>time travel</strong> (возможность выполнять запросы к таблице в состоянии на предыдущий момент времени), сбор мусора и даже транзакции.</p>
<p><strong>Каталог данных (Data catalog)</strong></p>
<p>Так же, как формат таблицы определяет, какие файлы составляют таблицу, каталог данных определяет, какие таблицы составляют базу данных. Каталоги используются для создания, переименования и удаления таблиц. В отличие от форматов хранения и таблиц, каталоги данных, такие как <strong>Polaris</strong> от <strong>Snowflake</strong> и <strong>Unity Catalog</strong> от <strong>Databricks</strong>, обычно работают как отдельный сервис, к которому можно обращаться через REST-интерфейс. <strong>Apache Iceberg</strong> также предлагает каталог, который может работать внутри клиента или как отдельный процесс. Движки запросов используют информацию каталога при чтении и записи таблиц. Традиционно каталоги и движки запросов были интегрированы, но их разделение позволило системам обнаружения данных и управления данными (обсуждается в разделе «Системы данных, закон и общество» — <strong>Data Systems, Law, and Society</strong>) также получать доступ к метаданным каталогов.</p>
<h2>Хранение, ориентированное на колонки (Column-Oriented Storage)</h2>
<p>Как обсуждалось в разделе «Звёзды и снежинки: схемы для аналитики» (Stars and Snowflakes: Schemas for Analytics), хранилища данных по соглашению часто используют реляционную схему с большой фактографической таблицей, содержащей внешние ключи, ссылающиеся на таблицы измерений. Если в ваших фактографических таблицах находятся триллионы строк и петабайты данных, то их эффективное хранение и запросы становятся сложной задачей. Таблицы измерений обычно значительно меньше (миллионы строк), поэтому в этом разделе мы сосредоточимся на хранении фактов.</p>
<p>Хотя фактографические таблицы часто содержат более 100 колонок, типичный запрос к хранилищу данных обращается лишь к 4–5 из них за раз («SELECT *»-запросы редко нужны для аналитики). Рассмотрим запрос в примере 4-1: он обращается к большому числу строк (каждое событие покупки фруктов или сладостей в течение календарного 2024 года), но ему нужно получить доступ лишь к трём колонкам таблицы <code>fact_sales</code>: <code>date_key</code>, <code>product_sk</code> и <code>quantity</code>. Все остальные колонки запрос игнорирует.</p>
<p><strong>Пример 4-1. Анализ того, склонны ли люди покупать больше свежих фруктов или сладостей в зависимости от дня недели</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT
  dim_date.weekday, dim_product.category,
  SUM(fact_sales.quantity) AS quantity_sold
FROM fact_sales
  JOIN dim_date    ON fact_sales.date_key   = dim_date.date_key
  JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
WHERE
  dim_date.year = 2024 AND
  dim_product.category IN ('Fresh fruit', 'Candy')
GROUP BY
  dim_date.weekday, dim_product.category;</pre><p><strong>Как можно выполнить этот запрос эффективно?</strong></p>
<p><strong>В большинстве OLTP-баз данных хранение организовано построчно:</strong> все значения из одной строки таблицы хранятся рядом друг с другом. Документо-ориентированные базы данных похожи: весь документ обычно хранится как одна непрерывная последовательность байт. Это можно увидеть в примере CSV на рисунке 4-1.</p>
<p>Чтобы обработать запрос, подобный примеру 4-1, у вас могут быть индексы на <code>fact_sales.date_key</code> и/или <code>fact_sales.product_sk</code>, которые указывают движку хранения, где найти все продажи за определённую дату или для определённого продукта. Но затем построчный движок хранения всё равно должен загрузить все эти строки (каждая из которых состоит из более чем 100 атрибутов) с диска в память, разобрать их и отфильтровать те, что не соответствуют условиям. Это может занять много времени.</p>
<p>Идея хранения, ориентированного на колонки (или колоночного хранения), проста: не хранить все значения одной строки вместе, а хранить все значения каждой колонки вместе. Если каждая колонка хранится отдельно, то запросу нужно читать и разбирать только те колонки, которые используются в этом запросе, что может существенно сэкономить ресурсы. Принцип показан на рисунке 4-7 с использованием расширенной версии фактографической таблицы из рисунка 3-5.</p>
<hr />
<p><strong>Примечание</strong><br />
Колоночное хранение проще всего понять в реляционной модели данных, но оно в равной мере применимо и к нереляционным данным. Например, Parquet — это формат колоночного хранения, который поддерживает документо-ориентированную модель данных, основанную на Google Dremel, с использованием техники, известной как shredding или striping.</p>
<hr />
<p><strong>Рисунок 4-7. Хранение реляционных данных по колонкам, а не по строкам</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2093" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-scaled.png" alt="" width="2560" height="1737" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-300x204.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-1024x695.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-768x521.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-1536x1042.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-2048x1390.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-370x250.png 370w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-450x305.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-780x529.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_7-1600x1086.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Структура хранения, ориентированная на колонки, основана на том, что каждая колонка хранит строки в одном и том же порядке. Таким образом, если нужно восстановить всю строку, можно взять 23-й элемент из каждой отдельной колонки и собрать их вместе, чтобы получить 23-ю строку таблицы.</p>
<p>На самом деле колоночные движки хранения не хранят целую колонку (содержащую, возможно, триллионы строк) целиком. Вместо этого они разбивают таблицу на блоки из тысяч или миллионов строк, и внутри каждого блока хранят значения каждой колонки отдельно. Так как многие запросы ограничены определённым диапазоном дат, распространённо делать так, чтобы каждый блок содержал строки за определённый временной диапазон. Тогда запросу нужно загружать только необходимые колонки в тех блоках, которые пересекаются с нужным диапазоном дат.</p>
<p>Колоночное хранение используется почти во всех аналитических базах данных сегодня — от масштабных облачных хранилищ данных, таких как Snowflake, до односерверных встраиваемых баз данных, таких как <strong>DuckDB</strong>, и систем аналитики продуктов, таких как <strong>Pinot</strong> и <strong>Druid</strong>. Оно используется в форматах хранения, таких как <strong>Parquet</strong>, <strong>ORC</strong>, <strong>Lance</strong> и <strong>Nimble</strong>, а также во внутрипамятных аналитических форматах, таких как <strong>Apache Arrow</strong> и <strong>Pandas/NumPy</strong>. Некоторые базы данных временных рядов, такие как <strong>InfluxDB IOx</strong> и <strong>TimescaleDB</strong>, также основаны на колоночном хранении.</p>
<h2>Сжатие колонок (Column Compression)</h2>
<p>Помимо загрузки с диска только тех колонок, которые нужны для запроса, мы можем ещё больше уменьшить требования к пропускной способности диска и сети с помощью сжатия данных. К счастью, колоночное хранение очень хорошо подходит для сжатия.</p>
<p>Взгляните на последовательности значений для каждой колонки на рисунке 4-7: они часто выглядят довольно повторяющимися, что является хорошим признаком для сжатия. В зависимости от данных в колонке можно использовать разные методы сжатия. Один из методов, который особенно эффективен в хранилищах данных, — это битовая (bitmap) кодировка, показанная на рисунке 4-8.</p>
<p><strong>Рисунок 4-8. Сжатое, индексированное битовыми картами хранение одной колонки</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2094" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-scaled.png" alt="" width="2560" height="1891" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-300x222.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-1024x756.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-768x567.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-1536x1134.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-2048x1512.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-450x332.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-780x576.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_8-1600x1182.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Часто количество уникальных значений в колонке невелико по сравнению с количеством строк (например, у ритейлера могут быть миллиарды транзакций продаж, но только 100 000 уникальных продуктов). Мы можем взять колонку с n уникальными значениями и превратить её в n отдельных битовых карт: одну битовую карту для каждого уникального значения, с одним битом для каждой строки. Бит равен 1, если строка имеет это значение, и 0 — если нет.</p>
<p>Один из вариантов — хранить эти битовые карты, используя один бит на строку. Однако такие карты, как правило, содержат много нулей (мы говорим, что они разреженные). В таком случае битовые карты можно дополнительно кодировать с помощью run-length encoding: подсчитывать количество последовательных нулей или единиц и хранить это число, как показано в нижней части рисунка 4-8. Такие техники, как roaring bitmaps, переключаются между двумя представлениями битовых карт, используя то, которое является наиболее компактным. Это может сделать кодировку колонки исключительно эффективной.</p>
<p>Битовые индексы, подобные этим, очень хорошо подходят для типов запросов, которые часто встречаются в хранилище данных. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">WHERE product_sk IN (31, 68, 69)</pre><p>Загрузить три битовые карты для <code>product_sk = 31</code>, <code>product_sk = 68</code> и <code>product_sk = 69</code>, и вычислить побитовое ИЛИ (OR) трёх карт, что можно сделать очень эффективно.</p><pre class="urvanov-syntax-highlighter-plain-tag">WHERE product_sk = 30 AND store_sk = 3</pre><p>Загрузить битовые карты для <code>product_sk = 30</code> и <code>store_sk = 3</code>, и вычислить побитовое И (AND). Это работает потому, что колонки содержат строки в одном и том же порядке, поэтому k-й бит в битовой карте одной колонки соответствует той же строке, что и k-й бит в битовой карте другой колонки.</p>
<p>Битовые карты также можно использовать для ответа на графовые запросы, например, для поиска всех пользователей социальной сети, на которых подписан пользователь X и которые также подписаны на пользователя Y. Существуют и различные другие схемы сжатия для колоночных баз данных, о которых можно узнать в ссылках.</p>
<hr />
<p><strong>Примечание</strong></p>
<p>Не путайте колоночные базы данных с широкими колонками (также известными как column-family), в которых строка может содержать тысячи колонок, и нет необходимости, чтобы у всех строк были одинаковые колонки. Несмотря на сходство в названии, базы данных с широкими колонками ориентированы на строки, поскольку они хранят все значения строки вместе. Примеры модели с широкими колонками — <strong>Google Bigtable</strong>, <strong>Apache Accumulo</strong> и <strong>HBase</strong>.</p>
<hr />
<h2>Порядок сортировки в колоночном хранении (Sort Order in Column Storage)</h2>
<p>В колоночном хранилище не обязательно имеет значение, в каком порядке хранятся строки. Проще всего хранить их в порядке вставки, так как вставка новой строки тогда просто означает добавление в конец каждой колонки. Однако мы можем выбрать упорядочивание, как это делали ранее с <strong>SSTable</strong>, и использовать его как механизм индексации.</p>
<p>Важно заметить, что не имеет смысла сортировать каждую колонку отдельно, потому что тогда мы больше не будем знать, какие элементы колонок принадлежат одной строке. Мы можем восстановить строку только потому, что знаем: k-й элемент в одной колонке принадлежит той же строке, что и k-й элемент в другой колонке.</p>
<p>Таким образом, данные должны сортироваться целыми строками, даже если они хранятся по колонкам. Администратор базы данных может выбрать, по каким колонкам должна быть отсортирована таблица, используя знания о типичных запросах. Например, если запросы часто ориентированы на диапазоны дат, такие как последний месяц, разумно сделать date_key первым ключом сортировки. Тогда запрос сможет просматривать только строки за последний месяц, что будет значительно быстрее, чем просмотр всех строк.</p>
<p>Вторая колонка может определять порядок сортировки строк, имеющих одинаковое значение в первой колонке. Например, если <code>date_key</code> является первым ключом сортировки в рисунке 4-7, разумно выбрать <code>product_sk</code> в качестве второго ключа сортировки, чтобы все продажи одного продукта за один день хранились вместе. Это поможет запросам, которые должны группировать или фильтровать продажи по продукту в пределах определённого диапазона дат.</p>
<p>Ещё одно преимущество упорядочивания заключается в том, что оно может помочь со сжатием колонок. Если у первичного ключа сортировки немного уникальных значений, то после сортировки образуются длинные последовательности, где одно и то же значение повторяется много раз подряд. Простое run-length encoding, которое мы использовали для битовых карт на рисунке 4-8, может сжать такую колонку до нескольких килобайт — даже если таблица содержит миллиарды строк.</p>
<p>Эффект сжатия наиболее выражен для первого ключа сортировки. Второй и третий ключи будут более перемешаны и, соответственно, не дадут таких длинных последовательностей повторяющихся значений. Колонки с более низким приоритетом сортировки будут казаться практически случайными, поэтому их, вероятно, не удастся сжать столь эффективно. Но даже сортировка первых нескольких колонок уже является выигрышем в целом.</p>
<h2>Запись в колоночное хранилище (Writing to Column-Oriented Storage)</h2>
<p>Чтения в хранилищах данных, как правило, состоят из агрегаций по большому количеству строк; колоночное хранение, сжатие и сортировка помогают ускорить такие запросы на чтение. Записи в хранилище данных, как правило, представляют собой массовую загрузку данных, часто через ETL-процесс.</p>
<p>В случае с колоночным хранением запись отдельной строки где-то посередине отсортированной таблицы была бы крайне неэффективной, так как пришлось бы переписывать все сжатые колонки начиная с позиции вставки. Однако массовая запись большого числа строк сразу распределяет (амортизирует) стоимость переписывания колонок, делая её эффективной.</p>
<p>Для выполнения записей пакетами часто используется журналируемый (log-structured) подход. Все записи сначала попадают в ориентированное на строки, отсортированное, хранимое в памяти хранилище. Когда накапливается достаточно записей, они сливаются с колоночными закодированными файлами на диске и записываются в новые файлы пакетно. Так как старые файлы остаются неизменяемыми, а новые записываются целиком, объектное хранилище хорошо подходит для их хранения.</p>
<p>Запросам необходимо учитывать как данные колонок на диске, так и недавние записи в памяти и объединять их. Движок выполнения запросов скрывает это различие от пользователя. С точки зрения аналитика, данные, изменённые с помощью вставок, обновлений или удалений, немедленно отражаются в последующих запросах. Так работают Snowflake, Vertica, Apache Pinot, Apache Druid и многие другие.</p>
<h2>Выполнение запросов: компиляция и векторизация (Query Execution: Compilation and Vectorization)</h2>
<p>Сложный SQL-запрос для аналитики разбивается на план запроса, состоящий из нескольких этапов, называемых операторами, которые могут распределяться между несколькими машинами для параллельного выполнения. Планировщики запросов могут выполнять множество оптимизаций, выбирая, какие операторы использовать, в каком порядке их выполнять и где запускать каждый оператор.</p>
<p>Внутри каждого оператора движку запросов необходимо выполнять различные действия со значениями в колонке, например находить все строки, где значение входит в определённый набор значений (возможно, как часть join), или проверять, больше ли значение 15. Также нужно просматривать несколько колонок для одной и той же строки, например чтобы найти все транзакции продаж, где продукт — это бананы, а магазин — это определённый интересующий магазин.</p>
<p>Для запросов в хранилище данных, которым нужно просканировать миллионы строк, важно учитывать не только объём данных, которые нужно считать с диска, но и процессорное время, необходимое для выполнения сложных операторов. Самый простой вид оператора работает как интерпретатор языка программирования: при итерации по каждой строке он проверяет структуру данных, представляющую запрос, чтобы понять, какие сравнения или вычисления нужно выполнить над какими колонками. К сожалению, это слишком медленно для многих аналитических задач. Два альтернативных подхода к эффективному выполнению запросов получили распространение:</p>
<p><strong>Компиляция запросов (Query compilation)</strong></p>
<p>Движок запросов берёт SQL-запрос и генерирует код для его выполнения. Код перебирает строки одну за другой, смотрит на значения в интересующих колонках, выполняет необходимые сравнения или вычисления и копирует нужные значения в выходной буфер, если условия удовлетворены. Движок запросов компилирует сгенерированный код в машинный код (часто с использованием существующего компилятора, такого как LLVM), а затем запускает его на колоночных закодированных данных, загруженных в память. Такой подход к генерации кода похож на just-in-time (JIT) компиляцию, используемую в JVM и аналогичных средах выполнения.</p>
<p><strong>Векторизованная обработка (Vectorized processing)</strong></p>
<p>Запрос интерпретируется, а не компилируется, но его выполнение ускоряется за счёт обработки множества значений из колонки сразу, пакетами, вместо итерации по строкам одну за другой. В базу данных встроен фиксированный набор предопределённых операторов; им можно передавать аргументы и получать обратно пакет результатов.</p>
<p>Например, мы можем передать колонку <code>product_sk</code> и <code>ID</code> продукта «бананы» оператору равенства и получить в ответ битовую карту (по одному биту на каждое значение входной колонки, где 1 означает «это банан»); затем мы можем передать колонку <code>store_sk</code> и <code>ID</code> интересующего магазина тому же оператору равенства и получить другую битовую карту; а затем передать обе карты оператору «побитовое И (AND)», как показано на <strong>рисунке 4-9</strong>. Результатом будет <strong>битовая карта</strong>, содержащая 1 для всех продаж бананов в данном магазине.</p>
<p><strong>Рисунок 4-9. Побитовое И двух битовых карт подходит для векторизации</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2095" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-scaled.png" alt="" width="2560" height="926" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-300x108.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-1024x370.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-768x278.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-1536x555.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-2048x740.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-450x163.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-780x282.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_9-1600x578.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Два подхода очень различаются с точки зрения реализации, но оба применяются на практике. Оба могут достигать очень высокой производительности, используя особенности современных процессоров:</p>
<ul>
<li>предпочтение последовательного доступа к памяти перед случайным доступом для снижения числа промахов в кэше;</li>
<li>выполнение большей части работы во внутренних циклах (с малым числом инструкций и без вызовов функций), чтобы загрузить конвейер обработки инструкций CPU и избежать ошибок предсказания переходов;</li>
<li>использование параллелизма, такого как многопоточность и инструкции single-instruction-multi-data (SIMD);</li>
<li>работа напрямую с сжатыми данными без их декодирования в отдельное представление в памяти, что экономит затраты на выделение памяти и копирование.</li>
</ul>
<h2>Материализованные представления и кубы данных (Materialized Views and Data Cubes)</h2>
<p>Мы уже встречались с материализованными представлениями в разделе “Materializing and Updating Timelines”: в реляционной модели данных это объект, похожий на таблицу, содержимое которого является результатом некоторого запроса. Разница в том, что <strong>материализованное представление</strong> — это фактическая копия результатов запроса, записанная на диск, тогда как виртуальное представление — это всего лишь ярлык для упрощённой записи запросов. Когда вы читаете из виртуального представления, SQL-движок на лету разворачивает его в исходный запрос представления и затем обрабатывает уже развёрнутый запрос.</p>
<p>Когда исходные данные меняются, материализованное представление должно обновляться соответствующим образом. Некоторые базы данных могут делать это автоматически, а также существуют системы, такие как <strong>Materialize</strong>, которые специализируются на поддержке материализованных представлений. Выполнение таких обновлений означает больше работы при записи, но материализованные представления могут улучшить производительность чтения в нагрузках, где часто приходится выполнять одни и те же запросы.</p>
<p><strong>Материализованные агрегаты</strong> — это разновидность материализованных представлений, которая может быть полезна в хранилищах данных. Как мы обсуждали ранее, запросы в хранилище данных часто включают агрегатные функции, такие как <code>COUNT</code>, <code>SUM</code>, <code>AVG</code>, <code>MIN</code> или <code>MAX</code> в SQL. Если одни и те же агрегаты используются многими разными запросами, может быть расточительно каждый раз пересчитывать их на сырых данных. Почему бы не закэшировать некоторые из наиболее часто используемых подсчётов или сумм? <strong>Куб данных (data cube)</strong> или <strong>OLAP-куб</strong> делает именно это, создавая сетку агрегатов, сгруппированных по различным измерениям. На рисунке 4-10 показан пример.</p>
<p><strong>Рисунок 4-10. Два измерения куба данных, агрегирование данных суммированием</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2096" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-scaled.png" alt="" width="2560" height="1319" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-300x155.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-1024x528.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-768x396.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-1536x791.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-2048x1055.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-450x232.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-780x402.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_10-1600x824.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Представим, что каждый факт имеет внешние ключи только к двум таблицам измерений — на рисунке 4-10 это <code>date_key</code> и <code>product_sk</code>. Тогда можно нарисовать двумерную таблицу: по одной оси — даты, по другой — продукты. Каждая ячейка содержит агрегат (например, <code>SUM</code>) атрибута (например, <code>net_price</code>) всех фактов с данной комбинацией дата–продукт. Затем можно применить тот же агрегат по каждой строке или столбцу и получить сводку, уменьшенную на одно измерение (например, продажи по продукту без учёта даты или продажи по дате без учёта продукта).</p>
<p>В общем случае факты часто имеют больше двух измерений. На рисунке 3-5 пять измерений: дата, продукт, магазин, акция и клиент. Намного труднее представить, как выглядел бы пятиразмерный гиперкуб, но принцип остаётся тем же: каждая ячейка содержит продажи для конкретной комбинации дата–продукт–магазин–акция–клиент. Эти значения затем можно многократно агрегировать вдоль каждого из измерений.</p>
<p>Преимущество материализованного куба данных в том, что определённые запросы становятся очень быстрыми, так как они фактически уже предвычислены. Например, если вы хотите узнать общие продажи по каждому магазину за вчерашний день, нужно просто посмотреть на итоги вдоль соответствующего измерения — нет необходимости сканировать миллионы строк.</p>
<p>Недостаток заключается в том, что куб данных не обладает такой же гибкостью, как запросы к сырым данным. Например, невозможно вычислить, какая доля продаж приходится на товары дороже <code>$100</code>, если цена не является одним из измерений. Поэтому большинство хранилищ данных стараются сохранять как можно больше сырых данных и используют агрегаты, такие как кубы данных, лишь как средство повышения производительности для определённых запросов.</p>
<h1>Многомерные и полнотекстовые индексы (Multidimensional and Full-Text Indexes)</h1>
<p><strong>B-деревья и LSM-деревья </strong>позволяют выполнять диапазонные запросы по одному атрибуту: например, если ключом является имя пользователя, можно использовать их как индекс для эффективного поиска всех имён, начинающихся с буквы «L». Но иногда поиска только по одному атрибуту недостаточно.</p>
<p>Наиболее распространённый тип многоколонного индекса называется <strong>конкатенированным индексом (concatenated index)</strong>. Он просто объединяет несколько полей в один ключ, последовательно добавляя одну колонку к другой (определение индекса задаёт, в каком порядке объединяются поля). Это похоже на старомодную бумажную телефонную книгу, которая обеспечивает индекс по (фамилия, имя) к номеру телефона. Благодаря порядку сортировки индекс можно использовать, чтобы найти всех людей с определённой фамилией или всех людей с определённой комбинацией фамилия–имя. Однако индекс бесполезен, если нужно найти всех людей с определённым именем.</p>
<p>С другой стороны, <strong>многомерные индексы</strong> позволяют выполнять запросы сразу по нескольким колонкам. Один из наиболее важных случаев применения — геопространственные данные. Например, сайт поиска ресторанов может иметь базу данных с широтой и долготой каждого ресторана. Когда пользователь смотрит рестораны на карте, сайт должен найти все рестораны внутри прямоугольной области карты, которую пользователь в данный момент просматривает. Для этого нужен двумерный диапазонный запрос, например:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT * 
FROM restaurants
WHERE latitude  &gt; 51.4946 
AND latitude  &lt; 51.5079
AND longitude &gt; -0.1162 
AND longitude &lt; -0.1004;</pre><p><strong>Конкатенированный индекс по колонкам широты и долготы</strong> не способен эффективно отвечать на такой запрос: он может выдать либо все рестораны в диапазоне широт (но на любой долготе), либо все рестораны в диапазоне долгот (но где угодно между Северным и Южным полюсами), но не оба условия одновременно.</p>
<p>Один из вариантов — преобразовать двумерное местоположение в одно число с помощью заполняющей пространство кривой (space-filling curve), а затем использовать обычный индекс B-дерева. Чаще применяются специализированные пространственные индексы, такие как R-деревья или Bkd-деревья; они делят пространство так, что близкие точки данных группируются в одном поддереве. Например, PostGIS реализует геопространственные индексы как R-деревья, используя механизм обобщённых деревьев поиска (Generalized Search Tree) PostgreSQL. Также можно использовать регулярно расположенные сетки из треугольников, квадратов или шестиугольников.</p>
<p>Многомерные индексы применяются не только для географических координат. Например, на сайте электронной коммерции можно использовать трёхмерный индекс по измерениям (red, green, blue), чтобы искать товары в определённом диапазоне цветов. Или в базе данных метеорологических наблюдений можно иметь двумерный индекс по (дата, температура), чтобы эффективно находить все наблюдения за 2013 год, когда температура была между 25 и 30 ℃. С одномерным индексом пришлось бы либо просматривать все записи за 2013 год (независимо от температуры) и затем фильтровать их по температуре, либо наоборот. 2D-индекс может сузить поиск сразу и по дате, и по температуре.</p>
<h2>Полнотекстовый поиск</h2>
<p><strong>Полнотекстовый поиск</strong> позволяет искать в коллекции текстовых документов (веб-страницах, описаниях товаров и т. д.) по ключевым словам, которые могут встречаться где угодно в тексте. Информационный поиск — это большая специализированная область, которая часто включает языко-специфическую обработку: например, несколько азиатских языков пишутся без пробелов или знаков пунктуации между словами, и поэтому разделение текста на слова требует модели, которая указывает, какие последовательности символов образуют слово. Полнотекстовый поиск также часто включает сопоставление слов, которые схожи, но не идентичны (например, опечатки или разные грамматические формы слов), а также синонимы. Эти проблемы выходят за рамки данной книги.</p>
<p>Тем не менее, в своей основе, полнотекстовый поиск можно рассматривать как еще один вид многомерного запроса: в данном случае каждое слово, которое может встретиться в тексте (термин), является измерением. Документ, содержащий термин x, имеет значение 1 в измерении x, а документ, не содержащий x, имеет значение 0. Поиск документов, упоминающих “red apples” (“красные яблоки”), означает запрос, который ищет 1 в измерении red и одновременно 1 в измерении apples. Таким образом, количество измерений может быть очень большим.</p>
<p>Структура данных, которую многие поисковые системы используют для ответа на такие запросы, называется инвертированным индексом. Это структура «ключ-значение», где ключ — это термин, а значение — список идентификаторов всех документов, содержащих данный термин (список вхождений). Если идентификаторы документов являются последовательными числами, список вхождений также может быть представлен как разреженное битовое поле, как на рисунке 4-8: n-й бит в битовом поле для термина x равен 1, если документ с идентификатором n содержит термин x.</p>
<p>Поиск всех документов, содержащих оба термина x и y, теперь аналогичен векторизованному запросу в хранилище данных, который ищет строки, удовлетворяющие двум условиям (рисунок 4-9): загружаются два битовых поля для терминов x и y и вычисляется их побитовое И. Даже если битовые поля закодированы методом run-length, это можно сделать очень эффективно.</p>
<p>Например, Lucene, механизм полнотекстового индексирования, используемый в Elasticsearch и Solr, работает именно так. Он хранит отображение от термина к списку вхождений в отсортированных файлах наподобие SSTable, которые объединяются в фоне, используя тот же подход с журналируемой структурой, который мы рассматривали ранее в этой главе. Тип индекса GIN в PostgreSQL также использует списки вхождений для поддержки полнотекстового поиска и индексирования внутри JSON-документов.</p>
<p>Вместо разбиения текста на слова, альтернативой является нахождение всех подстрок длиной n, которые называются n-граммами. Например, триграммы (n = 3) строки &#171;hello&#187; это &#171;hel&#187;, &#171;ell&#187; и &#171;llo&#187;. Если построить инвертированный индекс всех триграмм, можно искать документы по произвольным подстрокам длиной как минимум три символа. Триграммные индексы даже позволяют использовать регулярные выражения в поисковых запросах; минус в том, что они получаются довольно большими.</p>
<p>Чтобы справляться с опечатками в документах или запросах, Lucene может искать слова в пределах определенного расстояния редактирования (расстояние редактирования 1 означает, что одна буква была добавлена, удалена или заменена). Он делает это, сохраняя множество терминов как конечный автомат по символам в ключах, аналогичный префиксному дереву (trie), и преобразуя его в автомат Левенштейна, который поддерживает эффективный поиск слов в пределах заданного расстояния редактирования.</p>
<h2>Векторные представления (Vector Embeddings)</h2>
<p><strong>Семантический поиск</strong> выходит за пределы синонимов и опечаток, пытаясь понять концепции документа и намерения пользователя. Например, если на ваших страницах помощи есть страница с заголовком “cancelling your subscription” (“отмена подписки”), пользователи должны иметь возможность найти эту страницу по запросам “how to close my account” (“как закрыть мой аккаунт”) или “terminate contract” (“расторгнуть контракт”), которые близки по смыслу, даже если используют совершенно разные слова.</p>
<p>Чтобы понять семантику документа — его значение — индексы семантического поиска используют модели встраивания (embedding models), которые переводят документ в вектор вещественных значений с плавающей точкой, называемый векторным представлением (vector embedding). Вектор представляет точку в многомерном пространстве, и каждое вещественное значение представляет положение документа вдоль оси одного измерения. Модели встраивания генерируют векторы, которые располагаются близко друг к другу (в этом многомерном пространстве), если входные документы семантически похожи.</p>
<hr />
<p><strong>Примечание</strong></p>
<p>Мы уже встречали термин векторизованная обработка в разделе “Query Execution: Compilation and Vectorization”. Векторы в семантическом поиске имеют другое значение. В векторизованной обработке вектор относится к пакету битов, который можно обработать с помощью специально оптимизированного кода. В моделях встраивания векторы — это список чисел с плавающей точкой, представляющих местоположение в многомерном пространстве.</p>
<hr />
<p>Например, трехмерное векторное представление для страницы Википедии об аграрном хозяйстве может быть [0.1, 0.22, 0.11]. Страница Википедии об овощах будет находиться довольно близко, возможно с представлением [0.13, 0.19, 0.24]. Страница о звездных схемах может иметь представление [0.82, 0.39, -0.74], находясь сравнительно далеко. Мы можем видеть, что первые два вектора ближе друг к другу, чем третий.</p>
<p>Модели встраивания используют гораздо более длинные векторы (часто более 1000 чисел), но принципы остаются теми же. Мы не пытаемся понять, что означают отдельные числа; они просто способ для моделей указывать положение в абстрактном многомерном пространстве. Поисковые системы используют функции расстояния, такие как косинусное сходство или евклидово расстояние, чтобы измерять расстояние между векторами. Косинусное сходство измеряет косинус угла между двумя векторами, чтобы определить, насколько они близки, тогда как евклидово расстояние измеряет прямое расстояние между двумя точками в пространстве.</p>
<p>Многие ранние модели встраивания, такие как Word2Vec, BERT и GPT, работали с текстовыми данными. Такие модели обычно реализуются как нейронные сети. Исследователи затем создали модели встраивания для видео, аудио и изображений. Более недавно архитектура моделей стала мультимодальной: одна модель может генерировать векторы для нескольких типов данных, например текста и изображений.</p>
<p>Семантические поисковые системы используют модель встраивания, чтобы сгенерировать вектор при вводе пользователем запроса. Запрос пользователя и связанный контекст (например, местоположение пользователя) подаются в модель. После того как модель генерирует векторное представление запроса, поисковая система должна найти документы с похожими векторами, используя векторный индекс.</p>
<p>Векторные индексы хранят векторы коллекции документов. Чтобы выполнить запрос к индексу, вы передаете вектор запроса, и индекс возвращает документы, чьи векторы ближе всего к вектору запроса. Так как R-деревья, которые мы рассматривали ранее, плохо работают для векторов с большим количеством измерений, используются специализированные векторные индексы, такие как:</p>
<p><strong>Плоские индексы (Flat indexes)</strong><br />
Векторы сохраняются в индексе как есть. Запрос должен прочитать каждый вектор и измерить его расстояние до вектора запроса. Плоские индексы точны, но измерение расстояния между запросом и каждым вектором медленно.</p>
<p><strong>Индексированные файлы (IVF)</strong><br />
Векторное пространство кластеризуется на разделы (называемые центроидами) векторов, чтобы сократить количество векторов, которые необходимо сравнить. IVF-индексы работают быстрее, чем плоские индексы, но могут давать только приближенные результаты: запрос и документ могут попасть в разные разделы, даже если они находятся близко друг к другу. Запрос к IVF-индексу сначала определяет probes — просто количество разделов, которые нужно проверить. Запросы, использующие большее число probes, будут более точными, но и более медленными, так как нужно будет сравнивать больше векторов.</p>
<p><strong>Иерархический навигируемый малый мир (HNSW)</strong><br />
HNSW-индексы поддерживают несколько уровней векторного пространства, как показано на Рисунке 4-11. Каждый уровень представлен в виде графа, где узлы представляют векторы, а рёбра — близость к соседним векторам. Запрос начинается с поиска ближайшего вектора на самом верхнем уровне, где узлов немного. Затем запрос перемещается к тому же узлу на нижнем уровне и следует по рёбрам этого уровня, который связан плотнее, в поисках вектора, который ближе к запросу. Процесс продолжается до тех пор, пока не будет достигнут последний уровень. Как и IVF-индексы, HNSW-индексы являются приближенными.</p>
<p><strong>Рисунок 4-11. Поиск записи в базе данных, которая ближе всего к заданному вектору-запросу в HNSW-индексе</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2097" src="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-scaled.png" alt="" width="2560" height="1398" srcset="https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-300x164.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-1024x559.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-768x420.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-1536x839.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-2048x1119.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-450x246.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-780x426.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/chapter_4_image_4_11-1600x874.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Многие популярные векторные базы данных реализуют IVF- и HNSW-индексы. Библиотека Faiss от Facebook имеет множество вариаций каждого, а PostgreSQL pgvector также поддерживает оба. Полные детали алгоритмов IVF и HNSW выходят за рамки этой книги, но их статьи являются отличным источником.</p>
<h1>Резюме</h1>
<p>В этой главе мы попытались разобраться, как базы данных выполняют хранение и извлечение данных. Что происходит, когда вы сохраняете данные в базе, и что делает база данных, когда вы снова запрашиваете эти данные?</p>
<p><strong>Раздел «Аналитические и операционные системы»</strong> ввёл различие между обработкой транзакций (OLTP) и аналитикой (OLAP). В этой главе мы увидели, что движки хранения, оптимизированные для OLTP, сильно отличаются от оптимизированных для аналитики:</p>
<ul>
<li>OLTP-системы оптимизированы для большого числа запросов, каждый из которых читает и записывает небольшое количество записей и требует быстрых ответов. Записи обычно обращаются по первичному ключу или вторичному индексу, и эти индексы, как правило, являются упорядоченными отображениями от ключа к записи, которые также поддерживают диапазонные запросы.</li>
<li>Хранилища данных и подобные аналитические системы оптимизированы для сложных запросов на чтение, которые сканируют большое количество записей. Обычно они используют колонко-ориентированную схему хранения с сжатием, минимизирующим количество данных, которые нужно прочитать с диска, а также just-in-time компиляцию запросов или векторизацию, чтобы минимизировать затраты процессорного времени на обработку данных.</li>
</ul>
<p><strong>С OLTP-стороны мы рассмотрели движки хранения из двух основных школ мысли:</strong></p>
<ul>
<li>Журнально-структурированный подход, который позволяет только добавлять данные в файлы и удалять устаревшие файлы, но никогда не обновляет уже записанный файл. К этой группе относятся SSTables, LSM-деревья, RocksDB, Cassandra, HBase, Scylla, Lucene и другие. В целом, журнально-структурированные движки хранения, как правило, обеспечивают высокую пропускную способность на запись.</li>
<li>Подход с обновлением на месте, который рассматривает диск как набор фиксированных по размеру страниц, которые можно перезаписывать. B-деревья, крупнейший пример этой философии, используются во всех основных реляционных OLTP-базах данных, а также во многих нереляционных. Как правило, B-деревья лучше подходят для чтения, обеспечивая более высокую пропускную способность на чтение и меньшее время отклика, чем журнально-структурированное хранение.</li>
</ul>
<p>Затем мы рассмотрели индексы, которые могут искать по нескольким условиям одновременно: многомерные индексы, такие как R-деревья, которые позволяют искать точки на карте по широте и долготе одновременно, и индексы полнотекстового поиска, которые могут искать несколько ключевых слов, встречающихся в одном тексте. Наконец, векторные базы данных используются для семантического поиска по текстам и другим медиа; они используют векторы с большим количеством измерений и находят похожие документы, сравнивая сходство векторов.</p>
<p>Как разработчик приложений, вооружённый этим знанием о внутреннем устройстве движков хранения, вы гораздо лучше понимаете, какой инструмент лучше всего подходит для вашей конкретной задачи. Если вам нужно изменить параметры настройки базы данных, это понимание позволяет представить, какой эффект может дать большее или меньшее значение.</p>
<p>Хотя эта глава не сделала вас экспертом в настройке какого-либо конкретного движка хранения, она, надеюсь, снабдила вас достаточным словарём и идеями, чтобы вы могли разобраться в документации к выбранной вами базе данных.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-4-storage-and-retrieval/">Глава 4. Хранение и извлечение</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-4-storage-and-retrieval/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 3. Модели данных и языки запросов</title>
		<link>https://datatalks.ru/chapter-3-data-models-and-query-languages/</link>
					<comments>https://datatalks.ru/chapter-3-data-models-and-query-languages/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 27 Jul 2025 11:38:40 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1953</guid>

					<description><![CDATA[<p>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта Глава 3. Модели данных и языки запросов Границы моего языка означают границы моего мира. Людвиг Витгенштейн, Логико-философский трактат (1922) Модели данных — пожалуй, самая важная часть разработки программного обеспечения, поскольку они оказывают глубочайшее влияние: не только на то, как пишется софт, но и на [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-3-data-models-and-query-languages/">Глава 3. Модели данных и языки запросов</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта</em></p>
<h1>Глава 3. Модели данных и языки запросов</h1>
<blockquote><p>Границы моего языка означают границы моего мира.<br />
<strong>Людвиг Витгенштейн, Логико-философский трактат (1922)</strong></p></blockquote>
<hr />
<p><strong>Модели данных</strong> — пожалуй, самая важная часть разработки программного обеспечения, поскольку они оказывают глубочайшее влияние: не только на то, как пишется софт, но и на то, как мы думаем о проблеме, которую решаем.</p>
<p>Большинство приложений строится путем наложения одной модели данных поверх другой. Для каждого слоя ключевой вопрос звучит так: как он представлен с точки зрения следующего нижележащего слоя?</p>
<p><strong>Например:</strong></p>
<p>Как разработчик приложения, вы смотрите на реальный мир (в котором есть люди, организации, товары, действия, денежные потоки, сенсоры и т. д.) и моделируете его в терминах объектов или структур данных, а также API, которые манипулируют этими структурами. Эти структуры часто специфичны для вашего приложения.</p>
<p>Когда вы хотите сохранить эти структуры данных, вы выражаете их в терминах модели данных общего назначения, например, <code>JSON-</code> или <code>XML-</code>документов, таблиц в реляционной базе данных или вершин и рёбер в графе. Эти модели данных и являются темой данной главы.</p>
<p>Инженеры, которые разработали ваше ПО для баз данных, выбрали способ представления этих <strong>документных/реляционных/графовых данных</strong> в виде байтов в памяти, на диске или в сети. Такое представление может позволять выполнять запросы к данным, искать их, изменять и обрабатывать различными способами.</p>
<p>На ещё более низких уровнях инженеры-«железячники» выяснили, как представлять байты в терминах электрических токов, импульсов света, магнитных полей и т.п.</p>
<p>В сложном приложении может быть больше промежуточных уровней, например API, построенные поверх других API, но базовая идея остаётся той же: <strong>каждый слой скрывает сложность нижележащих, предоставляя чистую модель данных.</strong> Эти абстракции позволяют разным группам людей — например, инженерам вендора СУБД и разработчикам приложений, использующим эту СУБД — эффективно работать вместе.</p>
<p>На практике широко применяются несколько различных моделей данных, зачастую для разных целей. Для некоторых типов данных и некоторых запросов одна модель подходит естественно, а другая выглядит громоздкой. <strong>В этой главе мы исследуем такие компромиссы, сравнив реляционную модель, документную модель, графовые модели данных, event sourcing и dataframes.</strong> Мы также кратко рассмотрим языки запросов, которые позволяют работать с этими моделями. Это сравнение поможет вам понять, когда использовать ту или иную модель.</p>
<h1>ТЕРМИНОЛОГИЯ: ДЕКЛАРАТИВНЫЕ ЯЗЫКИ ЗАПРОСОВ</h1>
<p>Многие <strong>языки запросов</strong>, рассматриваемые в этой главе (такие как <strong>SQL</strong>, <strong>Cypher</strong>, <strong>SPARQL</strong> или <strong>Datalog</strong>), являются <strong>декларативными</strong>. Это означает, что вы указываете шаблон данных, который хотите получить, — какие условия должны удовлетворяться результатами и каким образом данные должны быть трансформированы (например, отсортированы, сгруппированы или агрегированы), — но не описываете, как достичь этой цели. Оптимизатор запросов в СУБД может сам решить, какие индексы и какие алгоритмы соединений использовать, и в каком порядке выполнять различные части запроса.</p>
<p>В отличие от этого, в большинстве языков программирования вы должны написать алгоритм — то есть указать компьютеру, какие операции и в каком порядке выполнять. <strong>Декларативный язык запросов привлекателен тем, что он обычно более лаконичен и проще в написании, чем явный алгоритм.</strong> Но ещё важнее то, что <strong>он скрывает детали реализации движка запросов</strong>, благодаря чему СУБД может внедрять улучшения производительности без необходимости менять сами запросы.</p>
<p><strong>Например, база данных может выполнить декларативный запрос параллельно на нескольких CPU-ядрах и машинах, и вам не придётся заботиться о том, как реализовать этот параллелизм.</strong> Вручную закодировать алгоритм с поддержкой такой параллельной обработки было бы крайне трудозатратно.</p>
<h1>Реляционная модель против документной модели</h1>
<p>Наиболее известная модель данных сегодня — это, вероятно, SQL, основанная на реляционной модели, предложенной <strong>Эдгаром Коддом</strong> в <strong>1970</strong> году: данные организованы в отношения (в SQL они называются таблицами), где <strong>каждое отношение — это неупорядоченное множество кортежей</strong> (строк в SQL).</p>
<p>Изначально реляционная модель была теоретическим предложением, и многие тогда сомневались, можно ли её эффективно реализовать. Однако к середине 1980-х системы управления реляционными базами данных (СУБД) и SQL стали основными инструментами для большинства, кому требовалось хранить и запрашивать данные с какой-либо регулярной структурой. Многие сценарии управления данными десятилетиями остаются доминируемой областью реляционных данных — например, <strong>бизнес-аналитика</strong>.</p>
<p>Со временем появилось множество конкурирующих подходов к хранению и запросу данных. В 1970-х и начале 1980-х основными альтернативами были сетевая и иерархическая модели, но реляционная модель вытеснила их. <strong>Объектные базы данных</strong> появились и снова ушли в конце 1980-х и начале 1990-х. <strong>XML-базы</strong> появились в начале 2000-х, но получили лишь нишевое распространение. Каждый конкурент реляционной модели в своё время вызывал огромный хайп, но он никогда не был долговечным. Вместо этого <strong>SQL расширился, включив в себя другие типы данных помимо реляционного ядра</strong> — например, добавив поддержку XML, JSON и графовых данных.</p>
<p>В 2010-х <strong>NoSQL</strong> стал новым модным словом, пытавшимся свергнуть доминирование реляционных баз данных. <strong>NoSQL не означает одну конкретную технологию</strong>, а представляет собой набор идей вокруг новых моделей данных, гибкости схем, масштабируемости и движения в сторону моделей лицензирования open source. Некоторые базы позиционировали себя как NewSQL, так как они стремились обеспечить масштабируемость систем NoSQL вместе с моделью данных и транзакционными гарантиями традиционных реляционных баз данных. <strong>Идеи NoSQL и NewSQL сильно повлияли на дизайн систем данных</strong>, но по мере того как принципы были широко восприняты, использование этих терминов сошло на нет.</p>
<p>Одним из долгосрочных эффектов движения NoSQL стала <strong>популярность документной модели</strong>, которая обычно представляет данные в виде JSON. Эта модель изначально получила распространение благодаря специализированным документным базам данных, таким как <strong>MongoDB</strong> и <strong>Couchbase</strong>, хотя большинство реляционных баз данных теперь также добавили поддержку JSON.</p>
<p><span style="color: #993300;"><strong>По сравнению с реляционными таблицами, которые часто воспринимаются как имеющие жёсткую и негибкую схему, JSON-документы считаются более гибкими.</strong></span><br />
Плюсы и минусы документных и реляционных данных активно обсуждаются; давайте рассмотрим некоторые ключевые моменты этой дискуссии.</p>
<h2>Объектно-реляционный разрыв</h2>
<p>Большая часть разработки приложений сегодня ведётся на <strong>объектно-ориентированных языках программирования</strong>, что ведёт к распространённой <strong>критике модели данных SQL</strong>: если данные хранятся в реляционных таблицах, требуется <strong>громоздкий слой трансляции между объектами в коде приложения и моделью базы данных</strong>, основанной на таблицах, строках и столбцах. Этот разрыв между моделями иногда называют <strong>несогласованностью импедансов (impedance mismatch)</strong>.</p>
<p><strong>Примечание</strong></p>
<p><strong>Термин impedance mismatch</strong> заимствован из электроники. Каждая электрическая цепь имеет определённый импеданс (сопротивление переменному току) на своих входах и выходах. Когда вы соединяете выход одной цепи со входом другой, передача мощности через соединение максимальна, если выходной и входной импедансы этих двух цепей совпадают. Несогласованность импедансов может привести к отражению сигнала и другим проблемам.</p>
<h2>Object-relational mapping (ORM)</h2>
<p><strong>Фреймворки объектно-реляционного отображения (ORM)</strong>, такие как <strong>ActiveRecord</strong> и <strong>Hibernate</strong>, уменьшают количество шаблонного кода, необходимого для этого слоя трансляции, но они часто подвергаются критике. Наиболее часто упоминаемые проблемы:</p>
<ul>
<li>ORM сложны и не могут полностью скрыть различия между двумя моделями, поэтому разработчикам всё равно приходится думать и о реляционном, и об объектном представлении данных.</li>
<li>ORM, как правило, используются только для разработки OLTP-приложений; инженерам данных, подготавливающим данные для аналитики, всё равно приходится работать с базовым реляционным представлением, поэтому дизайн реляционной схемы остаётся важным даже при использовании ORM.</li>
<li>Многие ORM работают только с реляционными OLTP-базами. Организации с разнообразными системами данных, такими как поисковые движки, графовые базы данных и системы NoSQL, могут столкнуться с нехваткой поддержки ORM.</li>
<li>Некоторые ORM автоматически генерируют реляционные схемы, но они могут оказаться неудобными для пользователей, которые работают с реляционными данными напрямую, и неэффективными для базовой базы данных. Кастомизация схемы и генерации запросов ORM может быть сложной и свести на нет выгоду от использования ORM.</li>
<li>ORM упрощают написание неэффективных запросов, например, проблему N+1-запроса. Допустим, вы хотите отобразить список комментариев пользователей на странице, поэтому выполняете один запрос, который возвращает N комментариев, каждый из которых содержит ID его автора. Чтобы показать имя автора комментария, нужно найти этот ID в таблице пользователей. В написанном вручную SQL вы бы, скорее всего, выполнили join в запросе и вернули имя автора вместе с каждым комментарием, но с ORM вы можете в итоге сделать отдельный запрос к таблице пользователей для каждого из N комментариев, чтобы найти его автора, в результате получится N+1 запрос к базе данных, что медленнее, чем выполнение join в самой базе. Чтобы избежать этой проблемы, вам, возможно, придётся указать ORM забирать информацию об авторе одновременно с выборкой комментариев.</li>
</ul>
<p>Тем не менее, у ORM также есть преимущества:</p>
<ul>
<li>Для данных, которые хорошо подходят для реляционной модели, какой-то вид трансляции между постоянным реляционным и объектным представлением в памяти неизбежен, и ORM уменьшают количество шаблонного кода, необходимого для этой трансляции. Сложные запросы, возможно, всё равно придётся обрабатывать вне ORM, но ORM могут помочь в простых и повторяющихся случаях.</li>
<li>Некоторые ORM помогают с кэшированием результатов запросов к базе данных, что может снизить нагрузку на базу данных.</li>
<li>ORM также могут помочь в управлении миграциями схемы и других административных задачах.</li>
</ul>
<h2>Документная модель данных для связей «один-ко-многим»</h2>
<p>Не все данные хорошо поддаются реляционному представлению; рассмотрим пример, чтобы исследовать ограничение реляционной модели. На рисунке 3-1 показано, как резюме (профиль LinkedIn) может быть выражено в реляционной схеме. Весь профиль может быть идентифицирован уникальным идентификатором <code>user_id</code>. Поля, такие как <code>first_name</code> и <code>last_name</code>, встречаются ровно один раз на пользователя, поэтому они могут быть смоделированы как столбцы в таблице <code>users</code>.</p>
<p>У большинства людей было больше одной работы за карьеру (<code>positions</code>), также у людей может быть разное количество периодов обучения и любое количество контактной информации. Один из способов представления таких <strong>связей «один-ко-многим»</strong> — поместить позиции, образование и контактную информацию в отдельные таблицы со ссылкой внешнего ключа на таблицу <code>users</code>, как на рисунке 3-1.</p>
<p><strong>Рисунок 3-1. Представление профиля LinkedIn с использованием реляционной схемы</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2008" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1.png" alt="" width="1772" height="1414" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-300x239.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-1024x817.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-768x613.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-1536x1226.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-450x359.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-780x622.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_1-1600x1277.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>Другой способ представления той же информации, который, возможно, выглядит более естественным и ближе к объектной структуре в коде приложения, — это <code>JSON-документ</code>, показанный в примере 3-1.</p>
<p><strong>Пример 3-1. Представление профиля LinkedIn как JSON-документа</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "user_id":     251,
  "first_name":  "Barack",
  "last_name":   "Obama",
  "headline":    "Former President of the United States of America",
  "region_id":   "us:91",
  "photo_url":   "/p/7/000/253/05b/308dd6e.jpg",
  "positions": [
    {"job_title": "President", "organization": "United States of America"},
    {"job_title": "US Senator (D-IL)", "organization": "United States Senate"}
  ],
  "education": [
    {"school_name": "Harvard University",  "start": 1988, "end": 1991},
    {"school_name": "Columbia University", "start": 1981, "end": 1983}
  ],
  "contact_info": {
    "website": "https://barackobama.com",
    "twitter": "https://twitter.com/barackobama"
  }
}</pre><p>Некоторые разработчики считают, что модель <code>JSON</code> уменьшает несогласованность импедансов между кодом приложения и уровнем хранения. Однако, как мы увидим в главе 5, <strong>у JSON как формата кодирования данных тоже есть проблемы.</strong></p>
<ul>
<li>Отсутствие схемы часто называют преимуществом; мы обсудим это в разделе «Гибкость схемы в документной модели».</li>
<li>Представление JSON обладает лучшей локальностью, чем многотабличная схема на рисунке 3-1 (см. «Локальность данных для чтения и записи»).</li>
<li>Если вы хотите получить профиль в реляционном примере, вам нужно либо выполнить несколько запросов (запрашивая каждую таблицу по user_id), либо выполнить громоздкий многотабличный join между таблицей users и её подчинёнными таблицами. В JSON-представлении вся релевантная информация находится в одном месте, что делает запрос одновременно быстрее и проще.</li>
</ul>
<p>Связи «один-ко-многим» из профиля пользователя к позициям, истории образования и контактной информации подразумевают древовидную структуру данных, и представление JSON делает эту структуру дерева явной (см. рисунок 3-2).</p>
<p><strong>Рисунок 3-2. Связи «один-ко-многим», формирующие древовидную структуру</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2009" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2.png" alt="" width="1772" height="780" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-300x132.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-1024x451.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-768x338.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-1536x676.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-450x198.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-780x343.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_2-1600x704.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p><strong>Примечание</strong></p>
<p>Такой тип связи иногда называют <strong>«один-к-немногим» (one-to-few)</strong>, а не <strong>«один-ко-многим» (one-to-many)</strong>, поскольку резюме обычно содержит небольшое количество позиций. В ситуациях, когда может быть действительно большое количество связанных элементов — например, комментарии к посту знаменитости в соцсети, которых могут быть тысячи, — встраивание их всех в один документ может оказаться слишком громоздким, поэтому предпочтительнее реляционный подход, показанный на рисунке 3-1.</p>
<h2>Нормализация, денормализация и join’ы</h2>
<p>В примере 3-1 в предыдущем разделе <code>region_id</code> приведён в виде <code>ID</code>, а не в виде текстовой строки &#171;Washington, DC, United States&#187;. <strong>Почему?</strong></p>
<p>Если в пользовательском интерфейсе есть текстовое поле для ввода региона, имеет смысл хранить его как текстовую строку. Но существуют преимущества наличия стандартизованных списков географических регионов и предоставления пользователю выбора из выпадающего списка или автокомплита:</p>
<ul>
<li>Единообразие стиля и написания в разных профилях</li>
<li><strong>Избежание неоднозначности</strong>, если существует несколько мест с одинаковым названием (если строка была бы просто &#171;Washington&#187;, то шла бы речь о DC или о штате?)</li>
<li><strong>Простота обновления</strong> — имя хранится только в одном месте, поэтому его легко обновить везде, если это когда-либо потребуется (например, смена названия города по политическим причинам)</li>
<li><strong>Поддержка локализации</strong> — при переводе сайта на другие языки стандартизованные списки могут быть локализованы, так что регион можно отобразить на языке пользователя</li>
<li><strong>Более качественный поиск</strong> — например, поиск людей на Восточном побережье США может сопоставить этот профиль, так как список регионов может закодировать факт, что Вашингтон находится на Восточном побережье (что не очевидно из строки &#171;Washington, DC&#187;)</li>
</ul>
<p>Храните ли вы ID или текстовую строку — это <strong>вопрос нормализации.</strong> Когда вы используете <code>ID</code>, ваши данные более нормализованы: информация, имеющая значение для человека (например, текст Washington, DC), хранится только в одном месте, и всё, что на неё ссылается, использует ID (который имеет смысл только внутри базы данных). Когда вы храните текст напрямую, вы дублируете человеко-понятную информацию в каждой записи, которая её использует; такое представление является денормализованным.</p>
<p>Преимущество использования <code>ID</code> в том, что, поскольку он не имеет смысла для человека, его никогда не нужно менять: <code>ID</code> может оставаться тем же самым, даже если изменяется информация, которую он идентифицирует. Всё, что имеет значение для человека, может потребовать изменения в будущем — и если эта информация продублирована, все избыточные копии придётся обновлять. Это требует большего объёма кода, большего числа операций записи, больше места на диске и несёт риск несогласованностей (когда часть копий информации обновлена, а часть — нет).</p>
<p><strong>Недостаток нормализованного представления</strong> заключается в том, что каждый раз, когда вы хотите отобразить запись, содержащую ID, вам <strong>нужно сделать дополнительный поиск</strong>, чтобы разрешить ID в человеко-читаемую форму.</p>
<p>В реляционной модели данных это делается с помощью <code>join</code>, например:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT users.*, regions.region_name
FROM users
JOIN regions ON users.region_id = regions.id
WHERE users.id = 251;</pre><p><strong>Документные базы данных могут хранить как нормализованные, так и денормализованные данные, но их часто связывают с денормализацией</strong> — отчасти потому, что модель данных JSON упрощает хранение дополнительных денормализованных полей, а отчасти потому, что <strong>слабая поддержка join’ов</strong> во многих документных базах данных делает нормализацию неудобной.</p>
<p>Некоторые документные базы данных вообще не поддерживают join’ы, поэтому их приходится выполнять в коде приложения — то есть сначала вы получаете документ, содержащий ID, а затем выполняете второй запрос, чтобы разрешить этот ID в другой документ. В MongoDB также можно выполнить join с использованием оператора <code>$lookup</code> в конвейере агрегации:</p><pre class="urvanov-syntax-highlighter-plain-tag">db.users.aggregate([
  { $match: { _id: 251 } },
  { $lookup: {
      from: "regions",
      localField: "region_id",
      foreignField: "_id",
      as: "region"
  } }
])</pre><p></p>
<h2>Компромиссы нормализации</h2>
<p>В примере с резюме, хотя поле <code>region_id</code> является ссылкой на стандартизованный набор регионов, имя организации (компании или госучреждения, где работал человек) и <code>school_name</code> (где он учился) — это просто строки. Такое представление является денормализованным: многие люди могли работать в одной и той же компании, но нет <code>ID</code>, который бы связывал их.</p>
<p>Возможно, организация и школа тоже должны быть сущностями, а профиль должен ссылаться на их <code>ID</code> вместо имён? Те же аргументы, которые применимы к ссылке на ID региона, применимы и здесь. Например, предположим, что мы хотим включить логотип школы или компании в дополнение к их названию:</p>
<ul>
<li>В денормализованном представлении мы бы включали URL изображения логотипа в профиль каждого человека; это делает JSON-документ самодостаточным, но создаёт проблему, если когда-либо потребуется изменить логотип, так как нам придётся найти все вхождения старого URL и обновить их.</li>
<li>В нормализованном представлении мы бы создали сущность, представляющую организацию или школу, и хранили её имя, URL логотипа и, возможно, другие атрибуты (описание, новостную ленту и т. д.) один раз в этой сущности. Каждое резюме, упоминающее организацию, просто ссылалось бы на её ID, и обновить логотип было бы легко.</li>
</ul>
<p><strong>В общем случае нормализованные данные обычно быстрее записывать (так как существует только одна копия), но медленнее читать (так как требуются join’ы); денормализованные данные, как правило, быстрее читать (меньше join’ов), но дороже записывать (больше копий для обновления, больше используемого дискового пространства).</strong> Вам может быть полезно рассматривать денормализацию как форму производных данных («Системы учёта и производные данные»), поскольку необходимо настроить процесс обновления избыточных копий данных.</p>
<p>Помимо стоимости выполнения всех этих обновлений, нужно также учитывать согласованность базы данных, если процесс завершится сбоем на полпути при выполнении обновлений. Базы данных, предлагающие атомарные транзакции (см. «Атомарность»), упрощают поддержание согласованности, но не все базы предоставляют атомарность для нескольких документов. Также можно обеспечить согласованность с помощью потоковой обработки.</p>
<p><strong>Нормализация, как правило, лучше подходит для OLTP-систем</strong>, где и чтение, и обновления должны быть быстрыми; аналитические системы часто лучше работают с денормализованными данными, так как они выполняют обновления пакетами, а производительность запросов только для чтения является доминирующей задачей. Более того, в системах малого и среднего масштаба нормализованная модель данных часто является лучшей, потому что не нужно беспокоиться о поддержании нескольких копий данных в согласованности друг с другом, а стоимость выполнения join’ов приемлема. Однако в системах очень большого масштаба стоимость join’ов может стать проблемной.</p>
<h2>Денормализация в примере с социальной сетью</h2>
<p>В разделе «Кейс: домашние таймлайны социальной сети» мы сравнивали нормализованное представление (рисунок 2-1) и денормализованное (предвычисленные, материализованные таймлайны): здесь join между постами и подписками оказался слишком дорогим, и материализованный таймлайн является кэшем результата этого join’а. Процесс fan-out, вставляющий новый пост в таймлайны подписчиков, был нашим способом поддерживать денормализованное представление в согласованности.</p>
<p>Однако реализация материализованных таймлайнов в X (ранее Twitter) не хранит фактический текст каждого поста: каждая запись фактически хранит только ID поста, ID пользователя, который его опубликовал, и немного дополнительной информации для идентификации репостов и ответов.</p>
<p>Другими словами, это предвычисленный результат (приблизительно) следующего запроса:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT posts.id, posts.sender_id 
FROM posts
JOIN follows ON posts.sender_id = follows.followee_id
WHERE follows.follower_id = current_user
ORDER BY posts.timestamp DESC
LIMIT 1000</pre><p>Это означает, что каждый раз при чтении таймлайна сервису всё равно нужно выполнять два join’а: искать ID поста, чтобы получить фактическое содержимое поста (а также статистику, например количество лайков и ответов), и искать профиль отправителя по ID (чтобы получить его имя пользователя, аватар и другие детали). Этот процесс поиска человеко-читаемой информации по ID называется гидратацией ID (hydrating the IDs), и это по сути join, выполняемый в коде приложения.</p>
<p>Причина хранения только ID в предвычисленном таймлайне заключается в том, что данные, на которые они ссылаются, быстро меняются: количество лайков и ответов может изменяться несколько раз в секунду у популярного поста, и некоторые пользователи регулярно меняют своё имя или фото профиля. Так как таймлайн должен показывать актуальное количество лайков и аватар в момент просмотра, не имело бы смысла денормализовать эту информацию в материализованный таймлайн. Более того, стоимость хранения значительно возросла бы при такой денормализации.</p>
<p>Этот пример показывает, что необходимость выполнения join’ов при чтении данных не является, как иногда утверждается, препятствием для создания высокопроизводительных, масштабируемых сервисов. Гидратация ID поста и ID пользователя на самом деле является достаточно лёгкой операцией для масштабирования, так как она хорошо параллелится, и её стоимость не зависит от количества аккаунтов, на которые вы подписаны, или количества ваших подписчиков.</p>
<p>Если вам нужно решить, денормализовать ли что-то в вашем приложении, кейс социальной сети показывает, что выбор не всегда очевиден: наиболее масштабируемый подход может включать денормализацию одних данных и сохранение нормализованного вида для других. Вам придётся тщательно учитывать, как часто меняется информация, и стоимость операций чтения и записи (которые могут определяться выбросами, такими как пользователи с большим количеством подписок/подписчиков в случае типичной социальной сети). <strong>Нормализация и денормализация сами по себе не являются хорошими или плохими — это лишь компромисс с точки зрения производительности чтения и записи, а также объёма усилий для реализации.</strong></p>
<h2>Связи «многие-к-одному» и «многие-ко-многим»</h2>
<p>Хотя позиции и образование на рисунке 3-1 являются примерами связей «один-ко-многим» или «один-к-немногим» (одно резюме имеет несколько позиций, но каждая позиция принадлежит только одному резюме), поле region_id является примером связи «многие-к-одному» (многие люди живут в одном регионе, но предполагается, что каждый человек живёт только в одном регионе в любой момент времени).</p>
<p>Если мы введём сущности для организаций и школ и будем ссылаться на них по ID из резюме, то у нас также появятся связи «многие-ко-многим» (один человек работал в нескольких организациях, и у организации есть несколько прошлых или текущих сотрудников). В реляционной модели такая связь обычно представляется как ассоциативная таблица или join-таблица, как показано на рисунке 3-3: каждая позиция связывает один ID пользователя с одним ID организации.</p>
<p><strong>Рисунок 3-3. Связи многие-ко-многим в реляционной модели</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2011" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3.png" alt="" width="1772" height="745" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-300x126.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-1024x431.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-768x323.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-1536x646.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-450x189.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-780x328.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_3-1600x673.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p><strong>Связи многие-к-одному и многие-ко-многим</strong> не так просто уместить в один автономный JSON-документ; они больше подходят для нормализованного представления. В документной модели возможное представление показано в примере 3-2 и проиллюстрировано на рисунке 3-4: данные внутри каждого пунктирного прямоугольника можно сгруппировать в один документ, но связи с организациями и учебными заведениями лучше всего представлять как ссылки на другие документы.</p>
<p><strong>Пример 3-2. Резюме, которое ссылается на организации по ID.</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "user_id":    251,
  "first_name": "Barack",
  "last_name":  "Obama",
  "positions": [
    {"start": 2009, "end": 2017, "job_title": "President",         "org_id": 513},
    {"start": 2005, "end": 2008, "job_title": "US Senator (D-IL)", "org_id": 514}
  ],
  ...
}</pre><p><strong>Рисунок 3-4. Связи многие-ко-многим в документной модели: данные внутри каждого пунктирного блока можно сгруппировать в один документ</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2012" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4.png" alt="" width="1772" height="1121" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-300x190.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-1024x648.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-768x486.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-1536x972.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-450x285.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-780x493.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_4-1600x1012.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p><strong>Связи многие-ко-многим</strong> часто нужно запрашивать «в обоих направлениях»: например, найти все организации, в которых работал конкретный человек, и найти всех людей, которые работали в конкретной организации. Один из способов обеспечить такие запросы — хранить ссылки на ID с обеих сторон, то есть резюме включает ID каждой организации, где работал человек, а документ организации включает ID резюме, которые её упоминают. Такое представление денормализовано, поскольку связь хранится в двух местах, что может привести к несоответствиям.</p>
<p><strong>Нормализованное представление</strong> хранит связь только в одном месте и полагается на вторичные индексы, чтобы обеспечить эффективные запросы в обоих направлениях. В реляционной схеме на рисунке 3-3 мы сказали бы базе данных создать индексы по столбцам user_id и org_id таблицы positions.</p>
<p><strong>В документной модели</strong> из примера 3-2 базе данных нужно индексировать поле org_id внутри объектов массива positions. Многие документные базы данных и реляционные СУБД с поддержкой JSON умеют создавать такие индексы по значениям внутри документа.</p>
<h2>Звёзды и снежинки: схемы для аналитики</h2>
<p>Хранилища данных (см. «Хранилище данных») обычно реляционные, и есть несколько широко используемых соглашений для структуры таблиц в хранилище данных: <strong>звёздная схема, снежинка, dimensional modeling (измерительное моделирование) и одна большая таблица (OBT)</strong>. Эти структуры оптимизированы под нужды бизнес-аналитиков. ETL-процессы преобразуют данные из операционных систем в эту схему.</p>
<p><strong>Рисунок 3-5 показывает пример звёздной схемы</strong>, которая может использоваться в хранилище данных продуктового ритейлера.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2013" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5.png" alt="" width="1772" height="1482" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-300x251.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-1024x856.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-768x642.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-1536x1285.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-450x376.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-780x652.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_5-1600x1338.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>В центре схемы находится так называемая <strong>факт-таблица</strong> (в этом примере она называется fact_sales). Каждая строка факт-таблицы представляет событие, произошедшее в определённое время (здесь каждая строка — это покупка продукта клиентом). Если бы мы анализировали веб-трафик, а не розничные продажи, каждая строка могла бы представлять просмотр страницы или клик пользователя.</p>
<p><strong>Звёздная или снежинка-схема</strong> в основном состоит из <strong>отношений «многие-к-одному»</strong> (например, многие продажи происходят для одного конкретного продукта, в одном конкретном магазине), представленных как факт-таблица с внешними ключами на таблицы измерений или измерения на подизмерения. В принципе, могут существовать и другие типы отношений, но их часто денормализуют, чтобы упростить запросы. Например, если покупатель приобретает несколько разных продуктов за раз, эта многопозиционная транзакция явно не представляется; вместо этого в факт-таблице есть отдельная строка для каждого купленного продукта, и все эти факты просто имеют одинаковый ID клиента, ID магазина и временную метку.</p>
<p>Некоторые схемы хранилищ данных идут ещё дальше в денормализации и полностью исключают таблицы измерений, сворачивая информацию измерений в денормализованные столбцы самой факт-таблицы (по сути, предвычисляя соединение между факт-таблицей и таблицами измерений). Такой подход называется <strong>«одна большая таблица» (one big table, OBT)</strong>, и хотя он требует больше места для хранения, иногда он позволяет выполнять запросы быстрее.</p>
<p>В аналитическом контексте такая денормализация не является проблемой, так как данные обычно представляют собой журнал исторической информации, которая не будет изменяться (за исключением редких исправлений ошибок). Проблемы согласованности данных и избыточных операций записи, возникающие при денормализации в OLTP-системах, в аналитике не так остры.</p>
<h2>Когда использовать какую модель</h2>
<p><strong>Основные аргументы в пользу документной модели данных</strong> — это гибкость схемы, лучшая производительность благодаря локальности и то, что для некоторых приложений она ближе к объектной модели, используемой в коде приложения. Реляционная модель противопоставляет этому лучшую поддержку соединений, отношений «многие-к-одному» и «многие-ко-многим». Давайте рассмотрим эти аргументы подробнее.</p>
<p>Если данные в вашем приложении имеют документоподобную структуру (т.е. дерево отношений «один-ко-многим», где обычно всё дерево загружается целиком), тогда, вероятно, разумно использовать документную модель. Реляционная техника «измельчения» — разбиение документоподобной структуры на несколько таблиц (как positions, education и contact_info на Рисунке 3-1) — может приводить к громоздким схемам и излишне усложнённому коду приложения.</p>
<p>Документная модель имеет ограничения: например, вы не можете напрямую ссылаться на вложенный элемент внутри документа, а вместо этого должны говорить что-то вроде «второй элемент в списке должностей для пользователя 251». Если нужно ссылаться на вложенные элементы, лучше работает реляционный подход, так как можно напрямую обращаться к любому элементу по его ID.</p>
<p>Некоторые приложения позволяют пользователю выбирать порядок элементов: например, список задач или баг-трекер, где пользователь может перетаскивать задачи для изменения их порядка. Документная модель хорошо поддерживает такие приложения, так как элементы (или их ID) могут быть просто сохранены в JSON-массиве для определения порядка. В реляционных базах данных нет стандартного способа представления таких переставляемых списков, и используются различные приёмы: сортировка по целочисленному столбцу (с требованием перенумеровки при вставке в середину), связанный список ID или дробная индексация.</p>
<h2>Гибкость схемы в документной модели</h2>
<p>Большинство документных баз данных и поддержка JSON в реляционных базах не навязывают никакой схемы данным в документах. Поддержка XML в реляционных БД обычно сопровождается опциональной валидацией схемы. Отсутствие схемы означает, что в документ можно добавлять произвольные ключи и значения, а при чтении клиент не имеет гарантий относительно того, какие поля может содержать документ.</p>
<p>Документные базы иногда называют «безсхемными», но это вводит в заблуждение, так как код, читающий данные, обычно предполагает некоторую структуру — т.е. существует неявная схема, но она не навязывается базой. Более точный термин — schema-on-read (схема при чтении: структура данных неявная и интерпретируется только при чтении), в отличие от schema-on-write (схема при записи: традиционный подход реляционных баз, где схема явная, и база гарантирует, что все данные соответствуют ей при записи).</p>
<p>Schema-on-read похож на динамическую (в runtime) проверку типов в языках программирования, тогда как schema-on-write похож на статическую (на этапе компиляции) проверку типов. Как сторонники статической и динамической проверки типов спорят об их относительных достоинствах, так и валидация схем в базах данных является спорной темой, и в целом здесь нет правильного или неправильного ответа.</p>
<p>Различие подходов особенно заметно, когда приложение хочет изменить формат своих данных. Например, допустим, сейчас вы храните полное имя пользователя в одном поле, а хотите вместо этого хранить имя и фамилию отдельно. В документной базе вы бы просто начали записывать новые документы с новыми полями и добавили код в приложение, которое обрабатывает случай, когда читаются старые документы. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">if (user &amp;&amp; user.name &amp;&amp; !user.first_name) {
    // Documents written before Dec 8, 2023 don't have first_name
    user.first_name = user.name.split(" ")[0];
}</pre><p>Недостаток такого подхода в том, что каждая часть вашего приложения, читающая из базы, теперь должна уметь работать с документами старых форматов, которые могли быть записаны очень давно. С другой стороны, в базе с schema-on-write вы бы обычно выполнили миграцию примерно такого вида:</p><pre class="urvanov-syntax-highlighter-plain-tag">ALTER TABLE users ADD COLUMN first_name text DEFAULT NULL;
UPDATE users SET first_name = split_part(name, ' ', 1);      -- PostgreSQL
UPDATE users SET first_name = substring_index(name, ' ', 1);      -- MySQL</pre><p>В большинстве реляционных баз данных добавление столбца со значением по умолчанию выполняется быстро и без проблем даже на больших таблицах. Однако выполнение оператора UPDATE, скорее всего, будет медленным на большой таблице, так как нужно переписать каждую строку, и другие операции со схемой (например, изменение типа данных столбца) также обычно требуют копирования всей таблицы.</p>
<p>Существуют различные инструменты, позволяющие выполнять такого рода изменения схемы в фоновом режиме без простоя, но проведение таких миграций на больших базах остаётся операционно сложной задачей. Сложных миграций можно избежать, добавив только столбец first_name со значением NULL по умолчанию (что быстро), а заполнять его при чтении, как вы бы делали в документной базе.</p>
<p><strong>Schema-on-read подход</strong> выгоден, если элементы в коллекции по каким-то причинам имеют разную структуру (т.е. данные гетерогенны) — например:</p>
<ul>
<li>существует множество разных типов объектов, и нецелесообразно помещать каждый тип объекта в отдельную таблицу;</li>
<li>структура данных определяется внешними системами, над которыми у вас нет контроля, и которые могут изменяться в любой момент.</li>
</ul>
<p>В таких случаях схема может скорее мешать, чем помогать, и «безсхемные» документы могут быть гораздо более естественной моделью данных. Но в случаях, когда ожидается, что все записи будут иметь одинаковую структуру, схемы являются полезным механизмом документирования и обеспечения этой структуры. Мы подробнее рассмотрим схемы и их эволюцию в Главе 5.</p>
<h2>Локальность данных при чтении и записи</h2>
<p>Документ обычно хранится как одна непрерывная строка, закодированная в JSON, XML или двоичном варианте этих форматов (например, BSON в MongoDB). Если вашему приложению часто требуется доступ ко всему документу (например, для отображения его на веб-странице), то в такой локальности хранения есть преимущество в производительности. Если данные разделены между несколькими таблицами, как на рисунке 3-1, для их извлечения требуется несколько обращений к индексам, что может потребовать больше обращений к диску и занять больше времени.</p>
<p>Преимущество локальности применимо только в том случае, если вам нужны большие части документа одновременно. Обычно базе данных приходится загружать весь документ, что может быть неэффективно, если требуется лишь малая часть большого документа. При обновлениях документа обычно нужно переписать его целиком. По этим причинам обычно рекомендуется хранить документы достаточно маленькими и избегать частых мелких обновлений документа.</p>
<p>Однако идея хранения связанных данных вместе ради локальности не ограничивается документной моделью. Например, база данных Spanner от Google предоставляет те же свойства локальности в реляционной модели данных, позволяя схеме указывать, что строки таблицы должны быть вложены (вложенные) внутри родительской таблицы. Oracle позволяет то же самое, используя функцию под названием multi-table index cluster tables. Модель широких столбцов, популяризованная Google Bigtable и используемая, например, в HBase и Accumulo, имеет концепцию семейств столбцов, которые служат той же цели управления локальностью.</p>
<h1>Языки запросов для документов</h1>
<p>Ещё одно различие между реляционной и документной базой данных — это язык или API, используемый для их запросов. Большинство реляционных баз данных запрашиваются с помощью SQL, но документные базы данных более разнообразны. Некоторые позволяют только доступ «ключ-значение» по первичному ключу, другие также предлагают вторичные индексы для поиска значений внутри документов, а некоторые предоставляют богатые языки запросов.</p>
<p>XML-базы данных часто запрашиваются с помощью <strong>XQuery</strong> и <strong>XPath</strong>, которые позволяют выполнять сложные запросы, включая соединения между несколькими документами, а также форматировать результаты как XML. <strong>JSON Pointer</strong> и <strong>JSONPath</strong> предоставляют аналог <strong>XPath</strong> для <strong>JSON</strong>. Конвейер агрегации MongoDB, оператор <code>$lookup</code> которого для соединений мы видели в разделе «Нормализация, денормализация и соединения», является примером языка запросов к коллекциям JSON-документов.</p>
<p>Рассмотрим ещё один пример, чтобы почувствовать этот язык — на этот раз агрегацию, которая особенно нужна для аналитики. Представьте, что вы морской биолог и добавляете запись наблюдений в базу данных каждый раз, когда видите животных в океане. Теперь вы хотите создать отчёт о том, сколько акул вы видели по месяцам. В PostgreSQL такой запрос можно выразить так:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT date_trunc('month', observation_timestamp) AS observation_month, 
       sum(num_animals) AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;</pre><p>Функция <code>date_trunc('month', timestamp)</code> определяет календарный месяц, содержащий метку времени, и возвращает другую метку времени, представляющую начало этого месяца. Другими словами, она округляет метку времени вниз до ближайшего месяца.</p>
<p>Этот запрос сначала фильтрует наблюдения так, чтобы показывались только виды из семейства акул (Sharks), затем группирует наблюдения по календарному месяцу, в котором они произошли, и, наконец, суммирует количество животных, увиденных во всех наблюдениях за этот месяц.</p>
<p><strong>Тот же запрос можно выразить с помощью конвейера агрегации MongoDB следующим образом:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">db.observations.aggregate([
    { $match: { family: "Sharks" } },
    { $group: {
        _id: {
            year:  { $year:  "$observationTimestamp" },
            month: { $month: "$observationTimestamp" }
        },
        totalAnimals: { $sum: "$numAnimals" }
    } }
]);</pre><p>Язык конвейера агрегации по выразительности похож на подмножество SQL, но использует JSON-подобный синтаксис вместо синтаксиса в стиле английских предложений; различие, пожалуй, вопрос вкуса.</p>
<h2>Сближение документных и реляционных баз данных</h2>
<p>Документные базы данных и реляционные базы данных начинали как очень разные подходы к управлению данными, но со временем они стали всё более похожими. Реляционные базы данных добавили поддержку типов JSON и операторов запросов, а также возможность индексировать свойства внутри документов. <strong>Некоторые документные базы данных (такие как MongoDB, Couchbase и RethinkDB) добавили поддержку соединений, вторичных индексов и декларативных языков запросов.</strong></p>
<p>Это сближение моделей — хорошая новость для разработчиков приложений, поскольку реляционная модель и документная модель лучше всего работают, когда их можно комбинировать в одной базе данных. Многие документные базы данных нуждаются в реляционных ссылках на другие документы, а многие реляционные базы данных имеют области, где полезна гибкость схемы. Гибриды реляционно-документных баз данных — это мощное сочетание.</p>
<p><strong>ПРИМЕЧАНИЕ</strong></p>
<p>Оригинальное описание реляционной модели Коддом фактически допускало нечто подобное JSON внутри реляционной схемы. Он называл это несложные домены (nonsimple domains). Идея заключалась в том, что значение в строке не обязательно должно быть примитивным типом данных, таким как число или строка, но оно также может быть вложенным отношением (таблицей) — то есть вы можете иметь произвольно вложенную древовидную структуру в качестве значения, очень похожую на поддержку JSON или XML, добавленную в SQL более чем через 30 лет.</p>
<h2>Графоподобные модели данных</h2>
<p>Мы уже видели, что тип связей является важной отличительной особенностью между различными моделями данных. Если в вашем приложении в основном встречаются связи типа «один-ко-многим» (древовидные данные) и мало других связей между записями, то модель документа подходит.</p>
<p><strong>Но что, если в ваших данных очень распространены связи «многие-ко-многим»?</strong> Реляционная модель может обрабатывать простые случаи связей «многие-ко-многим», но по мере усложнения связей внутри данных становится более естественным моделировать их в виде графа.</p>
<p><strong>Граф состоит из двух видов объектов:</strong> вершины (также называемые узлами или сущностями) и рёбра (также называемые связями или дугами). Многие виды данных можно смоделировать как граф.</p>
<p>Типичные примеры:</p>
<ul>
<li><strong>Социальные графы</strong><br />
<strong>Вершины</strong> — это люди, а рёбра показывают, кто с кем знаком.</li>
<li><strong>Граф интернета</strong><br />
<strong>Вершины</strong> — это веб-страницы, а рёбра показывают HTML-ссылки на другие страницы.</li>
<li><strong>Дорожные или железнодорожные сети</strong><br />
<strong>Вершины</strong> — это перекрёстки, а рёбра представляют дороги или железнодорожные линии между ними.</li>
</ul>
<p>Существуют хорошо известные алгоритмы, которые могут работать с такими графами: например, приложения навигации ищут кратчайший путь между двумя точками в дорожной сети, а PageRank может использоваться для веб-графа, чтобы определить популярность веб-страницы и, соответственно, её рейтинг в результатах поиска.</p>
<p>Графы можно представлять несколькими различными способами. В модели списков смежности каждая вершина хранит идентификаторы соседних вершин, находящихся на расстоянии одного ребра. В качестве альтернативы можно использовать матрицу смежности — двумерный массив, где каждая строка и каждый столбец соответствуют вершине, значение равно нулю, если между вершинами строки и столбца нет ребра, и единице, если есть. Список смежности хорошо подходит для обхода графа, а матрица — для задач машинного обучения (см. раздел «Dataframes, Matrices, and Arrays»).</p>
<p>В приведённых выше примерах все вершины графа представляют один и тот же тип сущностей (люди, веб-страницы или дорожные развязки). Однако графы не ограничены такими однородными данными: не менее мощное применение графов — это предоставление единого способа хранения совершенно разных типов объектов в одной базе данных. Например:</p>
<p>Facebook поддерживает единый граф с множеством различных типов вершин и рёбер: вершины представляют людей, места, события, отметки посещений и комментарии пользователей; рёбра показывают, кто с кем дружит, какая отметка посещения относится к какому месту, кто прокомментировал какой пост, кто посетил какое событие и т. д.</p>
<p>Графы знаний используются поисковыми системами для записи фактов о сущностях, часто встречающихся в поисковых запросах, таких как организации, люди и места. Эта информация получается путём обхода и анализа текста на веб-сайтах; некоторые сайты, такие как Wikidata, также публикуют графовые данные в структурированной форме.</p>
<p>Существует несколько разных, но связанных способов структурирования и запросов данных в графах. В этом разделе мы рассмотрим модель property graph (реализованную в Neo4j, Memgraph, KùzuDB и других) и модель triple-store (реализованную в Datomic, AllegroGraph, Blazegraph и других). Эти модели достаточно похожи по возможностям выражения, и некоторые графовые базы данных (например, Amazon Neptune) поддерживают обе модели.</p>
<p>Мы также рассмотрим четыре языка запросов для графов (Cypher, SPARQL, Datalog и GraphQL), а также поддержку SQL для запросов к графам. Существуют и другие языки запросов к графам, например Gremlin, но выбранные дадут нам репрезентативный обзор.</p>
<p>Чтобы проиллюстрировать эти разные языки и модели, в этом разделе используется граф, показанный на рисунке 3-6, в качестве сквозного примера. Его можно было бы взять из социальной сети или генеалогической базы: он показывает двух людей — Люси из Айдахо и Алена из Сент-Ло, Франция. Они женаты и живут в Лондоне. Каждый человек и каждое место представлены вершинами, а связи между ними — рёбрами. Этот пример поможет продемонстрировать некоторые запросы, которые легко выполнять в графовых базах данных, но трудно — в других моделях.</p>
<p><strong>Рисунок 3-6. Пример данных графовой структуры (прямоугольники представляют вершины, стрелки представляют рёбра)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2014" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6.png" alt="" width="1772" height="1065" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-300x180.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-1024x615.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-768x462.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-1536x923.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-450x270.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-780x469.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_6-1600x962.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<h2>Property Graphs</h2>
<p><strong>В модели <code>property graph</code> (также известной как labeled property graph) каждая вершина состоит из:</strong></p>
<ul>
<li>Уникального идентификатора</li>
<li>Метки (строка), описывающей, какой тип объекта представляет эта вершина</li>
<li>Набора исходящих рёбер</li>
<li>Набора входящих рёбер</li>
<li>Коллекции свойств (пары ключ-значение)</li>
</ul>
<p><strong>Каждое ребро состоит из:</strong></p>
<ul>
<li>Уникального идентификатора</li>
<li>Вершины, из которой ребро начинается (хвостовая вершина)</li>
<li>Вершины, в которой ребро заканчивается (головная вершина)</li>
<li>Метки, описывающей тип связи между двумя вершинами</li>
<li>Коллекции свойств (пары ключ-значение)</li>
</ul>
<p>Графовое хранилище можно представить как две реляционные таблицы: одну для вершин и одну для рёбер, как показано в примере 3-3 (эта схема использует тип данных jsonb PostgreSQL для хранения свойств каждой вершины или ребра). Для каждого ребра хранится головная и хвостовая вершина; если вы хотите получить набор входящих или исходящих рёбер для вершины, вы можете запросить таблицу рёбер по head_vertex или tail_vertex соответственно.</p>
<p><strong>Пример 3-3. Представление property graph с использованием реляционной схемы</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE vertices (
    vertex_id   integer PRIMARY KEY,
    label       text,
    properties  jsonb
);

CREATE TABLE edges (
    edge_id     integer PRIMARY KEY,
    tail_vertex integer REFERENCES vertices (vertex_id),
    head_vertex integer REFERENCES vertices (vertex_id),
    label       text,
    properties  jsonb
);

CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);</pre><p><strong>Некоторые важные аспекты этой модели:</strong></p>
<ul>
<li>Любая вершина может иметь ребро, соединяющее её с любой другой вершиной. Нет схемы, которая ограничивает, какие типы объектов можно или нельзя связывать.</li>
<li>Для любой вершины можно эффективно найти как её входящие, так и исходящие рёбра, и таким образом обходить граф — то есть следовать по пути через цепочку вершин — как вперёд, так и назад. (Именно поэтому в Примере 3-3 есть индексы как по столбцам tail_vertex, так и по head_vertex.)</li>
<li>Используя разные метки для разных типов вершин и связей, вы можете хранить несколько различных видов информации в одном графе, сохраняя при этом чистую модель данных.</li>
<li>Таблица рёбер похожа на таблицу связей «многие-ко-многим»/join-таблицу, которую мы видели в разделе «Связи многие-к-одному и многие-ко-многим», но обобщённую так, чтобы можно было хранить много разных типов связей в одной таблице. Также могут существовать индексы по меткам и свойствам, что позволяет эффективно находить вершины или рёбра с определёнными свойствами.</li>
</ul>
<p><strong>ПРИМЕЧАНИЕ</strong></p>
<p><strong>Ограничение графовых моделей заключается в том, что ребро может связывать только две вершины друг с другом, тогда как реляционная join-таблица может представлять трёхсторонние или даже более сложные связи, имея несколько внешних ключей в одной строке.</strong> Такие связи могут быть представлены в графе путём создания дополнительной вершины, соответствующей каждой строке join-таблицы, и рёбер к/от этой вершины, или с помощью гиперграфа.</p>
<p>Эти особенности дают графам большую гибкость при моделировании данных, как показано на Рисунке 3-6. На рисунке изображены несколько вещей, которые было бы сложно выразить в традиционной реляционной схеме, например разные виды региональных структур в разных странах (во Франции есть департаменты и регионы, а в США — округа и штаты), особенности истории вроде страны внутри страны (пока что игнорируя тонкости суверенных государств и наций), а также различная детализация данных (текущее место жительства Люси указано на уровне города, а место рождения — только на уровне штата).</p>
<p>Можно представить, что граф расширяется и включает многие другие факты о Люси и Алене или других людях. Например, его можно использовать для обозначения пищевых аллергий (добавив вершину для каждого аллергена и ребро между человеком и аллергеном, указывающее на аллергию), а также связать аллергены с набором вершин, показывающих, какие продукты содержат какие вещества. Затем можно написать запрос, чтобы выяснить, что безопасно есть каждому человеку. Графы хороши для эволюционности: по мере того как вы добавляете функции в приложение, граф легко расширяется, чтобы учесть изменения в структурах данных приложения.</p>
<h2>Язык запросов Cypher</h2>
<p><strong>Cypher</strong> — это язык запросов для графов свойств, изначально созданный для базы данных <strong>Neo4j</strong>, а затем развитый в открытый стандарт <strong>openCypher</strong>. Помимо Neo4j, Cypher поддерживают <strong>Memgraph</strong>, <strong>KuzuDB</strong>, <strong>Amazon Neptune</strong>, <strong>Apache AGE</strong> (с хранением в PostgreSQL) и другие. Его название происходит от персонажа фильма Матрица и не связано с шифрами в криптографии.</p>
<p>Пример 3-4 показывает запрос Cypher для вставки левой части рисунка 3-6 в графовую базу данных. Остальная часть графа может быть добавлена аналогично. Каждой вершине даётся символическое имя, например usa или idaho. Это имя не сохраняется в базе данных, а используется только внутри запроса для создания рёбер между вершинами с помощью стрелочной нотации:</p><pre class="urvanov-syntax-highlighter-plain-tag">(idaho) -[:WITHIN]-&gt; (usa)</pre><p>создаёт ребро с меткой WITHIN, где idaho является начальной вершиной, а usa — конечной.</p>
<p><strong>Пример 3-4. Подмножество данных с Рисунка 3-6, представленное в виде запроса Cypher</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE
  (namerica :Location {name:'North America',  type:'continent'}),
  (usa      :Location {name:'United States',  type:'country'  }),
  (idaho    :Location {name:'Idaho',          type:'state'    }),
  (lucy     :Person   {name:'Lucy' }),
  (idaho) -[:WITHIN ]-&gt; (usa)  -[:WITHIN]-&gt; (namerica),
  (lucy)  -[:BORN_IN]-&gt; (idaho)</pre><p>Когда все вершины и рёбра с Рисунка 3-6 добавлены в базу данных, можно начинать задавать интересные вопросы: например, найти имена всех людей, которые эмигрировали из США в Европу. То есть найти все вершины, которые имеют ребро BORN_IN к локации в пределах США и одновременно ребро LIVING_IN к локации в пределах Европы, и вернуть свойство name каждой из этих вершин.</p>
<p>Пример 3-5 показывает, как выразить этот запрос на языке Cypher. Та же стрелочная нотация используется в предложении <code>MATCH</code> для поиска шаблонов в графе:</p><pre class="urvanov-syntax-highlighter-plain-tag">(person) -[:BORN_IN]-&gt; ()</pre><p>соответствует любым двум вершинам, связанным ребром с меткой BORN_IN. Начальная вершина этого ребра привязывается к переменной person, а конечная вершина остаётся безымянной.</p>
<p><strong>Пример 3-5. Запрос Cypher для поиска людей, эмигрировавших из США в Европу</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">MATCH
  (person) -[:BORN_IN]-&gt;  () -[:WITHIN*0..]-&gt; (:Location {name:'United States'}),
  (person) -[:LIVES_IN]-&gt; () -[:WITHIN*0..]-&gt; (:Location {name:'Europe'})
RETURN person.name</pre><p><strong>Запрос можно читать так:</strong><br />
Найти любую вершину (person), которая удовлетворяет обоим условиям:</p>
<ul>
<li>У person есть исходящее ребро BORN_IN к некоторой вершине. Из этой вершины можно следовать по цепочке исходящих рёбер WITHIN, пока в итоге не достигнется вершина типа Location, у которой свойство name равно &#171;United States&#187;.</li>
<li>У той же вершины person также есть исходящее ребро LIVES_IN. Следуя по этому ребру и затем по цепочке исходящих рёбер WITHIN, в итоге достигается вершина типа Location, у которой свойство name равно &#171;Europe&#187;.</li>
<li>Для каждой такой вершины person вернуть свойство name.</li>
</ul>
<p>Существует несколько возможных способов выполнения запроса. Описание выше предполагает, что вы начинаете с обхода всех людей в базе, проверяете место рождения и место жительства каждого и возвращаете только тех, кто соответствует критериям.</p>
<p>Но равноценно можно начать с двух вершин Location и двигаться назад. Если есть индекс по свойству name, можно эффективно найти вершины, представляющие США и Европу. Затем можно найти все локации (штаты, регионы, города и т.д.) в США и Европе, следуя по всем входящим рёбрам WITHIN. Наконец, можно найти людей, которые соединены входящим ребром BORN_IN или LIVES_IN с одной из этих вершин-локаций.</p>
<h2>Запросы к графам в SQL</h2>
<p>Пример 3-3 предполагал, что графовые данные могут быть представлены в реляционной базе данных. Но если мы поместим графовые данные в реляционную структуру, можем ли мы также выполнять к ним запросы с помощью SQL?</p>
<p><strong>Ответ — да, но с определёнными сложностями.</strong> Каждое ребро, которое вы проходите в графовом запросе, по сути является join’ом с таблицей рёбер. В реляционной базе данных вы обычно заранее знаете, какие именно join’ы нужны в вашем запросе. С другой стороны, в графовом запросе вам может понадобиться пройти переменное количество рёбер, прежде чем вы найдёте вершину, которую ищете, — то есть количество join’ов не фиксировано заранее.</p>
<p>В нашем примере это происходит в шаблоне <code>() -[:WITHIN*0..]-&gt; ()</code> в Cypher-запросе. Ребро LIVES_IN у человека может указывать на любой тип локации: улицу, город, район, регион, штат и т.д. Город может находиться WITHIN региона, регион WITHIN штата, штат WITHIN страны и так далее. Ребро LIVES_IN может указывать непосредственно на вершину-локацию, которую вы ищете, либо находиться на несколько уровней выше в иерархии локаций.</p>
<p>В Cypher конструкция <code>:WITHIN*0..</code> выражает эту идею очень лаконично: она означает «следуй по ребру WITHIN ноль или более раз». Это похоже на оператор * в регулярных выражениях.</p>
<p>Начиная с SQL:1999, эту идею переменной длины путей обхода в запросе можно выразить с помощью так называемых рекурсивных общих табличных выражений (синтаксис <code>WITH RECURSIVE</code>). В примере 3-6 показан тот же запрос — поиск имён людей, которые эмигрировали из США в Европу — записанный на SQL с использованием этой техники. Однако синтаксис получается очень громоздким по сравнению с Cypher.</p>
<p><strong>Пример 3-6. Тот же запрос, что и в примере 3-5, но записанный на SQL с использованием рекурсивных общих табличных выражений</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">WITH RECURSIVE

  -- in_usa is the set of vertex IDs of all locations within the United States
  in_usa(vertex_id) AS (
      SELECT vertex_id FROM vertices
        WHERE label = 'Location' AND properties-&gt;&gt;'name' = 'United States' 
    UNION
      SELECT edges.tail_vertex FROM edges 
        JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
        WHERE edges.label = 'within'
  ),

  -- in_europe is the set of vertex IDs of all locations within Europe
  in_europe(vertex_id) AS (
      SELECT vertex_id FROM vertices
        WHERE label = 'location' AND properties-&gt;&gt;'name' = 'Europe' 
    UNION
      SELECT edges.tail_vertex FROM edges
        JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
        WHERE edges.label = 'within'
  ),

  -- born_in_usa is the set of vertex IDs of all people born in the US
  born_in_usa(vertex_id) AS ( 
    SELECT edges.tail_vertex FROM edges
      JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
      WHERE edges.label = 'born_in'
  ),

  -- lives_in_europe is the set of vertex IDs of all people living in Europe
  lives_in_europe(vertex_id) AS ( 
    SELECT edges.tail_vertex FROM edges
      JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
      WHERE edges.label = 'lives_in'
  )

SELECT vertices.properties-&gt;&gt;'name'
FROM vertices
-- join to find those people who were both born in the US *and* live in Europe
JOIN born_in_usa     ON vertices.vertex_id = born_in_usa.vertex_id 
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;</pre><p></p>
<ol>
<li>Сначала найдите вершину, у которой свойство name имеет значение &#171;United States&#187;, и сделайте её первым элементом множества вершин in_usa.</li>
<li>Следуйте по всем входящим рёбрам within от вершин из множества in_usa и добавляйте их в то же множество до тех пор, пока все входящие рёбра within не будут посещены.</li>
<li>Сделайте то же самое, начиная с вершины, у которой свойство name имеет значение &#171;Europe&#187;, и постройте множество вершин in_europe.</li>
<li>Для каждой вершины из множества in_usa следуйте по входящим рёбрам born_in, чтобы найти людей, которые родились в каком-либо месте в пределах United States.</li>
<li>Аналогично, для каждой вершины из множества in_europe следуйте по входящим рёбрам lives_in, чтобы найти людей, которые живут в Europe.</li>
<li>Наконец, пересеките множество людей, рождённых в USA, с множеством людей, живущих в Europe, выполнив их join.</li>
</ol>
<p>Тот факт, что 4 строки Cypher-запроса требуют 31 строку в SQL, показывает, насколько выбор правильной модели данных и языка запросов может иметь значение. И это только начало; есть ещё больше деталей, которые необходимо учитывать, например, работа с циклами и выбор между обходом в ширину или в глубину. Oracle использует другое SQL-расширение для рекурсивных запросов, которое оно называет hierarchical. Однако ситуация может улучшиться: на момент написания планируется добавить язык запросов к графам под названием GQL в стандарт SQL, который обеспечит синтаксис, вдохновлённый Cypher, GSQL и PGQL.</p>
<h2>Triple-Stores и SPARQL</h2>
<p><strong>Модель triple-store</strong> в основном эквивалентна модели property graph, используя разные слова для описания одних и тех же идей. Тем не менее стоит её обсудить, так как существует множество инструментов и языков для triple-store, которые могут стать ценным дополнением к вашему инструментарию для построения приложений.</p>
<p>В triple-store вся информация хранится в виде очень простых трёхчастных утверждений: (subject, predicate, object). Например, в триплете (Jim, likes, bananas) Jim является subject, likes — predicate (глагол), а bananas — object.</p>
<p><strong>Subject триплета эквивалентен вершине в графе.</strong> Object может быть одним из двух:<br />
Значением примитивного типа данных, например строкой или числом. В этом случае predicate и object триплета эквивалентны ключу и значению свойства вершины subject. Используя пример с рисунка 3-6, (lucy, birthYear, 1989) — это как вершина lucy со свойствами {&#171;birthYear&#187;: 1989}.<br />
Другой вершиной в графе. В этом случае predicate — это ребро в графе, subject — это вершина-источник, а object — вершина-назначение. Например, в (lucy, marriedTo, alain) и subject, и object (lucy и alain) являются вершинами, а predicate marriedTo — это метка ребра, соединяющего их.</p>
<p><strong>ПРИМЕЧАНИЕ</strong></p>
<p>Чтобы быть точным, базы данных, предлагающие triple-подобную модель данных, часто должны хранить дополнительные метаданные для каждой кортежа. Например, AWS Neptune использует quads (4-элем. кортежи), добавляя graph ID к каждому триплету; Datomic использует 5-элем. кортежи, расширяя каждый триплет идентификатором транзакции и булевым значением, указывающим на удаление. Так как эти базы данных сохраняют базовую структуру subject-predicate-object, описанную выше, в этой книге они тем не менее называются triple-stores.</p>
<p>В примере 3-7 показаны те же данные, что и в примере 3-4, записанные в виде триплетов в формате, называемом Turtle, подмножестве Notation3 (N3).</p>
<p><strong>Пример 3-7. Подмножество данных с рисунка 3-6, представленное в виде Turtle-триплетов</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">@prefix : &lt;urn:example:&gt;.
_:lucy     a       :Person.
_:lucy     :name   "Lucy".
_:lucy     :bornIn _:idaho.
_:idaho    a       :Location.
_:idaho    :name   "Idaho".
_:idaho    :type   "state".
_:idaho    :within _:usa.
_:usa      a       :Location.
_:usa      :name   "United States".
_:usa      :type   "country".
_:usa      :within _:namerica.
_:namerica a       :Location.
_:namerica :name   "North America".
_:namerica :type   "continent".</pre><p>В этом примере вершины графа записаны как _:someName. Имя не имеет никакого значения за пределами этого файла; оно существует только потому, что иначе мы не знали бы, какие триплеты ссылаются на одну и ту же вершину. Когда предикат представляет ребро, объект является вершиной, как в _:idaho :within _:usa. Когда предикат является свойством, объект — строковый литерал, как в _:usa :name &#171;United States&#187;.</p>
<p>Довольно утомительно повторять один и тот же subject снова и снова, но, к счастью, можно использовать точки с запятой, чтобы сказать несколько вещей об одном и том же subject. Это делает формат Turtle довольно удобочитаемым: см. пример 3-8.</p>
<p><strong>Пример 3-8. Более компактный способ записи данных из примера 3-7</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">@prefix : &lt;urn:example:&gt;.
_:lucy     a :Person;   :name "Lucy";          :bornIn _:idaho.
_:idaho    a :Location; :name "Idaho";         :type "state";   :within _:usa.
_:usa      a :Location; :name "United States"; :type "country"; :within _:namerica.
_:namerica a :Location; :name "North America"; :type "continent".</pre><p></p>
<h2>СЕМАНТИЧЕСКАЯ СЕТЬ</h2>
<p>Некоторая часть исследовательских и разработческих усилий вокруг triple-store была мотивирована <strong>Семантической Сетью</strong> — инициативой начала 2000-х, целью которой было упростить обмен данными в масштабах интернета путём публикации данных не только в виде веб-страниц, читаемых человеком, но и в стандартизированном, машиночитаемом формате. Хотя Семантическая Сеть в её изначально задуманном виде не состоялась, наследие проекта Semantic Web живёт в ряде конкретных технологий: стандартах связанных данных, таких как JSON-LD, онтологиях, используемых в биомедицинской науке, протоколе Open Graph от Facebook (который используется для link unfurling), графах знаний, таких как Wikidata, и стандартизированных словарях для структурированных данных, поддерживаемых schema.org.</p>
<p><strong>Triple-store</strong> — это ещё одна технология Semantic Web, которая нашла применение за пределами своего первоначального сценария: даже если у вас нет интереса к Semantic Web, триплеты могут быть хорошей внутренней моделью данных для приложений.</p>
<h2>Модель данных RDF</h2>
<p><strong>Язык Turtle</strong>, который мы использовали в примере 3-8, на самом деле является способом кодирования данных в <strong>Resource Description Framework (RDF)</strong>, модели данных, которая была разработана для Semantic Web. RDF-данные также могут быть закодированы другими способами, например (более многословно) в XML, как показано в примере 3-9. Инструменты вроде Apache Jena могут автоматически конвертировать между различными RDF-кодировками.</p>
<p><strong>Пример 3-9. Данные из примера 3-8, представленные с использованием синтаксиса RDF/XML</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;rdf:RDF xmlns="urn:example:"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"&gt;

  &lt;Location rdf:nodeID="idaho"&gt;
    &lt;name&gt;Idaho&lt;/name&gt;
    &lt;type&gt;state&lt;/type&gt;
    &lt;within&gt;
      &lt;Location rdf:nodeID="usa"&gt;
        &lt;name&gt;United States&lt;/name&gt;
        &lt;type&gt;country&lt;/type&gt;
        &lt;within&gt;
          &lt;Location rdf:nodeID="namerica"&gt;
            &lt;name&gt;North America&lt;/name&gt;
            &lt;type&gt;continent&lt;/type&gt;
          &lt;/Location&gt;
        &lt;/within&gt;
      &lt;/Location&gt;
    &lt;/within&gt;
  &lt;/Location&gt;

  &lt;Person rdf:nodeID="lucy"&gt;
    &lt;name&gt;Lucy&lt;/name&gt;
    &lt;bornIn rdf:nodeID="idaho"/&gt;
  &lt;/Person&gt;
&lt;/rdf:RDF&gt;</pre><p>У RDF есть несколько особенностей из-за того, что он был разработан для обмена данными в масштабе интернета. Subject, predicate и object триплета часто являются URI. Например, предикат может быть URI, таким как <code>http://my-company.com/namespace#within</code> или <code>http://my-company.com/namespace#lives_in</code>, а не просто WITHIN или LIVES_IN. Логика этого дизайна заключается в том, что вы должны иметь возможность комбинировать свои данные с чьими-то ещё, и если они придают другое значение слову <code>within</code> или <code>lives_in</code>, то конфликта не будет, так как их предикаты на самом деле <code>http://other.org/foo#within</code> и <code>http://other.org/foo#lives_in</code>.</p>
<p>URL <code>http://my-company.com/namespace</code> не обязательно должен что-либо разрешать — с точки зрения RDF это просто namespace. Чтобы избежать возможной путаницы с <code>http:// URL</code>, примеры в этом разделе используют неразрешаемые URI, такие как <code>urn:example:within</code>. К счастью, вы можете просто один раз указать этот префикс в начале файла, а затем забыть о нём.</p>
<h2>Язык запросов SPARQL</h2>
<p><strong>SPARQL</strong> — это язык запросов для triple-store, использующих модель данных RDF. (Это акроним от <strong>SPARQL Protocol and RDF Query Language</strong>, произносится как “sparkle”.) Он появился раньше <strong>Cypher</strong>, и так как сопоставление паттернов в <strong>Cypher</strong> заимствовано из <strong>SPARQL</strong>, они выглядят весьма похоже.</p>
<p>Тот же самый запрос, что и раньше — поиск людей, которые переехали из US в Europe — столь же лаконичен в <strong>SPARQL</strong>, как и в <strong>Cypher</strong> (см. пример 3-10).</p>
<p><strong>Пример 3-10. Тот же запрос, что и в примере 3-5, выраженный на SPARQL</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">PREFIX : &lt;urn:example:&gt;

SELECT ?personName WHERE {
  ?person :name ?personName.
  ?person :bornIn  / :within* / :name "United States".
  ?person :livesIn / :within* / :name "Europe".
}</pre><p>Структура очень похожа. Следующие два выражения эквивалентны (в SPARQL переменные начинаются со знака вопроса):</p><pre class="urvanov-syntax-highlighter-plain-tag">(person) -[:BORN_IN]-&gt; () -[:WITHIN*0..]-&gt; (location)   # Cypher

?person :bornIn / :within* ?location.                   # SPARQL</pre><p>Так как RDF не различает свойства и рёбра, а просто использует предикаты для обоих случаев, вы можете применять один и тот же синтаксис для сопоставления свойств. В следующем выражении переменная usa связывается с любой вершиной, у которой есть свойство name со значением строки &#171;United States&#187;:</p><pre class="urvanov-syntax-highlighter-plain-tag">(usa {name:'United States'})   # Cypher

?usa :name "United States".    # SPARQL</pre><p>SPARQL поддерживается Amazon Neptune, AllegroGraph, Blazegraph, OpenLink Virtuoso, Apache Jena и различными другими triple-store.</p>
<h2>Datalog: рекурсивные реляционные запросы</h2>
<p><strong>Datalog</strong> — гораздо более старый язык, чем SPARQL или Cypher: он возник из академических исследований в 1980-х. Он менее известен среди разработчиков ПО и не имеет широкой поддержки в мейнстримных базах данных, но заслуживает большего внимания, так как это очень выразительный язык, особенно мощный для сложных запросов. Несколько нишевых баз данных, включая Datomic, LogicBlox, CozoDB и LIquid от LinkedIn, используют Datalog в качестве языка запросов.</p>
<p>Datalog фактически основан на реляционной модели данных, а не на графе, но он упоминается в разделе про графовые базы данных этой книги, потому что рекурсивные запросы на графах — это особая сильная сторона Datalog.</p>
<p><strong>Содержимое базы данных Datalog состоит из фактов, и каждый факт соответствует строке в реляционной таблице.</strong> Например, скажем, у нас есть таблица location, содержащая локации, и в ней три колонки: ID, name и type. Тот факт, что US является страной, может быть записан как location(2, &#171;United States&#187;, &#171;country&#187;), где 2 — это ID для US. В общем случае выражение table(val1, val2, …) означает, что таблица содержит строку, где первый столбец содержит val1, второй столбец содержит val2 и так далее.</p>
<p>В примере 3-11 показано, как записать данные с левой части рисунка 3-6 в Datalog. Рёбра графа (within, born_in и lives_in) представлены как двухколоночные join-таблицы. Например, Lucy имеет ID 100, а Idaho имеет ID 3, поэтому отношение “Lucy родилась в Idaho” представлено как born_in(100, 3).</p>
<p><strong>Пример 3-11. Подмножество данных с рисунка 3-6, представленное в виде фактов Datalog</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">location(1, "North America", "continent").
location(2, "United States", "country").
location(3, "Idaho", "state").

within(2, 1).    /* US is in North America */
within(3, 2).    /* Idaho is in the US     */

person(100, "Lucy").
born_in(100, 3). /* Lucy was born in Idaho */</pre><p>Теперь, когда мы определили данные, мы можем записать тот же запрос, что и раньше, как показано в примере 3-12. Он выглядит немного иначе, чем эквиваленты в Cypher или SPARQL, но не позволяйте этому вас смутить. Datalog — это подмножество Prolog, языка программирования, который вы могли встречать раньше, если изучали информатику.</p>
<p><strong>Пример 3-12. Тот же запрос, что и в примере 3-5, выраженный в Datalog</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">within_recursive(LocID, PlaceName) :- location(LocID, PlaceName, _). /* Rule 1 */

within_recursive(LocID, PlaceName) :- within(LocID, ViaID),          /* Rule 2 */
                                      within_recursive(ViaID, PlaceName).

migrated(PName, BornIn, LivingIn)  :- person(PersonID, PName),       /* Rule 3 */
                                      born_in(PersonID, BornID),
                                      within_recursive(BornID, BornIn),
                                      lives_in(PersonID, LivingID),
                                      within_recursive(LivingID, LivingIn).

us_to_europe(Person) :- migrated(Person, "United States", "Europe"). /* Rule 4 */
/* us_to_europe contains the row "Lucy". */</pre><p>Cypher и SPARQL начинают сразу же с SELECT, но Datalog делает небольшие шаги. Мы определяем правила, которые выводят новые виртуальные таблицы из базовых фактов. Эти производные таблицы похожи на (виртуальные) SQL-представления: они не сохраняются в базе данных, но вы можете запрашивать их так же, как таблицу, содержащую сохранённые факты.</p>
<p>В примере 3-12 мы определяем три производные таблицы: within_recursive, migrated и us_to_europe. Имя и колонки виртуальных таблиц определяются тем, что стоит перед символом :- в каждом правиле. Например, migrated(PName, BornIn, LivingIn) — это виртуальная таблица с тремя колонками: имя человека, название места, где он родился, и название места, где он живёт.</p>
<p>Содержимое виртуальной таблицы определяется частью правила после символа :-, где мы пытаемся найти строки, соответствующие определённому паттерну в таблицах. Например, person(PersonID, PName) соответствует строке person(100, &#171;Lucy&#187;), где переменная PersonID связывается со значением 100, а переменная PName связывается со значением &#171;Lucy&#187;. Правило применяется, если система может найти соответствие для всех паттернов в правой части оператора <code>:-</code> . Когда правило применяется, это как если бы левая часть <code>:-</code> была добавлена в базу данных (с заменой переменных на значения, которым они соответствуют).</p>
<p>Один из возможных способов применения правил следующий (и как показано на рисунке 3-7):</p>
<ul>
<li><code>location(1, "North America", "continent")</code> существует в базе данных, поэтому правило 1 применяется. Оно генерирует <code>within_recursive(1, "North America")</code>.</li>
<li><code>within(2, 1)</code> существует в базе данных, и предыдущий шаг сгенерировал <code>within_recursive(1, "North America")</code>, поэтому правило 2 применяется. Оно генерирует <code>within_recursive(2, "North America")</code>.</li>
<li><code>within(3, 2)</code> существует в базе данных, и предыдущий шаг сгенерировал <code>within_recursive(2, "North America")</code>, поэтому правило 2 применяется. Оно генерирует <code>within_recursive(3, "North America")</code>.</li>
</ul>
<p>Путём многократного применения правил 1 и 2 виртуальная таблица within_recursive может показать нам все локации в North America (или любой другой локации), содержащиеся в нашей базе данных.</p>
<p><strong>Рисунок 3-7. Определение того, что Idaho находится в North America, с использованием правил Datalog из примера 3-12</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2015" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7.png" alt="" width="1772" height="585" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-300x99.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-1024x338.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-768x254.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-1536x507.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-450x149.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-780x258.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_7-1600x528.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>Теперь правило 3 может найти людей, которые родились в некоторой локации BornIn и живут в некоторой локации LivingIn. Правило 4 вызывает правило 3 с BornIn = &#8216;United States&#8217; и LivingIn = &#8216;Europe&#8217; и возвращает только имена людей, которые соответствуют поиску. Запрашивая содержимое виртуальной таблицы us_to_europe, система Datalog наконец получает тот же ответ, что и в предыдущих запросах на Cypher и SPARQL.</p>
<p>Подход Datalog требует иного мышления по сравнению с другими языками запросов, обсуждёнными в этой главе. Он позволяет строить сложные запросы шаг за шагом, правило за правилом, где одно правило ссылается на другие, подобно тому, как вы разбиваете код на функции, вызывающие друг друга. И так же, как функции могут быть рекурсивными, правила Datalog также могут вызывать сами себя, как правило 2 в примере 3-12, что делает возможным обход графа в запросах Datalog.</p>
<h2>GraphQL</h2>
<p><strong>GraphQL</strong> — это язык запросов, который по замыслу гораздо более ограниченный, чем другие языки запросов, рассмотренные в этой главе. Цель GraphQL — позволить клиентскому ПО, работающему на устройстве пользователя (например, мобильному приложению или JavaScript-фронтенду веб-приложения), запрашивать JSON-документ с определённой структурой, содержащий поля, необходимые для отображения его пользовательского интерфейса. Интерфейсы GraphQL позволяют разработчикам быстро изменять запросы в клиентском коде без изменения серверных API.</p>
<p>Гибкость GraphQL имеет свою цену. Организациям, которые принимают GraphQL, часто требуется инструментарий для преобразования GraphQL-запросов в запросы к внутренним сервисам, которые часто используют REST или gRPC. Авторизация, ограничение частоты запросов и проблемы с производительностью — это дополнительные вопросы. Язык запросов GraphQL также ограничен, так как GraphQL поступает из недоверенного источника. Язык не позволяет выполнять ничего такого, что могло бы быть дорогостоящим в исполнении, иначе пользователи могли бы провести denial-of-service атаку на сервер, запустив множество дорогих запросов. В частности, GraphQL не допускает рекурсивные запросы (в отличие от Cypher, SPARQL, SQL или Datalog), и он не позволяет произвольные условия поиска, такие как “найти людей, которые родились в US и сейчас живут в Europe” (если только владельцы сервиса специально не решат предоставить такую возможность поиска).</p>
<p>Тем не менее GraphQL полезен. В примере 3-13 показано, как можно реализовать групповое чат-приложение вроде Discord или Slack с использованием GraphQL. Запрос получает все каналы, к которым имеет доступ пользователь, включая имя канала и 50 последних сообщений в каждом канале. Для каждого сообщения запрашиваются временная метка, содержимое сообщения, а также имя и URL аватара отправителя. Более того, если сообщение является ответом на другое сообщение, запрос также получает имя отправителя и содержимое сообщения, на которое был дан ответ (которое может отображаться более мелким шрифтом над ответом, чтобы дать немного контекста).</p>
<p><strong>Пример 3-13. Пример GraphQL-запроса для группового чат-приложения</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">query ChatApp {
  channels {
    name
    recentMessages(latest: 50) {
      timestamp
      content
      sender {
        fullName
        imageUrl
      }
      replyTo {
        content
        sender {
          fullName
        }
      }
    }
  }
}</pre><p>Пример 3-14 показывает, как может выглядеть ответ на запрос из примера 3-13. Ответ — это JSON-документ, который отражает структуру запроса: он содержит ровно те атрибуты, которые были запрошены, ни больше, ни меньше. Такой подход имеет то преимущество, что серверу не нужно знать, какие атрибуты требуются клиенту для отображения пользовательского интерфейса; вместо этого клиент может просто запросить то, что ему нужно. Например, этот запрос не запрашивает URL аватара для отправителя сообщения replyTo, но если бы пользовательский интерфейс изменили так, чтобы добавить этот аватар, клиенту было бы легко добавить требуемый атрибут imageUrl в запрос без изменения сервера.</p>
<p><strong>Пример 3-14. Возможный ответ на запрос из примера 3-13</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "data": {
    "channels": [
      {
        "name": "#general",
        "recentMessages": [
          {
            "timestamp": 1693143014,
            "content": "Hey! How are y'all doing?",
            "sender": {"fullName": "Aaliyah", "imageUrl": "https://..."},
            "replyTo": null
          },
          {
            "timestamp": 1693143024,
            "content": "Great! And you?",
            "sender": {"fullName": "Caleb", "imageUrl": "https://..."},
            "replyTo": {
              "content": "Hey! How are y'all doing?",
              "sender": {"fullName": "Aaliyah"}
            }
          },
          ...</pre><p>В примере 3-14 имя и URL аватара отправителя сообщения встроены непосредственно в объект сообщения. Если один и тот же пользователь отправляет несколько сообщений, эта информация повторяется в каждом сообщении. В принципе, можно было бы уменьшить это дублирование, но GraphQL делает выбор в пользу увеличения размера ответа, чтобы упростить отображение пользовательского интерфейса на основе данных.</p>
<p>Поле replyTo устроено аналогично: в примере 3-14 второе сообщение является ответом на первое, и содержимое (“Hey!…”) и отправитель Aaliyah дублируются внутри replyTo. Можно было бы вместо этого вернуть ID сообщения, на которое дан ответ, но тогда клиенту пришлось бы делать дополнительный запрос к серверу, если этот ID не входит в 50 последних возвращённых сообщений. Дублирование содержимого делает работу с данными значительно проще.</p>
<p>База данных сервера может хранить данные в более нормализованной форме и выполнять необходимые соединения для обработки запроса. Например, сервер может хранить сообщение вместе с ID пользователя-отправителя и ID сообщения, на которое дан ответ; получив такой запрос, как выше, сервер затем разрешает эти ID, чтобы найти соответствующие записи. Однако клиент может попросить сервер выполнить только такие соединения, которые явно предусмотрены в схеме GraphQL.</p>
<p>Хотя ответ на запрос GraphQL выглядит похожим на ответ от документной базы данных и хотя в названии есть слово “graph”, GraphQL может быть реализован поверх любой базы данных — реляционной, документной или графовой.</p>
<h1>Event Sourcing и CQRS</h1>
<p>Во всех моделях данных, которые мы обсуждали до сих пор, данные запрашиваются в том же виде, в котором они записаны — будь то JSON-документы, строки в таблицах или вершины и рёбра в графе. Однако в сложных приложениях иногда бывает трудно найти единую форму представления данных, которая смогла бы удовлетворить все различные способы их запроса и отображения. В таких ситуациях может быть полезно записывать данные в одной форме, а затем выводить из них несколько представлений, оптимизированных для разных типов чтения.</p>
<p>Мы уже видели эту идею в разделе “Системы учёта и производные данные”, а ETL (см. “Хранилище данных”) — это один из примеров такого процесса преобразования. Теперь мы пойдём дальше. Если мы всё равно собираемся выводить одно представление данных из другого, мы можем выбрать разные представления, соответственно оптимизированные для записи и для чтения. Как бы вы смоделировали свои данные, если бы хотели оптимизировать их только для записи, а эффективность запросов не имела бы значения?</p>
<p>Возможно, самый простой, быстрый и выразительный способ записи данных — это журнал событий: каждый раз, когда вы хотите записать какие-то данные, вы кодируете их в виде самодостаточной строки (например, в формате JSON), включая временную метку, а затем добавляете в последовательность событий. События в этом журнале неизменяемы: вы никогда не изменяете и не удаляете их, вы только добавляете новые события в журнал (которые могут заменить более ранние события). Событие может содержать произвольные свойства.</p>
<p>На рисунке 3-8 показан пример, который мог бы быть взят из системы управления конференцией. Конференция может быть сложной предметной областью: индивидуальные участники могут регистрироваться и оплачивать участие картой, компании могут заказывать места оптом, оплачивать по счёту, а затем позже распределять места между конкретными людьми. Некоторое количество мест может быть зарезервировано для докладчиков, спонсоров, добровольцев и так далее. Резервации также могут быть отменены, а организатор конференции может изменить вместимость мероприятия, переместив его в другое помещение. При всём этом простое вычисление числа доступных мест становится сложным запросом.</p>
<p><strong>Рисунок 3-8. Использование журнала неизменяемых событий как источника истины и вывод из него материализованных представлений</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2016" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8.png" alt="" width="1772" height="1321" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-300x224.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-1024x763.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-768x573.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-1536x1145.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-280x210.png 280w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-450x335.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-780x581.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_8-1600x1193.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>На рисунке 3-8 каждое изменение состояния конференции (например, когда организатор открывает регистрацию или участники делают и отменяют бронирования) сначала сохраняется как событие. Каждый раз, когда событие добавляется в журнал, несколько материализованных представлений (также называемых проекциями или моделями для чтения) также обновляются, чтобы отразить эффект этого события. В примере с конференцией может существовать одно материализованное представление, которое собирает всю информацию о статусе каждого бронирования, другое, которое вычисляет графики для панели управления организатора конференции, и третье, которое генерирует файлы для принтера, печатающего бейджи участников.</p>
<p>Идея использования событий как источника истины и выражения каждого изменения состояния в виде события называется event sourcing. Принцип поддержания отдельных представлений, оптимизированных для чтения, и их вывода из представления, оптимизированного для записи, называется command query responsibility segregation (CQRS). Эти термины появились в сообществе domain-driven design (DDD), хотя подобные идеи существуют уже давно, например в репликации конечных автоматов.</p>
<p>Когда поступает запрос от пользователя, он называется команда, и сначала его нужно проверить. Только когда команда выполнена и признана корректной (например, было достаточно доступных мест для запрошенного бронирования), она становится фактом, и соответствующее событие добавляется в журнал. Следовательно, журнал событий должен содержать только корректные события, а потребитель журнала событий, который строит материализованное представление, не имеет права отклонить событие.</p>
<p>При моделировании данных в стиле event sourcing рекомендуется называть события в прошедшем времени (например, “места были забронированы”), потому что событие — это запись факта о том, что что-то произошло в прошлом. Даже если пользователь позже решит изменить или отменить бронирование, остаётся фактом, что у него ранее была бронь, а изменение или отмена — это отдельное событие, которое добавляется позже.</p>
<p>Сходство между event sourcing и таблицей фактов в звёздной схеме, как обсуждалось в разделе “Звёзды и снежинки: схемы для аналитики”, заключается в том, что и то и другое — это коллекции событий, которые произошли в прошлом. Однако строки в таблице фактов имеют одинаковый набор колонок, тогда как в event sourcing может существовать множество различных типов событий, каждое со своими свойствами. Более того, таблица фактов — это неупорядоченная коллекция, а в event sourcing порядок событий важен: если сначала бронирование было сделано, а потом отменено, обработка этих событий в неправильном порядке не имела бы смысла.</p>
<p><strong>Event sourcing и CQRS имеют несколько преимуществ:</strong></p>
<ul>
<li>Для разработчиков системы события лучше передают намерение, почему что-то произошло. Например, проще понять событие “бронирование было отменено”, чем “в таблице bookings в строке 4001 колонка active была установлена в false, три строки, связанные с этим бронированием, были удалены из таблицы seat_assignments, и строка с возвратом была добавлена в таблицу payments”. Эти изменения строк всё равно могут произойти, когда материализованное представление обработает событие отмены, но когда они вызваны событием, причина обновлений становится гораздо яснее.</li>
<li>Ключевой принцип event sourcing заключается в том, что материализованные представления выводятся из журнала событий воспроизводимым образом: вы всегда должны иметь возможность удалить материализованные представления и пересоздать их, обработав те же события в том же порядке, используя тот же код. Если в коде поддержки представлений была ошибка, вы можете просто удалить представление и пересоздать его с новым кодом. Ошибку также проще найти, потому что вы можете запускать код поддержки представлений столько раз, сколько нужно, и проверять его поведение.</li>
<li>У вас может быть несколько материализованных представлений, оптимизированных для конкретных запросов, необходимых вашему приложению. Они могут храниться либо в той же базе данных, что и события, либо в другой, в зависимости от ваших нужд. Они могут использовать любую модель данных и могут быть денормализованы для быстрых чтений. Вы даже можете хранить представление только в памяти и не сохранять его, если можно пересоздать представление из журнала событий при каждом перезапуске сервиса.</li>
<li>Если вы решите представить существующую информацию новым способом, легко построить новое материализованное представление из уже существующего журнала событий. Вы также можете развивать систему для поддержки новых функций, добавляя новые типы событий или новые свойства в существующие типы событий (старые события при этом остаются неизменными). Вы также можете связывать новые поведения с существующими событиями (например, когда участник конференции отменяет бронь, его место можно предложить следующему в списке ожидания).</li>
<li>Если событие было записано по ошибке, вы можете удалить его и затем пересобрать представления без этого события. С другой стороны, в базе данных, где данные обновляются и удаляются напрямую, зафиксированную транзакцию часто трудно отменить. Event sourcing таким образом может уменьшить количество необратимых действий в системе, облегчая внесение изменений (см. “Эволюционируемость: делаем изменения лёгкими”).</li>
<li>Журнал событий также может служить журналом аудита всего, что произошло в системе, что ценно в регулируемых отраслях, где требуется такая возможность аудита.</li>
</ul>
<p><strong>Однако у event sourcing и CQRS есть и недостатки:</strong></p>
<ul>
<li>Нужно быть осторожным, если вовлечена внешняя информация. Например, пусть событие содержит цену в одной валюте, а для одного из представлений её нужно конвертировать в другую валюту. Так как обменный курс может колебаться, было бы проблематично запрашивать курс из внешнего источника при обработке события, потому что при пересоздании представления в другой день вы получите другой результат. Чтобы сделать логику обработки событий детерминированной, нужно либо включить курс валюты прямо в событие, либо иметь способ запрашивать исторический курс валюты на временную метку, указанную в событии, гарантируя, что этот запрос всегда возвращает один и тот же результат для одной и той же временной метки.</li>
<li>Требование неизменяемости событий создаёт проблемы, если события содержат персональные данные пользователей, так как пользователи могут воспользоваться своим правом (например, по GDPR) потребовать удаления своих данных. Если журнал событий ведётся на уровне одного пользователя, вы можете просто удалить весь журнал этого пользователя, но это не сработает, если журнал содержит события, относящиеся к нескольким пользователям. Можно попробовать хранить персональные данные вне самого события или шифровать их ключом, который позже можно удалить, но это также усложняет пересоздание производных состояний при необходимости.</li>
<li>Повторная обработка событий требует осторожности, если есть внешние видимые побочные эффекты — например, вы, вероятно, не захотите повторно отправлять письма с подтверждением каждый раз, когда пересобираете материализованное представление.</li>
<li>Вы можете реализовать event sourcing поверх любой базы данных, но существуют также системы, специально разработанные для поддержки этого паттерна, такие как EventStoreDB, MartenDB (на базе PostgreSQL) и Axon Framework. Также можно использовать брокеры сообщений, такие как Apache Kafka, для хранения журнала событий, а процессоры потоков могут поддерживать актуальность материализованных представлений.</li>
<li>Единственное важное требование состоит в том, что система хранения событий должна гарантировать, что все материализованные представления обрабатывают события в точно том же порядке, в каком они появляются в журнале; достичь этого в распределённой системе не всегда просто.</li>
</ul>
<h1>Dataframes, матрицы и массивы</h1>
<p>Модели данных, которые мы рассмотрели до сих пор в этой главе, как правило, используются как для обработки транзакций, так и для аналитических целей (см. “Аналитические против операционных систем”). Существуют также некоторые модели данных, которые вы, вероятно, встретите в аналитическом или научном контексте, но которые редко используются в OLTP-системах: dataframes и многомерные числовые массивы, такие как матрицы.</p>
<p><strong>Dataframes</strong> — это модель данных, поддерживаемая языком R, библиотекой Pandas для Python, Apache Spark, ArcticDB, Dask и другими системами. Это популярный инструмент для data scientists при подготовке данных к обучению моделей машинного обучения, но также он широко используется для исследования данных, статистического анализа данных, визуализации данных и аналогичных целей.</p>
<p>На первый взгляд dataframe похож на таблицу в реляционной базе данных или электронную таблицу. Он поддерживает операции, подобные реляционным, которые выполняют массовые операции над содержимым dataframe: например, применение функции ко всем строкам, фильтрация строк на основе некоторого условия, группировка строк по некоторым столбцам и агрегация других столбцов, объединение строк одного dataframe с другим dataframe по некоторому ключу (то, что реляционная база данных называет join, обычно называется merge в dataframes).</p>
<p>Вместо декларативного запроса, такого как SQL, dataframe обычно обрабатывается серией команд, которые изменяют его структуру и содержимое. Это соответствует типичному рабочему процессу data scientists, которые постепенно “wrangle” данные в форму, позволяющую находить ответы на интересующие вопросы. Эти манипуляции обычно происходят на приватной копии набора данных у data scientist, часто на локальной машине, хотя конечный результат может быть разделён с другими пользователями.</p>
<p>API dataframes также предоставляют широкий спектр операций, которые выходят далеко за рамки того, что предлагают реляционные базы данных, и модель данных часто используется способами, сильно отличающимися от типичного реляционного моделирования данных. Например, обычным использованием dataframes является преобразование данных из представления, похожего на реляционное, в представление в виде матрицы или многомерного массива, которое требуется многим алгоритмам машинного обучения для их входных данных.</p>
<p>Простой пример такого преобразования показан на рисунке 3-9. Слева у нас есть реляционная таблица, в которой показано, как разные пользователи оценили различные фильмы (по шкале от 1 до 5), а справа данные преобразованы в матрицу, где каждый столбец соответствует фильму, а каждая строка — пользователю (аналогично сводной таблице в электронной таблице). Матрица разреженная, то есть для многих комбинаций пользователь–фильм нет данных, но это нормально. Эта матрица может иметь много тысяч столбцов и поэтому не очень хорошо впишется в реляционную базу данных, но dataframes и библиотеки, которые поддерживают разреженные массивы (например, NumPy для Python), могут легко работать с такими данными.</p>
<p><strong>Рисунок 3-9. Преобразование реляционной базы данных рейтингов фильмов в матричное представление</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2017" src="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9.png" alt="" width="1772" height="690" srcset="https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9.png 1772w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-300x117.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-1024x399.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-768x299.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-1536x598.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-450x175.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-780x304.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/data_model_image_3_9-1600x623.png 1600w" sizes="(max-width: 1772px) 100vw, 1772px" /></a></p>
<p>Матрица может содержать только числа, и для преобразования нечисловых данных в числа в матрице используются различные техники. Например:<br />
Даты (которые опущены в примерной матрице на рисунке 3-9) можно масштабировать так, чтобы они стали числами с плавающей точкой в пределах некоторого подходящего диапазона.<br />
Для столбцов, которые могут принимать только одно из небольшого фиксированного набора значений (например, жанр фильма в базе фильмов), часто используется one-hot encoding: мы создаём столбец для каждого возможного значения (один для “комедии”, один для “драмы”, один для “ужасов” и т. д.), и для каждой строки, представляющей фильм, ставим 1 в столбце, соответствующем жанру этого фильма, и 0 во всех остальных. Такое представление также легко обобщается на фильмы, относящиеся сразу к нескольким жанрам.</p>
<p>Как только данные находятся в форме матрицы чисел, они становятся пригодными для операций линейной алгебры, которые лежат в основе многих алгоритмов машинного обучения. Например, данные на рисунке 3-9 могут быть частью системы рекомендаций фильмов, которые может понравиться пользователю. Dataframes достаточно гибки, чтобы позволить постепенно эволюционировать данные из реляционной формы в матричное представление, при этом предоставляя data scientist контроль над представлением, которое наиболее подходит для достижения целей анализа данных или процесса обучения модели.</p>
<p>Существуют также базы данных, такие как TileDB, которые специализируются на хранении больших многомерных массивов чисел; они называются array databases и чаще всего используются для научных наборов данных, таких как геопространственные измерения (растровые данные на регулярно расположенной сетке), медицинская визуализация или наблюдения с астрономических телескопов. Dataframes также используются в финансовой индустрии для представления временных рядов данных, таких как цены активов и сделки во времени.</p>
<h1>Резюме по главе</h1>
<p><strong>Модели данных</strong> — огромная тема, и в этой главе мы сделали краткий обзор широкого спектра различных моделей. У нас не было места, чтобы вдаваться во все детали каждой модели, но, надеемся, обзор оказался достаточным, чтобы разжечь интерес к дальнейшему изучению модели, которая лучше всего соответствует требованиям вашего приложения.</p>
<p>Реляционная модель, несмотря на то, что ей больше полувека, остаётся важной моделью данных для многих приложений — особенно в области хранилищ данных и бизнес-аналитики, где реляционные схемы типа звезды или снежинки и SQL-запросы повсеместны. Однако в других доменах также стали популярными несколько альтернатив реляционным данным:<br />
Модель документов нацелена на сценарии, где данные приходят в виде автономных JSON-документов, и где связи между одним документом и другим встречаются редко.<br />
Графовые модели данных идут в противоположном направлении, ориентируясь на сценарии, где всё потенциально связано со всем, и где запросы потенциально должны проходить через несколько “хопов”, чтобы найти интересующие данные (что можно выразить с помощью рекурсивных запросов в Cypher, SPARQL или Datalog).<br />
Dataframes обобщают реляционные данные на большое количество столбцов, тем самым предоставляя мост между базами данных и многомерными массивами, которые составляют основу значительной части машинного обучения, статистического анализа данных и научных вычислений.</p>
<p>В определённой степени одну модель можно эмулировать в терминах другой модели — например, графовые данные можно представить в реляционной базе данных — но результат может быть неудобным, как мы видели на примере поддержки рекурсивных запросов в SQL.</p>
<p>Поэтому были разработаны различные специализированные базы данных для каждой модели данных, предоставляющие языки запросов и движки хранения, оптимизированные для конкретной модели. Однако также существует тенденция к тому, что базы данных расширяются в соседние ниши, добавляя поддержку других моделей данных: например, реляционные базы данных добавили поддержку документных данных в виде JSON-столбцов, документные базы данных добавили реляционно-подобные join-операции, а поддержка графовых данных в SQL постепенно улучшается.</p>
<p>Ещё одна модель, которую мы обсудили, — это event sourcing, которая представляет данные в виде добавляемого только вперёд лога неизменяемых событий и которая может быть полезной для моделирования активностей в сложных бизнес-доменах. Append-only лог хорош для записи данных (как мы увидим в главе 4); чтобы поддерживать эффективные запросы, event log преобразуется в оптимизированные для чтения материализованные представления через CQRS.</p>
<p>Общим свойством нереляционных моделей данных является то, что они обычно не применяют схему к данным, которые они хранят, что может облегчить адаптацию приложений к меняющимся требованиям. Однако, скорее всего, ваше приложение всё же предполагает, что данные имеют определённую структуру; это лишь вопрос того, является ли схема явной (применяется при записи) или неявной (предполагается при чтении).</p>
<p>Хотя мы рассмотрели много материала, остались ещё модели данных, которые мы не упомянули. Чтобы привести лишь несколько кратких примеров:<br />
Исследователи, работающие с геномными данными, часто должны выполнять поиски по схожести последовательностей, что означает необходимость взять одну очень длинную строку (представляющую молекулу ДНК) и сопоставить её с большой базой строк, которые похожи, но не идентичны. Ни одна из описанных здесь баз данных не может справиться с таким использованием, поэтому исследователи написали специализированное программное обеспечение для геномных баз данных, например GenBank.<br />
Многие финансовые системы используют бухгалтерские книги с двойной записью как модель данных. Эти данные могут быть представлены в реляционных базах данных, но также существуют базы данных, такие как TigerBeetle, которые специализируются на этой модели данных. Криптовалюты и блокчейны обычно основаны на распределённых бухгалтерских книгах, в которых также встроена передача стоимости в модель данных.<br />
Полнотекстовый поиск, вероятно, можно рассматривать как своего рода модель данных, которая часто используется вместе с базами данных. Информационный поиск — это обширная специализированная тема, которую мы не будем подробно рассматривать в этой книге, но мы коснёмся поисковых индексов и векторного поиска в разделе “Полнотекстовый поиск”.</p>
<p>На этом нам пока придётся остановиться. В следующей главе мы обсудим некоторые компромиссы, которые вступают в игру при реализации описанных в этой главе моделей данных.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-3-data-models-and-query-languages/">Глава 3. Модели данных и языки запросов</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-3-data-models-and-query-languages/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 2. Определение нефункциональных требований</title>
		<link>https://datatalks.ru/chapter-2-defining-nonfunctional-requirements/</link>
					<comments>https://datatalks.ru/chapter-2-defining-nonfunctional-requirements/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 27 Jul 2025 11:09:33 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1943</guid>

					<description><![CDATA[<p>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта Глава 2. Определение нефункциональных требований Интернет был сделан настолько хорошо, что большинство людей воспринимают его как природный ресурс вроде Тихого океана, а не как нечто, созданное человеком. Когда в последний раз технология такого масштаба была настолько свободна от ошибок? — Алан Кэй, в интервью [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-2-defining-nonfunctional-requirements/">Глава 2. Определение нефункциональных требований</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта</em></p>
<h1>Глава 2. Определение нефункциональных требований</h1>
<blockquote><p>Интернет был сделан настолько хорошо, что большинство людей воспринимают его как природный ресурс вроде Тихого океана, а не как нечто, созданное человеком. Когда в последний раз технология такого масштаба была настолько свободна от ошибок?</p>
<p>— Алан Кэй, в интервью журналу Dr Dobb’s Journal (2012)</p></blockquote>
<hr />
<p>Если вы разрабатываете приложение, вами будет двигать список требований. На первом месте в этом списке, скорее всего, стоит функциональность, которую должно предоставлять приложение: какие экраны и кнопки необходимы и что должно происходить при выполнении каждой операции для достижения цели вашего программного обеспечения. Это ваши функциональные требования.</p>
<p>Кроме того, у вас, вероятно, есть и некоторые нефункциональные требования: например, приложение должно быть быстрым, надёжным, безопасным, соответствующим юридическим нормам и простым в сопровождении. Эти требования могут не быть явно прописаны, поскольку могут казаться само собой разумеющимися, но они так же важны, как и функциональность приложения: приложение, которое невыносимо медленное или ненадёжное, может считаться практически несуществующим.</p>
<p>Многие нефункциональные требования, такие как безопасность, выходят за рамки этой книги. Но есть несколько нефункциональных требований, которые мы всё же рассмотрим, и эта глава поможет вам сформулировать их для ваших собственных систем:</p>
<ul>
<li>Как определить и измерить производительность системы (см. «Описание производительности»);</li>
<li>Что означает надёжность сервиса — а именно, его способность продолжать правильно работать, даже когда что-то идёт не так (см. «Надёжность и устойчивость к отказам»);</li>
<li>Обеспечение масштабируемости системы за счёт эффективных способов увеличения вычислительных ресурсов при росте нагрузки (см. «Масштабируемость»);</li>
<li>Упрощение долгосрочного сопровождения системы (см. «Сопровождаемость»).</li>
</ul>
<p>Терминология, представленная в этой главе, также будет полезна в следующих главах, когда мы перейдём к подробному рассмотрению того, как реализуются системы, работающие с большими объёмами данных. Однако абстрактные определения могут быть довольно сухими; чтобы сделать идеи более наглядными, начнём главу с примера — рассмотрим, как может работать социальная сеть, что даст практические примеры производительности и масштабируемости.</p>
<h1>Case Study: Social Network Home Timelines</h1>
<p>Представьте, что вам поручили реализовать социальную сеть по типу X (ранее Twitter), в которой пользователи могут публиковать сообщения и подписываться друг на друга. Это будет сильное упрощение того, как подобный сервис работает на самом деле, но оно поможет продемонстрировать некоторые вопросы, возникающие в системах большого масштаба.</p>
<p>Предположим, что пользователи публикуют 500 миллионов сообщений в день, или в среднем 5 700 сообщений в секунду. Время от времени скорость может подскакивать до 150 000 сообщений в секунду. Также предположим, что в среднем один пользователь подписан на 200 человек и имеет 200 подписчиков (хотя разброс очень большой: у большинства людей всего несколько подписчиков, а у некоторых знаменитостей, таких как Барак Обама, их более 100 миллионов).</p>
<h2>Представление пользователей, сообщений и подписок</h2>
<p>Представим, что мы храним все данные в реляционной базе данных, как показано на рисунке 2-1. У нас есть одна таблица для пользователей, одна таблица для сообщений и одна таблица для подписок между пользователями.</p>
<p><strong>Рисунок 2-1. Простая реляционная схема социальной сети, в которой пользователи могут подписываться друг на друга</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_1.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1963" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_1.jpeg" alt="" width="1745" height="529" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_1.jpeg 1745w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-300x91.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-1024x310.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-768x233.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-1536x466.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-450x136.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-780x236.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_1-1600x485.jpeg 1600w" sizes="(max-width: 1745px) 100vw, 1745px" /></a></p>
<p>Предположим, что основной операцией чтения, которую должна поддерживать наша социальная сеть, является домашняя лента — отображение недавних сообщений от людей, на которых вы подписаны (для простоты мы будем игнорировать рекламу, рекомендации и другие расширения). Мы могли бы написать следующий SQL-запрос, чтобы получить домашнюю ленту для конкретного пользователя:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT posts.*, users.* 
FROM posts
  JOIN follows ON posts.sender_id = follows.followee_id
  JOIN users   ON posts.sender_id = users.id
WHERE follows.follower_id = current_user
ORDER BY posts.timestamp DESC
LIMIT 1000</pre><p>Чтобы выполнить этот запрос, база данных использует таблицу follows, чтобы найти всех, на кого подписан current_user, затем находит недавние публикации этих пользователей и сортирует их по времени, чтобы получить 1000 самых последних публикаций от любых из подписанных пользователей.</p>
<p>Публикации предполагаются актуальными, поэтому предположим, что после того как кто-то публикует сообщение, его подписчики должны увидеть его в течение 5 секунд. Один из способов добиться этого — клиент пользователя будет повторять вышеуказанный запрос каждые 5 секунд, пока пользователь в сети (это называется опросом, polling). Если предположить, что одновременно в сети находятся 10 миллионов пользователей, это означает выполнение запроса 2 миллиона раз в секунду. Даже если увеличить интервал опроса, это всё равно очень много.</p>
<p>Более того, сам запрос довольно затратен: если вы подписаны на 200 человек, необходимо получить список последних публикаций от каждого из этих 200 пользователей и объединить эти списки. 2 миллиона запросов к ленте в секунду означают, что база данных должна извлекать последние публикации от какого-то отправителя 400 миллионов раз в секунду — огромное число. И это в среднем. Некоторые пользователи подписаны на десятки тысяч аккаунтов; для них такой запрос крайне затратен и сложно сделать его быстрым.</p>
<h2>Материализация и обновление ленты</h2>
<p>Как можно улучшить ситуацию? Во-первых, вместо опроса будет лучше, если сервер будет активно отправлять новые публикации всем подписчикам, которые сейчас находятся в сети. Во-вторых, стоит предварительно вычислять результаты приведённого выше запроса, чтобы запрос к домашней ленте можно было обслужить из кэша.</p>
<p>Представим, что для каждого пользователя мы храним структуру данных, содержащую его домашнюю ленту, т. е. недавние публикации от людей, на которых он подписан. Каждый раз, когда пользователь делает публикацию, мы находим всех его подписчиков и вставляем эту публикацию в домашнюю ленту каждого подписчика — как будто доставляем сообщение в почтовый ящик. Теперь, когда пользователь входит в систему, мы просто отдаем ему заранее подготовленную ленту. Более того, чтобы получать уведомления о новых публикациях в ленте, клиент пользователя просто подписывается на поток публикаций, добавляемых в его домашнюю ленту.</p>
<p>Недостаток этого подхода в том, что теперь нам нужно выполнять больше работы каждый раз, когда пользователь публикует сообщение, потому что домашние ленты являются производными данными, которые нужно обновлять. Этот процесс показан на рисунке 2-2. Когда один исходный запрос приводит к нескольким последующим, мы используем термин фан-аут (fan-out), чтобы описать, во сколько раз увеличивается количество запросов.</p>
<p><strong>Рисунок 2-2. Фан-аут: доставка новых публикаций каждому подписчику пользователя, сделавшего публикацию</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_2.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1964" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_2.jpeg" alt="" width="1754" height="556" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_2.jpeg 1754w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-300x95.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-1024x325.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-768x243.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-1536x487.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-450x143.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-780x247.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_2-1600x507.jpeg 1600w" sizes="(max-width: 1754px) 100vw, 1754px" /></a></p>
<p>При скорости 5700 публикаций в секунду и среднем количестве подписчиков на одну публикацию — 200 (т. е. коэффициент фан-аута 200) — нам нужно выполнять чуть больше 1 миллиона записей в домашние ленты в секунду. Это много, но всё же значительно меньше, чем 400 миллионов обращений к публикациям отправителей в секунду, которые пришлось бы делать в противном случае.</p>
<p>Если скорость публикаций возрастает из-за какого-то события, нам не обязательно сразу выполнять доставку ленты — мы можем поставить задачи в очередь и принять тот факт, что публикации будут временно появляться с небольшой задержкой в лентах подписчиков. Даже во время всплесков нагрузки ленты остаются быстрыми в загрузке, так как мы просто отдаём их из кэша.</p>
<p>Этот процесс предварительного вычисления и обновления результатов запроса называется материализацией, а кэш ленты — пример материализованного представления. Недостаток материализации в том, что каждый раз, когда знаменитость публикует сообщение, нам нужно проделать большой объём работы, чтобы вставить это сообщение в домашние ленты каждого из миллионов её подписчиков.</p>
<p>Один из способов решить эту проблему — обрабатывать публикации знаменитостей отдельно от всех остальных: мы можем сэкономить ресурсы, не добавляя такие публикации в миллионы лент, а сохраняя их отдельно и объединяя с материализованной лентой при чтении. Несмотря на такие оптимизации, работа с аккаунтами знаменитостей в социальной сети может требовать значительных инфраструктурных затрат.</p>
<h1>Описание производительности</h1>
<p>Большинство обсуждений производительности программного обеспечения рассматривают два основных типа метрик:</p>
<ul>
<li><strong>Время отклика</strong><br />
Промежуток времени от момента, когда пользователь делает запрос, до момента получения ответа. Единицы измерения — секунды (или миллисекунды, или микросекунды).</li>
<li><strong>Пропускная способность</strong><br />
Количество запросов в секунду или объём данных в секунду, обрабатываемых системой. При заданном объёме ресурсов оборудования существует максимальная пропускная способность, которую можно выдержать. Единица измерения — «что-то в секунду».</li>
</ul>
<p>В примере социальной сети метрики «публикаций в секунду» и «записей в ленту в секунду» относятся к пропускной способности, а «время загрузки домашней ленты» или «время доставки публикации подписчикам» — это метрики времени отклика.</p>
<p>Между пропускной способностью и временем отклика часто существует связь; пример такой зависимости для онлайн-сервиса показан на рисунке 2-3. Сервис демонстрирует низкое время отклика при низкой нагрузке, но время отклика растёт по мере увеличения нагрузки. Это связано с очередями: когда запрос поступает на сильно загруженную систему, скорее всего, процессор уже занят обработкой другого запроса, и поэтому входящий запрос должен подождать, пока предыдущий завершится. По мере приближения пропускной способности к пределу аппаратных возможностей задержки из-за очередей растут резко.</p>
<p><strong>Рисунок 2-3. По мере приближения пропускной способности сервиса к его предельной нагрузке, время отклика резко возрастает из-за очередей</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_3.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1965" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_3.jpeg" alt="" width="1267" height="496" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_3.jpeg 1267w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_3-300x117.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_3-1024x401.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_3-768x301.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_3-450x176.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_3-780x305.jpeg 780w" sizes="(max-width: 1267px) 100vw, 1267px" /></a></p>
<h2>КОГДА ПЕРЕГРУЖЕННАЯ СИСТЕМА НЕ ВОССТАНАВЛИВАЕТСЯ</h2>
<p>Если система близка к перегрузке, с пропускной способностью, доведённой до предела, она иногда может войти в порочный круг, при котором становится менее эффективной и, следовательно, ещё более перегруженной. Например, если существует длинная очередь запросов, ожидающих обработки, время отклика может увеличиться настолько, что клиенты начинают прерывать ожидание (таймаут) и повторно отправлять запрос. Это приводит к ещё большему росту числа запросов и усугубляет проблему — так называемый «шторм повторов» (retry storm). Даже если нагрузка затем снижается, такая система может оставаться в перегруженном состоянии до перезагрузки или другого способа сброса. Это явление называется метастабильным сбоем (metastable failure) и может вызывать серьёзные сбои в производственных системах.</p>
<p>Чтобы избежать перегрузки службы из-за повторных запросов, можно увеличить и рандомизировать интервал между повторными попытками на стороне клиента (экспоненциальная задержка повторов, exponential backoff) и временно прекратить отправку запросов в службу, которая недавно возвращала ошибки или не отвечала (автоматический предохранитель, circuit breaker или алгоритм токен-бакета, token bucket). Сервер также может сам обнаружить приближение к перегрузке и начать проактивно отклонять запросы (сброс нагрузки, load shedding), а также отправлять клиентам ответы с просьбой замедлить скорость запросов (обратное давление, backpressure). Выбор алгоритмов очередей и балансировки нагрузки также может сыграть роль.</p>
<p>С точки зрения метрик производительности, пользователи чаще всего заботятся именно о времени отклика, тогда как пропускная способность определяет необходимые вычислительные ресурсы (например, сколько серверов потребуется), а значит — стоимость обслуживания конкретной нагрузки. Если ожидается рост пропускной способности сверх возможностей текущего оборудования, нужно расширить ресурсы; система считается масштабируемой, если её максимальную пропускную способность можно существенно увеличить добавлением вычислительных ресурсов.</p>
<p>В этом разделе мы сосредоточимся в первую очередь на времени отклика, а к пропускной способности и масштабируемости вернёмся в разделе “Scalability”.</p>
<h2>Latency (Задержка) и Response Time (Время Отклика)</h2>
<p>«Задержка» (latency) и «время отклика» (response time) иногда используются как синонимы, но в этой книге термины применяются в конкретном смысле (показано на рисунке 2-4):</p>
<ul>
<li>Время отклика — это то, что видит клиент; включает все задержки, возникающие где-либо в системе.</li>
<li>Время обслуживания — это длительность, в течение которой служба активно обрабатывает запрос пользователя.</li>
</ul>
<p>Задержки в очереди могут возникать в нескольких точках: например, после получения запроса, он может ждать доступности ЦП перед обработкой; ответ может быть задержан в буфере перед отправкой по сети, если другие процессы на той же машине активно передают данные через сетевой интерфейс.</p>
<p>Задержка — обобщённый термин для времени, в течение которого запрос не обрабатывается активно, т. е. находится в латентном состоянии. В частности, сетевая задержка или сетевое время задержки означает время, за которое запрос и ответ проходят по сети.</p>
<p><strong>Рисунок 2-4. Время отклика, время обслуживания, сетевая задержка и задержка в очереди</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_4.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1966" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_4.jpeg" alt="" width="1438" height="495" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_4.jpeg 1438w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_4-300x103.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_4-1024x352.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_4-768x264.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_4-450x155.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_4-780x268.jpeg 780w" sizes="(max-width: 1438px) 100vw, 1438px" /></a></p>
<p>На рисунке 2-4 время течёт слева направо, каждое взаимодействующее звено показано в виде горизонтальной линии, а сообщение запроса или ответа изображено в виде толстой диагональной стрелки от одного узла к другому. С таким стилем диаграмм вы будете часто сталкиваться в этой книге.</p>
<p>Время отклика может значительно варьироваться от одного запроса к другому, даже если вы снова и снова отправляете один и тот же запрос. Случайные задержки могут вызываться множеством факторов: переключением контекста на фоновый процесс, потерей сетевого пакета и повторной передачей по TCP, паузой на сборку мусора, ошибкой страницы, приводящей к чтению с диска, механическими вибрациями в серверной стойке и другими причинами.</p>
<p>Задержки в очереди часто составляют большую часть вариации во времени отклика. Поскольку сервер может обрабатывать только ограниченное количество запросов параллельно (например, по числу ядер процессора), достаточно лишь нескольких медленных запросов, чтобы задержать последующие — это явление называется блокировкой начала очереди (head-of-line blocking). Даже если последующие запросы имеют быстрое время обслуживания, клиент увидит общее медленное время отклика из-за ожидания завершения предыдущего запроса. Задержка в очереди не является частью времени обслуживания, поэтому важно измерять время отклика на стороне клиента.</p>
<h2>Среднее, Медиана и Процентили</h2>
<p>Поскольку время отклика варьируется от запроса к запросу, его нужно рассматривать не как одно число, а как распределение значений, которое можно измерять. На рисунке 2-5 каждая серая полоса представляет запрос к службе, а её высота показывает, сколько времени занял этот запрос. Большинство запросов достаточно быстрые, но иногда встречаются выбросы, длящиеся значительно дольше. Вариации сетевой задержки также называются джиттером (jitter).</p>
<p><strong>Рисунок 2-5. Иллюстрация среднего и процентилей: время отклика для выборки из 100 запросов к сервису</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_5.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1968" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_5.jpeg" alt="" width="1466" height="439" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_5.jpeg 1466w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_5-300x90.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_5-1024x307.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_5-768x230.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_5-450x135.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_5-780x234.jpeg 780w" sizes="(max-width: 1466px) 100vw, 1466px" /></a></p>
<p>Часто сообщается среднее время отклика сервиса (технически — арифметическое среднее: то есть сумма всех времен отклика, делённая на количество запросов). Среднее время отклика полезно для оценки пределов пропускной способности. Однако среднее — не очень хорошая метрика, если вы хотите узнать «типичное» время отклика, потому что оно не показывает, сколько пользователей фактически испытали ту или иную задержку.</p>
<p>Обычно лучше использовать процентили. Если взять список всех времен отклика и отсортировать его от самого быстрого до самого медленного, то медиана будет находиться посередине: например, если медианное время отклика составляет 200 мс, это значит, что половина запросов возвращается быстрее 200 мс, а половина — медленнее. Это делает медиану хорошей метрикой, если вы хотите знать, сколько времени пользователи обычно ждут. Медиана также называется 50-м процентилем и иногда обозначается как p50.</p>
<p>Чтобы понять, насколько плохи выбросы, можно посмотреть на более высокие процентили: 95-й, 99-й и 99.9-й процентили являются распространёнными (обозначаются соответственно как p95, p99 и p999). Это пороговые значения времени отклика, при которых 95%, 99% или 99.9% запросов выполняются быстрее соответствующего порога. Например, если 95-й процентиль времени отклика составляет 1.5 секунды, это означает, что 95 из 100 запросов выполняются быстрее 1.5 секунды, а 5 из 100 — за 1.5 секунды или дольше. Это показано на рисунке 2-5.</p>
<p>Высокие процентили времени отклика, также известные как латентность хвоста (tail latencies), важны, потому что они напрямую влияют на пользовательский опыт. Например, Amazon определяет требования ко времени отклика внутренних сервисов по 99.9-му процентилю, даже если это затрагивает лишь 1 из 1000 запросов. Это потому, что пользователи с самыми медленными запросами часто имеют наибольшее количество данных в своих аккаунтах из-за большого числа покупок — другими словами, это самые ценные клиенты. Важно удерживать этих клиентов, обеспечивая для них быструю работу сайта.</p>
<p>С другой стороны, оптимизация 99.99-го процентиля (самый медленный 1 из 10 000 запросов) была признана слишком дорогой и не дающей достаточной выгоды для целей Amazon. Снижение времени отклика на очень высоких процентилях трудно, так как они легко зависят от случайных факторов вне вашего контроля, а отдача от улучшений снижается.</p>
<h2>ВЛИЯНИЕ ВРЕМЕНИ ОТКЛИКА НА ПОЛЬЗОВАТЕЛЯ</h2>
<p>Интуитивно кажется очевидным, что быстрая служба лучше для пользователей, чем медленная. Однако удивительно сложно найти достоверные данные, чтобы количественно оценить влияние задержек на поведение пользователей.</p>
<p>Некоторые часто цитируемые статистики ненадёжны. В 2006 году Google сообщала, что замедление выдачи результатов поиска с 400 мс до 900 мс было связано с снижением трафика и доходов на 20%. Однако другое исследование Google в 2009 году сообщало, что увеличение задержки на 400 мс привело лишь к 0.6% снижению количества поисковых запросов в день, а в том же году Bing обнаружила, что увеличение времени загрузки на две секунды снизило доход от рекламы на 4.3%. Более свежие данные от этих компаний, по-видимому, не публиковались.</p>
<p>Более недавнее исследование от Akamai утверждает, что увеличение времени отклика на 100 мс снижает коэффициент конверсии для сайтов электронной коммерции на до 7%; однако при более внимательном рассмотрении то же исследование показывает, что очень быстрое время загрузки страниц также коррелирует с низкими коэффициентами конверсии! Это кажущееся противоречие объясняется тем, что страницы, загружающиеся быстрее всего — это часто страницы без полезного контента (например, страницы ошибки 404). Однако, поскольку исследование не пытается отделить влияние содержимого страницы от времени загрузки, его результаты, вероятно, не имеют особого значения.</p>
<p>Исследование Yahoo сравнивало показатели кликов (CTR) по результатам поиска, загружающимся быстро и медленно, при прочих равных условиях качества результатов. Оно показывает на 20–30% больше кликов по быстрым результатам, если разница во времени между быстрым и медленным ответом составляет 1.25 секунды или больше.</p>
<h2>ИСПОЛЬЗОВАНИЕ МЕТРИК ВРЕМЕНИ ОТКЛИКА</h2>
<p>Высокие процентили особенно важны в бэкенд-сервисах, к которым делается несколько обращений в рамках одного пользовательского запроса. Даже если вызовы выполняются параллельно, финальный ответ пользователю всё равно должен ждать самого медленного из этих параллельных вызовов. Достаточно одного медленного вызова, чтобы весь пользовательский запрос оказался медленным, как показано на рисунке 2-6. Даже если только небольшой процент бэкенд-запросов медленные, вероятность получить медленный вызов увеличивается, если для одного пользовательского запроса требуется много обращений к бэкенду. В результате большая доля пользовательских запросов становится медленной — это явление называется усилением латентности хвоста (tail latency amplification).</p>
<p><strong>Рисунок 2-6. Когда для обработки одного запроса требуется несколько бэкенд-вызовов, достаточно одного медленного вызова, чтобы замедлить весь пользовательский запрос</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_2_6.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1969" src="https://datatalks.ru/wp-content/uploads/2025/07/image_2_6.jpeg" alt="" width="1416" height="640" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_2_6.jpeg 1416w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_6-300x136.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_6-1024x463.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_6-768x347.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_6-450x203.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_2_6-780x353.jpeg 780w" sizes="(max-width: 1416px) 100vw, 1416px" /></a></p>
<p>Процентили часто используются в <strong>целевых показателях обслуживания (SLO)</strong> и соглашениях об <strong>уровне обслуживания (SLA)</strong> как способ определить ожидаемую производительность и доступность сервиса. Например, <strong>SLO</strong> может устанавливать цель: медианное время отклика менее 200 мс, 99-й процентиль — менее 1 секунды, и не менее 99.9% корректных запросов должны завершаться без ошибки. <strong>SLA</strong> — это контракт, определяющий, что произойдёт, если <strong>SLO</strong> не будет выполнен (например, клиент может получить компенсацию). По крайней мере, это базовая идея; на практике определить хорошие метрики доступности для <strong>SLO</strong> и <strong>SLA</strong> — задача непростая.</p>
<h2>ВЫЧИСЛЕНИЕ ПРОЦЕНТИЛЕЙ</h2>
<p>Если вы хотите добавить процентили времени отклика на панель мониторинга своих сервисов, необходимо эффективно вычислять их на постоянной основе. Например, вы можете отслеживать скользящее окно с временами отклика за последние 10 минут. Каждую минуту вы рассчитываете медиану и различные процентили по значениям в этом окне и отображаете эти метрики на графике.</p>
<p>Самый простой способ реализации — хранить список времен отклика всех запросов в пределах временного окна и сортировать этот список каждую минуту. Если такой подход слишком неэффективен, существуют алгоритмы, способные вычислять хорошее приближение процентилей с минимальной нагрузкой на CPU и память.</p>
<p>Среди open-source библиотек для оценки процентилей: <code>HdrHistogram</code>, <code>t-digest</code>, <code>OpenHistogram</code> и <code>DDSketch</code>.</p>
<p>Имейте в виду, что усреднение процентилей (например, для уменьшения временного разрешения или объединения данных с нескольких машин) не имеет математического смысла — правильный способ агрегирования данных о времени отклика — это суммирование гистограмм.</p>
<h1>Надёжность и отказоустойчивость</h1>
<p>У всех есть интуитивное представление о том, что значит надёжная или ненадёжная система. Для программного обеспечения типичные ожидания включают:</p>
<ul>
<li>Приложение выполняет функции, которых ожидает пользователь.</li>
<li>Оно способно выдерживать ошибки пользователя или использование в неожиданных сценариях.</li>
<li>Его производительности достаточно для предполагаемого случая использования при ожидаемой нагрузке и объёме данных.</li>
<li>Система предотвращает несанкционированный доступ и злоупотребления.</li>
</ul>
<p>Если всё это вместе означает «работает правильно», то надёжность можно понимать как «продолжает работать правильно, даже когда что-то идёт не так». Чтобы точнее говорить о том, что может пойти не так, различим сбои и отказы:</p>
<ul>
<li><strong>Сбой (Fault)</strong><br />
Сбой — это когда какая-то часть системы перестаёт работать правильно: например, выходит из строя жёсткий диск, или падает одна из машин, или происходит сбой внешнего сервиса, от которого зависит система.</li>
<li><strong>Отказ (Failure)</strong><br />
Отказ — это когда система в целом перестаёт предоставлять требуемую услугу пользователю; другими словами, когда она не выполняет заданные целевые показатели обслуживания (SLO).</li>
</ul>
<p>Различие между сбоем и отказом может сбивать с толку, так как это одно и то же, но на разных уровнях. Например, если выходит из строя жёсткий диск, мы говорим, что диск отказал. Если система состоит только из одного диска, то она перестаёт предоставлять услугу. Но если система содержит множество дисков, то отказ одного диска — это всего лишь сбой с точки зрения большей системы, и она может его выдержать, если данные продублированы на других дисках.</p>
<h2>Отказоустойчивость (Fault Tolerance)</h2>
<p>Система называется отказоустойчивой, если она продолжает предоставлять требуемую услугу пользователю несмотря на определённые сбои. Если система не может выдержать отказ какой-либо своей части, такая часть называется <strong>единой точкой отказа (SPOF)</strong>, потому что сбой в ней приводит к отказу всей системы.</p>
<p>Например, в кейсе социальной сети возможным сбоем является отказ машины, участвующей в обновлении материализованных лент новостей (fan-out). Чтобы сделать этот процесс отказоустойчивым, нужно, чтобы другая машина могла взять на себя задачу без пропуска сообщений и без их дублирования. (Эта идея называется семантикой <strong>«ровно один раз» — exactly-once semantics</strong>).</p>
<p>Отказоустойчивость всегда ограничена определённым числом определённых типов сбоев. Например, система может выдержать максимум два одновременных отказа дисков, или выход из строя одной из трёх нод. Было бы бессмысленно пытаться выдержать любое количество сбоев: если выйдут из строя все узлы, уже ничего нельзя сделать. Если вся планета Земля (и все её серверы) будет поглощена чёрной дырой, то для устойчивости к такому сбою потребовался бы хостинг в космосе — удачи в согласовании такого бюджета.</p>
<p>Парадоксально, но в таких отказоустойчивых системах иногда имеет смысл умышленно вызывать сбои, например, случайным образом убивая отдельные процессы без предупреждения. Это называется <strong>внедрение сбоев (fault injection)</strong>. Многие критические ошибки возникают из-за плохой обработки ошибок; умышленно провоцируя сбои, вы гарантируете, что механизмы отказоустойчивости регулярно используются и тестируются, что повышает уверенность в их корректной работе при реальных сбоях. <strong>Chaos engineering</strong> — это дисциплина, цель которой — повысить уверенность в отказоустойчивости системы через эксперименты с преднамеренными сбоями.</p>
<p>Хотя мы обычно предпочитаем устойчивость к сбоям предотвращению сбоев, бывают случаи, когда предотвращение лучше, потому что «лечения» не существует. Например, в случае с безопасностью: если злоумышленник получил доступ к конфиденциальным данным, это событие нельзя отменить. Однако в этой книге в основном рассматриваются такие сбои, с которыми можно справиться, как описано далее.</p>
<h2>Аппаратные и программные сбои (Hardware and Software Faults)</h2>
<p>Когда мы думаем о причинах отказов системы, в первую очередь на ум приходят аппаратные сбои:</p>
<p>Примерно 2–5% магнитных жёстких дисков выходят из строя в год; в кластере из 10 000 дисков можно ожидать в среднем один отказ в день. Новые данные показывают, что диски становятся надёжнее, но уровень отказов остаётся значительным.</p>
<p>Примерно 0.5–1% SSD-дисков выходят из строя в год. Небольшие ошибки на уровне битов автоматически корректируются, но некорректируемые ошибки случаются примерно раз в год на диск, даже если диск довольно новый (т.е. мало использовался); у SSD частота таких ошибок выше, чем у магнитных дисков.</p>
<p>Другие аппаратные компоненты, такие как блоки питания, RAID-контроллеры и модули памяти, тоже выходят из строя, хотя и реже, чем диски.</p>
<p>Примерно одна из 1000 машин имеет процессорное ядро, которое иногда возвращает неверные результаты вычислений, вероятно из-за производственных дефектов. В некоторых случаях это вызывает сбой, но иногда программа просто выдаёт неправильный результат.</p>
<p>Данные в ОЗУ также могут быть повреждены — либо из-за случайных явлений (например, космических лучей), либо из-за физических дефектов. Даже при использовании памяти с ECC (кодами коррекции ошибок), более 1% машин сталкиваются с некорректируемой ошибкой в течение года, что обычно приводит к сбою машины и необходимости замены модуля памяти. Более того, некоторые патологические паттерны доступа к памяти могут с высокой вероятностью переворачивать биты.</p>
<p>Целый датацентр может стать недоступен (например, из-за отключения электроэнергии или сетевой ошибки) или даже быть полностью уничтожен (например, пожаром или наводнением). Хотя такие крупные инциденты случаются редко, их последствия могут быть катастрофическими, если сервис не выдерживает потерю датацентра.</p>
<p>Эти события достаточно редки, и о них часто не стоит беспокоиться при работе с небольшой системой, если можно легко заменить сломавшееся оборудование. Однако в системах крупного масштаба аппаратные сбои происходят настолько часто, что становятся обычной частью работы системы.</p>
<p>Устойчивость к аппаратным сбоям достигается с помощью избыточности.</p>
<p>Наша первая реакция на ненадёжное оборудование — добавить избыточность в отдельные аппаратные компоненты, чтобы снизить уровень отказов системы. Диски могут быть объединены в конфигурации RAID (распределение данных по нескольким дискам в одной машине, чтобы отказ одного диска не привёл к потере данных), серверы могут иметь два блока питания и горячую замену процессоров, а датацентры могут использовать аккумуляторы и дизельные генераторы для резервного питания. Такая избыточность может поддерживать работу машины без перебоев в течение многих лет.</p>
<p>Избыточность наиболее эффективна, когда отказы компонентов независимы, то есть возникновение одного сбоя не влияет на вероятность другого. Однако опыт показывает, что существуют значимые корреляции между отказами компонентов; например, недоступность целой стойки или даже всего датацентра всё ещё происходит чаще, чем хотелось бы.</p>
<p>Избыточность оборудования увеличивает время бесперебойной работы одной машины, однако, как обсуждалось в разделе «Распределённые против одноузловых систем», существуют преимущества использования распределённых систем, например, возможность пережить полную недоступность одного датацентра. По этой причине облачные системы меньше полагаются на надёжность отдельных машин, а вместо этого стремятся обеспечить высокую доступность сервисов, выдерживая отказ узлов на уровне программного обеспечения. Облачные провайдеры используют зоны доступности, чтобы указать, какие ресурсы физически находятся рядом друг с другом; ресурсы, размещённые в одном месте, с большей вероятностью выйдут из строя одновременно, чем географически разделённые.</p>
<p>Методы обеспечения отказоустойчивости, рассматриваемые в этой книге, предназначены для выдерживания потери целых машин, стоек или зон доступности. Обычно они работают за счёт того, что машина в одном датацентре может взять на себя задачи машины из другого датацентра, если та выйдет из строя или станет недоступной.</p>
<p>Системы, которые могут выдерживать потерю целых машин, имеют операционные преимущества: система из одного сервера требует планового простоя, если нужно перезагрузить машину (например, чтобы установить обновления безопасности ОС), тогда как отказоустойчивая система из нескольких узлов может быть обновлена поочерёдной перезагрузкой узлов без прерывания сервиса для пользователей. Это называется <strong>пошаговое обновление (rolling upgrade)</strong>.</p>
<h2>Программные сбои</h2>
<p>Хотя аппаратные отказы могут быть слабо коррелированы, они в основном независимы: например, если один диск вышел из строя, скорее всего, остальные диски в той же машине ещё будут работать. Программные сбои, напротив, часто высоко коррелированы, поскольку на многих узлах обычно работает одно и то же программное обеспечение и, соответственно, одни и те же ошибки. Такие сбои труднее предсказать, и они обычно чаще приводят к отказам систем, чем некоррелированные аппаратные сбои.</p>
<p>Например:</p>
<p>Ошибка в ПО, вызывающая отказ всех узлов одновременно при определённых условиях. Так, 30 июня 2012 года добавление високосной секунды вызвало зависание многих Java-приложений из-за ошибки в ядре Linux, что привело к сбоям интернет-сервисов. Из-за ошибки в прошивке все SSD определённой модели внезапно выходили из строя ровно через 32 768 часов работы (менее чем через 4 года), делая данные на них невосстановимыми.</p>
<p>Неконтролируемый процесс, потребляющий ограниченный общий ресурс, такой как процессорное время, память, дисковое пространство, сетевую пропускную способность или потоки. Например, процесс, потребляющий слишком много памяти при обработке большого запроса, может быть завершён ОС. Ошибка в клиентской библиотеке может привести к гораздо большему объёму запросов, чем ожидалось.</p>
<p>Зависимый сервис замедляется, перестаёт отвечать или начинает возвращать повреждённые ответы.</p>
<p>Взаимодействие между различными системами вызывает побочное поведение, которое не проявлялось при их тестировании по отдельности.</p>
<p>Каскадные отказы, когда проблема в одном компоненте перегружает другой, что вызывает его замедление или сбой, и так далее.</p>
<p>Ошибки, вызывающие такие программные сбои, часто долго остаются незаметными, пока не сработает редкое сочетание условий. В этих условиях становится ясно, что программа делает какие-то предположения об окружении, которые обычно верны, но в какой-то момент перестают быть таковыми.</p>
<p>Быстрого решения проблемы систематических программных ошибок не существует. Есть множество мелких полезных подходов: вдумчивое рассмотрение предположений и взаимодействий в системе; тщательное тестирование; изоляция процессов; разрешение процессам аварийно завершаться и перезапускаться; избежание «бурь повторных попыток» (см. «Когда перегруженная система не восстанавливается»); измерение, мониторинг и анализ поведения системы в продакшене.</p>
<h2>Люди и надёжность</h2>
<p>Программные системы проектируют и создают люди, и операторы, поддерживающие их работу, тоже люди. В отличие от машин, люди не просто следуют правилам; их сила — в креативности и адаптивности. Но именно эта особенность также ведёт к непредсказуемости и иногда к ошибкам, приводящим к сбоям, несмотря на лучшие намерения. Например, в одном исследовании крупных интернет-сервисов было установлено, что изменения конфигурации, внесённые операторами, были основной причиной сбоев, тогда как аппаратные отказы (серверы или сеть) сыграли роль лишь в 10–25% инцидентов.</p>
<p>Возникает соблазн назвать такие случаи «человеческой ошибкой» и надеяться, что их можно устранить с помощью более строгих процедур и контроля. Однако обвинение людей в ошибках — контрпродуктивно. То, что мы называем «человеческой ошибкой» — это не причина инцидента, а симптом проблемы в социотехнической системе, в которой люди стараются делать свою работу. Часто сложные системы обладают побочным поведением, при котором неожиданные взаимодействия компонентов тоже приводят к отказам.</p>
<p>Существуют различные технические меры, помогающие снизить влияние человеческих ошибок, включая: тщательное тестирование (как ручные тесты, так и property-based тестирование на большом количестве случайных входных данных), механизмы отката конфигураций, постепенное развёртывание нового кода, подробный мониторинг, инструменты наблюдаемости для диагностики проблем в продакшене (см. «Проблемы распределённых систем»), и интерфейсы, которые побуждают делать «правильное» и мешают сделать «неправильное».</p>
<p>Однако всё это требует времени и денег, и в реальности бизнеса организации часто отдают приоритет деятельности, приносящей доход, а не мероприятиям по повышению устойчивости к ошибкам. При выборе между новыми функциями и тестированием многие организации, что вполне объяснимо, выбирают функции. В такой ситуации, когда неизбежно происходит предотвратимая ошибка, бессмысленно винить человека, допустившего её — проблема заключается в приоритетах организации.</p>
<p>Всё чаще организации переходят к культуре анализа инцидентов без поиска виновных: после инцидента участников поощряют подробно рассказывать, что произошло, без страха наказания, так как это позволяет всей организации извлечь уроки и предотвратить повторение подобных проблем в будущем. Такой процесс может выявить необходимость изменить приоритеты бизнеса, инвестировать в недофинансированные участки, пересмотреть систему мотивации, или устранить другие системные проблемы, требующие внимания руководства.</p>
<p>В качестве общего принципа при разборе инцидентов следует остерегаться упрощённых объяснений. Утверждение «Боб должен был быть внимательнее при развёртывании» — бесполезно. Но и «Нам нужно переписать бэкенд на Haskell» — тоже. Вместо этого руководство должно использовать инцидент как возможность понять, как реально работает система глазами людей, которые с ней работают каждый день, и улучшить её на основе этих данных.</p>
<h2>НАСКОЛЬКО ВАЖНА НАДЁЖНОСТЬ?</h2>
<p>Надёжность важна не только для атомных электростанций и управления воздушным движением — даже самые обыденные приложения должны работать надёжно. Ошибки в бизнес-приложениях приводят к потере продуктивности (а также к юридическим рискам, если отчётность оказывается неверной), а сбои в работе сайтов электронной коммерции могут обернуться огромными потерями дохода и урона для репутации.</p>
<p>Во многих приложениях временный сбой на несколько минут или даже часов допустим, но потеря или повреждение данных навсегда — это катастрофа. Представьте себе родителя, хранящего все фотографии и видео своих детей в вашем фото-приложении. Как он себя почувствует, если база данных внезапно будет повреждена? Знает ли он, как восстановить её из резервной копии?<br />
Другой пример того, как ненадёжное программное обеспечение может навредить людям — это скандал с системой Horizon в британской почтовой службе. С 1999 по 2019 год сотни людей, управлявших отделениями почты в Великобритании, были осуждены за кражу или мошенничество, потому что бухгалтерское ПО показывало недостачу в их отчётах. Позже выяснилось, что многие из этих недостач были вызваны ошибками в программном обеспечении, и многие приговоры были пересмотрены. Причиной этого, вероятно самого крупного судебного заблуждения в истории Великобритании, стало то, что английское законодательство предполагает, что компьютеры работают правильно (и, следовательно, данные, предоставленные компьютером, являются надёжными), если нет доказательств обратного. Инженеры-программисты могут смеяться над идеей, что ПО может быть полностью без ошибок, но это слабое утешение для людей, которые были несправедливо осуждены, объявлены банкротами или даже покончили с собой из-за приговора, основанного на ненадёжной компьютерной системе.</p>
<p>Иногда мы осознанно жертвуем надёжностью, чтобы сократить затраты на разработку (например, при создании прототипа продукта для ещё не проверенного рынка) — но в таком случае нужно отчётливо осознавать, где мы упрощаем, и помнить о возможных последствиях.</p>
<h1>МАСШТАБИРУЕМОСТЬ</h1>
<p>Даже если система работает надёжно сегодня, это не значит, что она будет работать надёжно в будущем. Одна из частых причин ухудшения работы — рост нагрузки: возможно, система выросла с 10 000 до 100 000 одновременных пользователей или с 1 миллиона до 10 миллионов. Возможно, она обрабатывает гораздо большие объёмы данных, чем раньше.</p>
<p><strong>Масштабируемость</strong> — это термин, которым описывают способность системы справляться с возросшей нагрузкой. Иногда при обсуждении масштабируемости говорят что-то вроде: «Вы не Google и не Amazon. Перестаньте думать о масштабах и просто используйте реляционную базу данных». Применимо ли это к вам — зависит от типа приложения, которое вы разрабатываете.<br />
Если вы делаете новый продукт, у которого пока немного пользователей, например в стартапе, основная инженерная цель обычно — сохранять систему как можно более простой и гибкой, чтобы легко вносить изменения и адаптироваться к потребностям клиентов. В такой ситуации думать о гипотетических масштабах, которые могут понадобиться в будущем, вредно: в лучшем случае — это потраченные впустую усилия и преждевременная оптимизация, в худшем — это может зафиксировать вас в негибкой архитектуре и усложнить развитие приложения.</p>
<p>Причина в том, что масштабируемость — не одноосный ярлык: бессмысленно говорить «X масштабируется» или «Y не масштабируется». Вместо этого обсуждение масштабируемости предполагает вопросы вроде:</p>
<ul>
<li>«Если система вырастет определённым образом, какие есть варианты справиться с ростом?»</li>
<li>«Как можно добавить вычислительные ресурсы для обработки увеличенной нагрузки?»</li>
<li>«С учётом текущих темпов роста, когда мы упрёмся в пределы существующей архитектуры?»</li>
</ul>
<p>Если вам удастся сделать своё приложение популярным и, следовательно, сталкивающимся с растущей нагрузкой, вы узнаете, где находятся узкие места в производительности, и сможете определить, по каким осям нужно масштабироваться. Тогда и наступит время беспокоиться о методах масштабирования.</p>
<h2>ОПИСАНИЕ НАГРУЗКИ</h2>
<p>Сначала нужно кратко описать текущую нагрузку на систему; только после этого можно обсуждать вопросы роста (что произойдёт, если нагрузка удвоится?). Чаще всего измеряется пропускная способность: например, количество запросов в секунду к сервису, сколько гигабайт новых данных приходит в день или сколько оформляется покупок в час. Иногда важен пиковый показатель переменной величины, например, количество одновременно онлайн-пользователей в разделе «Кейс: домашняя лента в соцсети».</p>
<p>Часто на масштабируемость влияют и другие статистические характеристики нагрузки. Например, может потребоваться знать соотношение чтений и записей в базе данных, частоту попаданий в кэш или количество элементов данных на пользователя (например, число подписчиков в соцсети). Может быть важен средний случай, а может быть — крайние случаи, которые и создают основную нагрузку. Всё зависит от деталей конкретного приложения.</p>
<p>Описав нагрузку, можно исследовать, что произойдёт при её увеличении. Это можно рассматривать двумя способами:</p>
<ul>
<li>При увеличении нагрузки при неизменных ресурсах (CPU, память, пропускная способность сети и т. д.) — как изменяется производительность системы?</li>
<li>При увеличении нагрузки — насколько нужно увеличить ресурсы, чтобы сохранить прежнюю производительность?</li>
</ul>
<p>Обычно цель — сохранить производительность в рамках требований SLA (см. раздел “Использование метрик времени отклика”), одновременно минимизируя стоимость эксплуатации системы. Чем больше требуемые ресурсы, тем выше стоимость. Возможно, некоторые типы оборудования более экономичны, и эти параметры могут меняться со временем с появлением новых типов оборудования.</p>
<p>Если вы можете удвоить ресурсы, чтобы справиться с удвоенной нагрузкой, сохранив ту же производительность, это называется линейной масштабируемостью, и это считается хорошим показателем. Иногда даже удаётся справиться с удвоенной нагрузкой, добавив меньше, чем вдвое ресурсов — за счёт эффекта масштаба или более равномерного распределения пиковой нагрузки. Но чаще затраты растут быстрее, чем линейно, и причин для такой неэффективности может быть много. Например, при большом объёме данных обработка одного запроса на запись может требовать больше работы, чем при небольшом объёме данных, даже если сам запрос по размеру одинаковый.</p>
<h2>Shared-Memory, Shared-Disk, and Shared-Nothing Architecture</h2>
<p>Самый простой способ увеличить аппаратные ресурсы сервиса — перенести его на более мощную машину. <strong>Отдельные ядра CPU</strong> уже не становятся значительно быстрее, но можно купить машину (или арендовать облачный экземпляр) с большим количеством ядер, большим объёмом оперативной памяти и дискового пространства. Такой подход называется вертикальным масштабированием или масштабированием «вверх».</p>
<p><strong>Параллелизм на одной машине</strong> можно получить с помощью нескольких процессов или потоков. Все потоки, принадлежащие одному процессу, имеют доступ к одной и той же оперативной памяти, и поэтому этот подход также называют архитектурой с общей памятью (shared-memory). Проблема подхода с общей памятью в том, что его стоимость растёт быстрее, чем линейно: высокопроизводительная машина с вдвое большими ресурсами обычно стоит значительно больше, чем вдвое дороже. И из-за узких мест такая машина зачастую справляется с нагрузкой, меньшей, чем в два раза больше.</p>
<p>Другой подход — это <strong>архитектура с общим диском (shared-disk)</strong>, в которой используется несколько машин с независимыми CPU и RAM, но данные хранятся на массиве дисков, который общий для всех машин и к которому они подключаются через быструю сеть: Network-Attached Storage (NAS) или Storage Area Network (SAN). Такая архитектура традиционно применялась для внутренних хранилищ данных (data warehousing), но конфликты доступа и издержки блокировок ограничивают масштабируемость подхода с общим диском.</p>
<p>В противоположность этому, <strong>архитектура без общего ресурса (shared-nothing)</strong> (также называемая горизонтальным масштабированием или масштабированием «вширь») завоевала большую популярность. В этом подходе используется распределённая система с несколькими узлами, каждый из которых имеет собственные CPU, RAM и диски. Любая координация между узлами осуществляется на уровне программного обеспечения через обычную сеть.</p>
<p>Преимущества архитектуры без общего ресурса в том, что она потенциально может масштабироваться линейно, использовать любое оборудование с наилучшим соотношением цена/производительность (особенно в облаке), проще адаптировать ресурсы под рост или снижение нагрузки, и она может достичь большей отказоустойчивости за счёт распределения между несколькими дата-центрами и регионами. Недостатки — необходимость явного шардирования и вся сложность, связанная с распределёнными системами.</p>
<p>Некоторые облачные базы данных используют раздельные сервисы для хранения данных и выполнения транзакций (см. «Разделение хранения и вычислений»), при этом несколько вычислительных узлов получают доступ к одному и тому же сервису хранения. Эта модель в чём-то похожа на архитектуру с общим диском, но она избегает проблем масштабируемости старых систем: вместо предоставления абстракции файловой системы (NAS) или блочного устройства (SAN), сервис хранения предлагает специализированный API, разработанный под конкретные нужды базы данных.</p>
<h2>Принципы масштабируемости</h2>
<p>Архитектура систем, работающих в крупных масштабах, как правило, сильно зависит от конкретного приложения — не существует универсальной архитектуры, масштабируемой «на все случаи жизни» (неформально известной как «волшебный соус масштабирования»). Например, система, рассчитанная на 100 000 запросов в секунду, каждый объёмом 1 КБ, будет сильно отличаться от системы, рассчитанной на 3 запроса в минуту, каждый по 2 ГБ — несмотря на то, что у обеих одинаковая пропускная способность по данным (100 МБ/сек).</p>
<p>Более того, архитектура, подходящая под одну степень нагрузки, вряд ли справится с нагрузкой в 10 раз выше. Если вы работаете над быстрорастущим сервисом, скорее всего вам придётся переосмысливать архитектуру при каждом десятикратном росте нагрузки. Поскольку потребности приложения, скорее всего, будут меняться, обычно нет смысла планировать масштабирование дальше, чем на один порядок величины вперёд.</p>
<p>Хороший общий принцип масштабируемости — разделять систему на более мелкие компоненты, которые могут работать относительно независимо друг от друга. Это основной принцип, лежащий в основе микросервисов, шардирования, потоковой обработки и архитектуры без общего ресурса. Однако сложность в том, чтобы определить, что должно быть объединено, а что — разделено.</p>
<p>Другой полезный принцип — не усложнять систему без необходимости. Если однопроцессная база данных справляется с задачей — скорее всего, она предпочтительнее сложной распределённой системы. Автоматически масштабируемые системы (автоматически добавляющие или удаляющие ресурсы в ответ на изменения нагрузки) — это здорово, но если ваша нагрузка достаточно предсказуема, система с ручным масштабированием может преподносить меньше операционных сюрпризов. Система с пятью сервисами проще, чем система с пятьюдесятью. Хорошие архитектуры обычно представляют собой прагматичную комбинацию различных подходов.</p>
<h1>Сопровождаемость (Maintainability)</h1>
<p>Программное обеспечение не изнашивается и не страдает от усталости материалов, поэтому оно не ломается так же, как механические объекты. Но требования к приложению часто меняются, среда, в которой работает ПО, также меняется (например, его зависимости и базовая платформа), и в нём есть ошибки, которые нужно исправлять.<br />
Широко признано, что основная часть затрат на программное обеспечение связана не с его первоначальной разработкой, а с последующим обслуживанием — исправлением ошибок, поддержанием работы систем, расследованием сбоев, адаптацией к новым платформам, модификацией под новые случаи использования, устранением технического долга и добавлением новых функций.</p>
<p>Однако сопровождение также сложно. Если система успешно работает долгое время, вполне возможно, что она использует устаревшие технологии, которые сегодня мало кто понимает (например, мейнфреймы и код на COBOL); институциональные знания о том, как и почему система была спроектирована определённым образом, могли быть утрачены, когда сотрудники покинули организацию; возможно, потребуется исправлять ошибки других людей. Более того, компьютерная система часто тесно связана с человеческой организацией, которую она обслуживает, а это значит, что сопровождение таких унаследованных систем — это не только техническая, но и социальная проблема.</p>
<p>Каждая система, которую мы создаём сегодня, однажды станет унаследованной (legacy), если она достаточно ценна, чтобы просуществовать долго. Чтобы свести к минимуму трудности будущих поколений, которым придётся сопровождать наше программное обеспечение, мы должны проектировать его с учётом вопросов сопровождения. Хотя мы не всегда можем предсказать, какие решения создадут проблемы в будущем, в этой книге мы обратим внимание на несколько широко применимых принципов:</p>
<ul>
<li><strong>Работоспособность (Operability)</strong><br />
Облегчить организациям поддержание бесперебойной работы системы.</li>
<li><strong>Простота (Simplicity)</strong><br />
Облегчить новым инженерам понимание системы, реализуя её с помощью хорошо понятных, согласованных шаблонов и структур и избегая ненужной сложности.</li>
<li><strong>Развиваемость (Evolvability)</strong><br />
Облегчить инженерам внесение изменений в систему в будущем, её адаптацию и расширение под непредвиденные сценарии по мере изменения требований.</li>
</ul>
<h2>Работоспособность: упрощаем жизнь операторам</h2>
<p>Ранее мы обсуждали роль эксплуатации в разделе «Operations in the Cloud Era» и увидели, что человеческие процессы не менее важны для надёжной работы, чем программные инструменты. На самом деле, высказывалось мнение, что «хорошая эксплуатация зачастую может компенсировать недостатки плохого (или неполного) ПО, но хорошее ПО не может надёжно работать при плохой эксплуатации».</p>
<p>В крупномасштабных системах, включающих тысячи машин, ручное обслуживание было бы неоправданно дорогим, и автоматизация крайне важна. Однако автоматизация — это палка о двух концах: всегда будут крайние случаи (например, редкие сбои), которые потребуют ручного вмешательства от команды эксплуатации. Поскольку случаи, не покрытые автоматикой, как правило, самые сложные, повышение уровня автоматизации требует более квалифицированной команды, способной решать такие задачи.</p>
<p>Кроме того, если автоматизированная система даёт сбой, её часто труднее отлаживать, чем систему, в которой оператор выполняет часть действий вручную. По этой причине нельзя сказать, что больше автоматизации всегда означает лучшую работоспособность. Тем не менее, определённый уровень автоматизации важен, а «золотая середина» зависит от конкретного приложения и организации.</p>
<p>Хорошая работоспособность означает, что рутинные задачи выполняются легко, позволяя операционной команде сосредоточиться на более ценных задачах. Системы работы с данными могут облегчать выполнение рутинных операций следующими способами:</p>
<p>Предоставление средств мониторинга ключевых метрик системы и поддержка инструментов наблюдаемости (см. «Проблемы распределённых систем»), обеспечивающих представление о поведении системы во время работы. Здесь могут помочь различные коммерческие и открытые инструменты.</p>
<p>Отсутствие зависимости от конкретных машин (возможность отключения машин для обслуживания без остановки всей системы)</p>
<p>Предоставление хорошей документации и простой для понимания операционной модели («Если я сделаю X, произойдёт Y»)</p>
<p>Предоставление хорошего поведения по умолчанию, но с возможностью для администраторов при необходимости переопределять эти значения</p>
<p>Самовосстановление там, где это уместно, но при этом возможность вручную управлять состоянием системы, когда это нужно</p>
<p>Предсказуемое поведение, сведение к минимуму неожиданностей</p>
<h2>Простота: управление сложностью</h2>
<p>Небольшие программные проекты могут иметь восхитительно простой и выразительный код, но по мере роста проектов они часто становятся очень сложными и трудными для понимания. Эта сложность замедляет работу всех, кто должен взаимодействовать с системой, что ещё больше увеличивает стоимость сопровождения. Программный проект, погрязший в сложности, иногда описывают как «большой ком грязи» (big ball of mud).</p>
<p>Когда сложность затрудняет сопровождение, бюджеты и сроки часто превышаются. В сложном программном обеспечении также выше риск внесения ошибок при изменениях: когда разработчикам трудно понять и осмыслить систему, скрытые предположения, непредвиденные последствия и неожиданные взаимодействия легче упустить из виду. И наоборот, снижение сложности значительно улучшает сопровождаемость ПО, и поэтому простота должна быть ключевой целью при создании систем.</p>
<p>Простые системы легче понять, и поэтому мы должны стремиться решать задачи максимально простым способом. К сожалению, это проще сказать, чем сделать. То, является ли что-либо простым, часто субъективно и зависит от вкуса, поскольку не существует объективного стандарта простоты. Например, одна система может скрывать сложную реализацию за простым интерфейсом, тогда как другая может иметь простую реализацию, но раскрывать больше внутренних деталей пользователю — какая из них проще?</p>
<p>Одна из попыток осмыслить сложность — разделить её на два типа: существенную и случайную (essential и accidental complexity). Считается, что существенная сложность присуща предметной области приложения, а случайная возникает из-за ограничений используемых инструментов. Однако и это разделение несовершенно, поскольку границы между существенным и случайным меняются по мере развития наших инструментов.</p>
<p>Одним из лучших инструментов управления сложностью является абстракция. Хорошая абстракция может скрыть множество деталей реализации за чистым, простым для понимания фасадом. Хорошая абстракция также может быть использована в широком диапазоне различных приложений. Такое повторное использование не только эффективнее, чем повторная реализация одного и того же, но также ведёт к более высокому качеству ПО, поскольку улучшения в абстрактном компоненте приносят пользу всем приложениям, которые его используют.</p>
<p>Например, языки программирования высокого уровня — это абстракции, скрывающие машинный код, регистры процессора и системные вызовы. SQL — это абстракция, скрывающая сложные структуры данных на диске и в памяти, параллельные запросы от других клиентов и несогласованности после сбоев. Конечно, при программировании на языке высокого уровня мы всё ещё используем машинный код — мы просто не используем его напрямую, потому что абстракция языка программирования избавляет нас от необходимости об этом думать.</p>
<p>Абстракции для прикладного кода, призванные уменьшить его сложность, можно создавать с помощью методологий вроде шаблонов проектирования и предметно-ориентированного проектирования (DDD). Эта книга не о таких прикладных абстракциях, а о более универсальных, на основе которых вы можете строить свои приложения — таких как транзакции в базах данных, индексы и журналы событий. Если вы хотите использовать такие техники, как DDD, вы можете реализовать их поверх фундаментальных компонентов, описанных в этой книге.</p>
<h2>Развиваемость: упрощение изменений</h2>
<p>Чрезвычайно маловероятно, что требования к вашей системе останутся неизменными навсегда. Намного вероятнее, что они будут постоянно меняться: вы узнаёте новые факты, появляются ранее не учтённые сценарии, меняются бизнес-приоритеты, пользователи запрашивают новые функции, новые платформы заменяют старые, появляются юридические или регуляторные требования, рост системы требует архитектурных изменений и т. д.</p>
<p>С точки зрения организационных процессов, гибкие (Agile) рабочие подходы предоставляют структуру для адаптации к изменениям. Сообщество Agile также разработало технические инструменты и процессы, полезные при разработке ПО в условиях частых изменений, такие как разработка через тестирование (TDD) и рефакторинг. В этой книге мы ищем способы повышения гибкости на уровне системы, состоящей из нескольких различных приложений или сервисов с разными характеристиками.</p>
<p>То, насколько легко вы можете модифицировать систему работы с данными и адаптировать её к изменяющимся требованиям, тесно связано с её простотой и абстракциями: слабо связанная, простая система обычно легче поддаётся изменениям, чем тесно связанная и сложная. Поскольку это настолько важная идея, мы используем для обозначения гибкости на уровне систем данных отдельное слово: развиваемость (evolvability).</p>
<p>Одним из главных факторов, усложняющих изменения в больших системах, является необратимость некоторых действий, поэтому такие действия приходится выполнять с особой осторожностью. Например, если вы переходите с одной базы данных на другую, и не можете вернуться к старой системе в случае проблем с новой, ставки гораздо выше, чем если бы вы могли легко переключиться обратно. Минимизация необратимости улучшает гибкость.</p>
<h1>Краткая сводка по 2 главе</h1>
<p>В этой главе мы рассмотрели несколько примеров нефункциональных требований: производительность, надёжность, масштабируемость и сопровождаемость. Через эти темы мы также познакомились с принципами и терминологией, которые нам понадобятся в остальной части книги. Мы начали с практического примера реализации ленты новостей в социальной сети, который показал некоторые сложности, возникающие при масштабировании.</p>
<p>Мы обсудили, как измерять производительность (например, с помощью перцентилей времени отклика), нагрузку на систему (например, через показатели пропускной способности) и как они используются в соглашениях об уровне обслуживания (SLA). Масштабируемость — тесно связанное понятие: это способность поддерживать производительность при росте нагрузки. Мы увидели некоторые общие принципы масштабируемости, такие как разбиение задачи на части, которые могут работать независимо, и в следующих главах мы подробно рассмотрим технические методы масштабирования.</p>
<p>Для достижения надёжности можно использовать методы отказоустойчивости, позволяющие системе продолжать предоставлять услуги даже при сбоях компонентов (например, диска, машины или другого сервиса). Мы рассмотрели примеры аппаратных сбоев и отличили их от сбоев программного обеспечения, с которыми труднее справляться, поскольку они часто коррелированы. Ещё один аспект надёжности — устойчивость к человеческим ошибкам, и мы познакомились с анализом инцидентов без поиска виновных (blameless postmortems) как способом извлечения уроков из сбоев.</p>
<p>Наконец, мы рассмотрели несколько аспектов сопровождаемости, включая поддержку работы эксплуатационных команд, управление сложностью и упрощение эволюции функциональности приложения со временем. Не существует универсальных решений для достижения этих целей, но одним из подходов, который может помочь, является создание приложений с использованием хорошо понятных строительных блоков, предоставляющих полезные абстракции. Остальная часть этой книги будет посвящена таким строительным блокам, которые показали свою практическую ценность.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-2-defining-nonfunctional-requirements/">Глава 2. Определение нефункциональных требований</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-2-defining-nonfunctional-requirements/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 1. Компромиссы в архитектуре систем данных</title>
		<link>https://datatalks.ru/chapter-1-trade-offs-in-data-systems-architecture/</link>
					<comments>https://datatalks.ru/chapter-1-trade-offs-in-data-systems-architecture/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Thu, 24 Jul 2025 18:12:32 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Designing Data-Intensive Applications]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1926</guid>

					<description><![CDATA[<p>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта Глава 1. Компромиссы в архитектуре систем обработки данных Решений не существует — существуют лишь компромиссы. […] Но вы стараетесь добиться наилучшего возможного компромисса, и это всё, на что вы можете рассчитывать. Томас Соул, интервью с Фредом Барнсом (2005) Данные являются центральным элементом при разработке [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-1-trade-offs-in-data-systems-architecture/">Глава 1. Компромиссы в архитектуре систем данных</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги &#171;Designing Data-Intensive Applications, 2nd Edition&#187; подготовлен автором сайта</em></p>
<h1>Глава 1. Компромиссы в архитектуре систем обработки данных</h1>
<blockquote><p>Решений не существует — существуют лишь компромиссы. […] Но вы стараетесь добиться наилучшего возможного компромисса, и это всё, на что вы можете рассчитывать.</p>
<p><em>Томас Соул, интервью с Фредом Барнсом (2005)</em></p></blockquote>
<hr />
<p>Данные являются центральным элементом при разработке многих современных приложений. В веб- и мобильных приложениях, в программном обеспечении как услуге (<strong>SaaS</strong>) и облачных сервисах стало нормой хранить данные от множества различных пользователей в общей серверной инфраструктуре хранения данных. Данные о пользовательской активности, бизнес-транзакциях, устройствах и сенсорах необходимо сохранять и делать доступными для анализа. По мере взаимодействия пользователей с приложением они как читают ранее сохранённые данные, так и генерируют новые.</p>
<p>С малыми объёмами данных, которые можно хранить и обрабатывать на одной машине, обычно легко работать. Однако по мере роста объёма данных или частоты запросов возникает необходимость распределения данных по нескольким машинам, что порождает множество проблем. С усложнением требований к приложению становится недостаточно хранить всё в одной системе — может возникнуть необходимость комбинировать несколько систем хранения или обработки, обладающих разными возможностями.</p>
<p>Мы называем приложение <strong>«ориентированным на данные» (data-intensive)</strong>, если управление данными является одной из ключевых задач при его разработке. В то время как в системах, ориентированных на вычисления, основной вызов заключается в параллелизации ресурсоёмких вычислений, в ориентированных на данные приложениях мы чаще беспокоимся о таких вещах, как хранение и обработка больших объёмов данных, управление изменениями данных, обеспечение согласованности при сбоях и параллельном доступе, а также достижение высокой доступности сервисов.</p>
<p>Такие приложения, как правило, строятся из стандартных компонентов, обеспечивающих часто востребуемую функциональность. Например, многим приложениям необходимо:</p>
<ul>
<li>Хранить данные, чтобы они сами или другое приложение могли получить к ним доступ позже (<strong>базы данных</strong>)</li>
<li>Запоминать результат дорогостоящей операции, чтобы ускорить чтение (<strong>кэши</strong>)</li>
<li>Позволять пользователям искать данные по ключевым словам или фильтровать их различными способами (<strong>поисковые индексы</strong>)</li>
<li>Обрабатывать события и изменения данных по мере их возникновения (<strong>стриминг/обработка потоков</strong>)</li>
<li>Периодически обрабатывать большие накопленные объёмы данных (<strong>пакетная обработка</strong>)</li>
</ul>
<p>При построении приложения мы обычно берём несколько программных систем или сервисов — таких как <strong>базы данных </strong>или<strong> API</strong> — и объединяем их с помощью собственного прикладного кода. Если вы используете системы данных строго в соответствии с тем, для чего они были предназначены, то процесс может быть довольно простым.</p>
<p>Однако по мере роста амбиций вашего приложения начинают возникать сложности. Существует множество систем баз данных с разными характеристиками, подходящими для различных задач — как выбрать нужную? Существует несколько подходов к кэшированию, множество способов построения поисковых индексов и т.д. — как оценить их компромиссы? Нужно понять, какие инструменты и подходы наиболее уместны в конкретной задаче, и бывает трудно комбинировать инструменты, если необходимо выполнить то, что ни один из них в одиночку не умеет.</p>
<p>Эта книга — руководство, которое поможет вам принимать решения о том, какие технологии использовать и как их сочетать. Как вы увидите, не существует универсального подхода, который был бы заведомо лучше остальных; у всего есть свои плюсы и минусы. С помощью этой книги вы научитесь задавать правильные вопросы, чтобы оценивать и сравнивать системы обработки данных и определять, какой подход наилучшим образом удовлетворит потребности вашего конкретного приложения.</p>
<p><strong>Мы начнём наш путь с изучения способов, которыми данные обычно используются в современных организациях.</strong> Многие из описываемых здесь идей происходят из корпоративного программного обеспечения (т.е. программных решений и инженерных практик, применяемых в крупных организациях, таких как корпорации и государственные структуры), поскольку исторически именно крупные организации сталкивались с такими объёмами данных, для которых требовались сложные технические решения. Если у вас достаточно небольшой объём данных, можно просто хранить всё в Excel! Однако в последние годы стало обычным делом, что и небольшие компании, и стартапы работают с большими объёмами данных и строят ориентированные на данные системы.</p>
<p>Одной из ключевых проблем в системах работы с данными является то, что разные люди по-разному взаимодействуют с данными. Если вы работаете в компании, у вас с командой будет один набор приоритетов, а у другой команды могут быть совершенно иные цели, даже если вы работаете с одним и тем же датасетом! Более того, эти цели могут быть неявными или плохо сформулированными, что ведёт к недопониманию и разногласиям по поводу того, какой подход является «правильным».</p>
<p>Чтобы помочь вам разобраться в возможных вариантах, эта глава сравнивает несколько противоположных концепций и рассматривает их компромиссы:</p>
<ul>
<li>различие между операционными и аналитическими системами («Analytical versus Operational Systems»);</li>
<li>плюсы и минусы облачных сервисов и систем с самостоятельным размещением («Cloud versus Self-Hosting»);</li>
<li>когда следует переходить от одноузловых систем к распределённым системам («Distributed versus Single-Node Systems»);</li>
<li>и как сбалансировать потребности бизнеса и права пользователя («Data Systems, Law, and Society»).</li>
</ul>
<p>Кроме того, эта глава предоставит вам терминологию, которая понадобится нам в остальной части книги.</p>
<h1>ТЕРМИНОЛОГИЯ: ФРОНТЕНДЫ И БЭКЕНДЫ</h1>
<p>Многое из того, что мы будем обсуждать в этой книге, относится к разработке бэкенда. Чтобы объяснить этот термин: в веб-приложениях клиентский код (который выполняется в веб-браузере) называется <strong>фронтендом</strong>, а серверный код, который обрабатывает пользовательские запросы, называется <strong>бэкендом</strong>. Мобильные приложения аналогичны фронтендам тем, что предоставляют пользовательские интерфейсы, которые часто взаимодействуют по Интернету с серверным бэкендом. Иногда фронтенды управляют данными локально на устройстве пользователя, но основные проблемы инфраструктуры данных чаще всего лежат на стороне бэкенда: фронтенду необходимо работать лишь с данными одного пользователя, тогда как бэкенд управляет данными от имени всех пользователей.</p>
<p>Бэкенд-сервис обычно доступен по <strong>HTTP</strong> (иногда через <strong>WebSocket</strong>); он обычно состоит из некоторого прикладного кода, который читает и записывает данные в одну или несколько баз данных, а также иногда взаимодействует с дополнительными системами данных, такими как <strong>кэши</strong> или <strong>очереди сообщений</strong> (всё это вместе мы можем называть инфраструктурой данных). Прикладной код часто является «без состояния» (т.е. после завершения обработки одного HTTP-запроса он забывает всё, что касается этого запроса), и любая информация, которую необходимо сохранить между запросами, должна храниться либо на клиенте, либо в серверной инфраструктуре данных.</p>
<h1>Аналитические против операционных систем (Analytical versus Operational Systems)</h1>
<p>Если вы работаете с системами данных в крупной организации, скорее всего, вы столкнётесь с несколькими разными типами людей, которые работают с данными. Первый тип — это инженеры бэкенда, которые создают сервисы, обрабатывающие запросы на чтение и обновление данных; эти сервисы часто обслуживают внешних пользователей, либо напрямую, либо косвенно — через другие сервисы. Иногда сервисы предназначены для внутреннего использования другими частями организации.</p>
<p>В дополнение к командам, управляющим бэкенд-сервисами, обычно есть две другие группы людей, которым требуется доступ к данным организации: <strong>бизнес-аналитики</strong>, которые формируют отчёты о деятельности организации, чтобы помочь руководству принимать более обоснованные решения (business intelligence, или BI), и <strong>дата-сайентисты</strong>, которые ищут новые инсайты в данных или создают пользовательские функции на основе анализа данных и машинного обучения/ИИ (например, рекомендации на сайте электронной коммерции «покупатели, купившие X, также купили Y», предиктивная аналитика вроде скоринга рисков или фильтрации спама, а также ранжирование результатов поиска).</p>
<p>Хотя бизнес-аналитики и дата-сайентисты используют разные инструменты и работают по-разному, у них есть общее: оба занимаются аналитикой, то есть они исследуют данные, сгенерированные пользователями и бэкенд-сервисами, но, как правило, не модифицируют эти данные (за исключением, возможно, исправления ошибок). Они могут создавать производные датасеты, в которых исходные данные каким-либо образом обработаны. Это привело к разделению на два типа систем — различию, которое мы будем использовать на протяжении всей книги:</p>
<ul>
<li><strong>Операционные системы</strong> состоят из бэкенд-сервисов и инфраструктуры данных, где данные создаются, например, путём обслуживания внешних пользователей. Здесь прикладной код как читает, так и изменяет данные в своих базах данных на основе действий, совершаемых пользователями.</li>
<li><strong>Аналитические системы</strong> обслуживают потребности бизнес-аналитиков и дата-сайентистов. Они содержат только для чтения копию данных из операционных систем и оптимизированы под те типы обработки данных, которые нужны для аналитики.</li>
</ul>
<p>Как мы увидим в следующем разделе, операционные и аналитические системы часто разделены — и на то есть веские причины. По мере того как эти системы развивались, появились две новые специализированные роли: дата-инженеры и аналитик-инженеры. <strong>Дата-инженеры</strong> — это специалисты, которые знают, как интегрировать операционные и аналитические системы и несут ответственность за более широкую инфраструктуру данных в организации. <strong>Аналитики-инженеры</strong> моделируют и трансформируют данные, чтобы сделать их более полезными для бизнес-аналитиков и дата-сайентистов в организации.</p>
<p>Многие инженеры специализируются либо на операционной, либо на аналитической стороне. Тем не менее, эта книга охватывает как операционные, так и аналитические системы данных, поскольку обе играют важную роль в жизненном цикле данных внутри организации. Мы подробно изучим инфраструктуру данных, которая используется для предоставления сервисов как внутренним, так и внешним пользователям, чтобы вы могли эффективнее взаимодействовать с коллегами с другой стороны этого разделения.</p>
<h2>Характеристика обработки транзакций и аналитики (Characterizing Transaction Processing and Analytics)</h2>
<p>В первые годы автоматизации бизнес-процессов запись в базу данных обычно соответствовала совершению коммерческой транзакции: продаже, заказу у поставщика, выплате зарплаты сотруднику и т.п. По мере того как базы данных стали использоваться в областях, не связанных с денежными операциями, термин «транзакция» всё равно закрепился, обозначая группу операций чтения и записи, которые образуют логическую единицу.</p>
<p>Даже когда базы данных начали применяться для самых разных типов данных — публикации в соцсетях, ходы в играх, контакты в адресной книге и многое другое — базовый шаблон доступа остался схожим с обработкой бизнес-транзакций. Операционная система обычно ищет небольшое количество записей по какому-либо ключу (это называется точечный запрос, или point query). Записи вставляются, обновляются или удаляются на основе ввода пользователя. Поскольку эти приложения являются интерактивными, такой шаблон доступа стал известен как <strong>онлайновая обработка транзакций (OLTP)</strong>.</p>
<p>Однако базы данных также всё шире начали использоваться для аналитики, и шаблоны доступа в этом случае сильно отличаются от OLTP. Аналитический запрос, как правило, сканирует огромное количество записей и вычисляет агрегированные показатели (такие как количество, сумма или среднее значение), а не возвращает отдельные записи пользователю. Например, бизнес-аналитик в сети супермаркетов может захотеть получить ответы на <strong>аналитические запросы</strong> вроде:</p>
<ol>
<li>Какова была общая выручка каждого из наших магазинов в январе?</li>
<li>Насколько больше обычного мы продали бананов во время последней акции?</li>
<li>Какой бренд детского питания чаще всего покупают вместе с подгузниками бренда X?</li>
</ol>
<p>Отчёты, формируемые на основе таких запросов, имеют важное значение для <strong>бизнес-аналитики (BI)</strong>, помогая руководству принимать последующие решения. Чтобы отличить такой способ использования баз данных от транзакционной обработки, его назвали <strong>онлайновой аналитической обработкой (OLAP)</strong>. Разграничение между OLTP и аналитикой не всегда является чётким, но некоторые типичные характеристики приведены в Таблице 1-1.</p>
<p><strong>Таблица 1-1. Сравнение характеристик операционных и аналитических систем</strong></p>
<table>
<thead>
<tr>
<th>Свойство</th>
<th>Операционные системы (OLTP)</th>
<th>Аналитические системы (OLAP)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Основной шаблон чтения</td>
<td>Точечные запросы (получение отдельных записей по ключу)</td>
<td>Агрегация по большому количеству записей</td>
</tr>
<tr>
<td>Основной шаблон записи</td>
<td>Создание, обновление и удаление отдельных записей</td>
<td>Массовый импорт (ETL) или поток событий</td>
</tr>
<tr>
<td>Пример пользователя-человека</td>
<td>Конечный пользователь веб-/мобильного приложения</td>
<td>Внутренний аналитик для поддержки принятия решений</td>
</tr>
<tr>
<td>Пример машинного использования</td>
<td>Проверка, разрешено ли действие</td>
<td>Обнаружение шаблонов мошенничества/злоупотреблений</td>
</tr>
<tr>
<td>Тип запросов</td>
<td>Фиксированный набор запросов, предопределённых в приложении</td>
<td>Аналитик может выполнять произвольные запросы</td>
</tr>
<tr>
<td>Представляемые данные</td>
<td>Актуальное состояние данных (текущий момент времени)</td>
<td>История событий, произошедших с течением времени</td>
</tr>
<tr>
<td>Размер датасета</td>
<td>От гигабайт до терабайт</td>
<td>От терабайт до петабайт</td>
</tr>
</tbody>
</table>
<p><strong>ПРИМЕЧАНИЕ</strong><br />
Значение слова онлайн в OLAP не вполне ясно; вероятно, оно указывает на то, что запросы выполняются не только для предопределённых отчётов, но и интерактивно — аналитики используют систему OLAP для исследовательских запросов.</p>
<hr />
<p>В <strong>операционных системах</strong> пользователям, как правило, не разрешается самостоятельно составлять SQL-запросы и выполнять их на базе данных, поскольку это потенциально позволяет им читать или изменять данные, к которым у них нет доступа. Кроме того, они могут написать запросы с высокой стоимостью выполнения, что ухудшит производительность базы данных для других пользователей. По этим причинам системы OLTP в основном выполняют фиксированный набор запросов, встроенных в код приложения, и лишь изредка используются разовые специальные запросы для обслуживания или устранения неполадок. С другой стороны, <strong>аналитические базы данных</strong> обычно предоставляют пользователям свободу писать произвольные SQL-запросы вручную или генерировать запросы автоматически с помощью инструментов визуализации данных или дашбордов, таких как <strong>Tableau</strong>, <strong>Looker</strong> или <strong>Microsoft Power BI</strong>.</p>
<p>Существует также тип систем, предназначенных для аналитических нагрузок (запросов, агрегирующих множество записей), но встроенных в пользовательские продукты. Эта категория известна как <strong>продуктовая аналитика</strong> или <strong>аналитика в реальном времени</strong>, и к системам, спроектированным для такого использования, относятся <strong>Pinot</strong>, <strong>Druid</strong> и <strong>ClickHouse</strong>.</p>
<h1>Хранилище данных (Data Warehousing)</h1>
<p>Поначалу одни и те же базы данных использовались как для обработки транзакций, так и для аналитических запросов. SQL оказался довольно гибким в этом отношении: он хорошо подходит для обоих типов запросов. Тем не менее, в конце 1980-х и начале 1990-х годов появилась тенденция: компании стали отказываться от использования своих OLTP-систем для аналитики и начали выполнять аналитику на отдельной системе баз данных. Эта отдельная база данных получила название <strong>хранилище данных (data warehouse)</strong>.</p>
<p>Крупное предприятие может иметь десятки или даже сотни систем OLTP: системы, обеспечивающие работу клиентского сайта, управляющие кассовыми терминалами в физических магазинах, отслеживающие запасы на складах, планирующие маршруты для транспорта, управляющие поставщиками, администрирующие сотрудников и выполняющие множество других задач. Каждая из этих систем сложна и требует отдельной команды для поддержки, поэтому они в основном работают независимо друг от друга.</p>
<p>Обычно нецелесообразно, чтобы бизнес-аналитики и дата-сайентисты напрямую делали запросы к этим OLTP-системам, по нескольким причинам:</p>
<ul>
<li>интересующие данные могут быть распределены между несколькими операционными системами, и их сложно объединить в одном запросе (эта проблема известна как информационные силосы, data silos);</li>
<li>схемы и структуры данных, оптимальные для OLTP, плохо подходят для аналитики (см. «Звёзды и снежинки: схемы для аналитики»);</li>
<li>аналитические запросы могут быть весьма ресурсоёмкими, и их выполнение на базе данных OLTP может повлиять на производительность для других пользователей;</li>
<li>системы OLTP могут находиться в отдельной сети, к которой у пользователей нет прямого доступа по соображениям безопасности или соответствия нормативам.</li>
</ul>
<p><strong>Хранилище данных</strong>, напротив, представляет собой отдельную базу данных, к которой аналитики могут обращаться сколько угодно, не затрагивая операции OLTP. Как мы увидим в Главе 4, хранилища данных часто хранят данные в форме, сильно отличающейся от баз данных OLTP, чтобы оптимизировать выполнение типичных аналитических запросов.</p>
<p>Хранилище данных содержит только для чтения копию данных из всех различных OLTP-систем компании. Данные извлекаются из OLTP-баз данных (с помощью периодического дампа данных или непрерывного потока обновлений), трансформируются в схему, удобную для анализа, очищаются, а затем загружаются в хранилище данных. Этот процесс загрузки данных в хранилище известен как <strong>Extract–Transform–Load (ETL)</strong> и показан на Рисунке 1-1. Иногда порядок шагов transform и load меняется местами (т.е. трансформация выполняется в хранилище данных, после загрузки), что приводит к схеме <strong>ELT</strong>.</p>
<p><strong>Рисунок 1-1. Упрощённая схема ETL в хранилище данных</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1935" src="https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse.jpeg" alt="" width="1432" height="996" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse.jpeg 1432w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse-300x209.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse-1024x712.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse-768x534.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse-450x313.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_1_Simplified_outline_of_ETL_into_data_warehouse-780x543.jpeg 780w" sizes="(max-width: 1432px) 100vw, 1432px" /></a></p>
<p>В некоторых случаях источниками данных для ETL-процессов являются внешние SaaS-продукты, такие как системы управления взаимоотношениями с клиентами (CRM), email-маркетинг или системы обработки кредитных карт. В таких случаях у вас нет прямого доступа к исходной базе данных, поскольку она доступна только через API поставщика программного обеспечения. Перенос данных из этих внешних систем в собственное хранилище данных может дать возможность выполнять анализы, которые невозможны через <strong>SaaS API</strong>. ETL для SaaS API часто реализуется с помощью специализированных сервисов-коннекторов данных, таких как <strong>Fivetran</strong>, <strong>Singer</strong> или <strong>AirByte</strong>.</p>
<p>Некоторые системы баз данных предлагают <strong>гибридную транзакционную/аналитическую обработку</strong> (<strong>HTAP</strong>), цель которой — обеспечить выполнение <strong>OLTP</strong> и аналитики в одной системе без необходимости ETL из одной системы в другую. Однако многие системы <strong>HTAP</strong> по сути состоят из OLTP-системы в связке с отдельной аналитической системой, скрытых за общим интерфейсом — поэтому различие между ними по-прежнему важно для понимания того, как работают такие системы.</p>
<p>Более того, несмотря на существование <strong>HTAP</strong>, разделение транзакционных и аналитических систем является обычной практикой из-за различий в целях и требованиях. В частности, считается хорошей практикой, чтобы каждая операционная система имела свою собственную базу данных (см. «Микросервисы и Serverless»), что приводит к сотням отдельных операционных баз данных; в то время как у предприятия, как правило, имеется одно хранилище данных, чтобы бизнес-аналитики могли объединять данные из нескольких операционных систем в одном запросе.</p>
<p>Таким образом, <strong>HTAP</strong> не заменяет хранилища данных. Скорее, он полезен в сценариях, когда одному и тому же приложению нужно как выполнять аналитические запросы, сканирующие большое количество строк, так и читать и обновлять отдельные записи с низкой задержкой. Обнаружение мошенничества может включать такие типы нагрузок, например.</p>
<p>Разделение между операционными и аналитическими системами является частью более широкой тенденции: по мере увеличения требований к нагрузке системы становятся всё более специализированными и оптимизированными под конкретные типы задач. Системы общего назначения могут легко обрабатывать малые объёмы данных, но с увеличением масштаба системы, как правило, становятся более специализированными.</p>
<h1>От хранилища данных к озеру данных (Data Lake)</h1>
<p>Хранилище данных часто использует реляционную модель данных, к которой можно обращаться с помощью SQL (см. Главу 3), возможно — через специализированное программное обеспечение для бизнес-аналитики. Эта модель хорошо подходит для типов запросов, которые выполняют бизнес-аналитики, но она хуже приспособлена для нужд дата-сайентистов, которым может понадобиться выполнять такие задачи, как:</p>
<p>Преобразование данных в форму, пригодную для обучения модели машинного обучения; часто это требует преобразования строк и столбцов таблицы базы данных в вектор или матрицу числовых значений, называемых признаками. Процесс выполнения такого преобразования с целью максимизации производительности обученной модели называется разработкой признаков (feature engineering), и он часто требует пользовательского кода, который трудно выразить с помощью SQL.</p>
<p><strong>Обработка текстовых данных</strong> (например, отзывов о продукте) с использованием методов обработки естественного языка с целью извлечения из них структурированной информации (например, настроения автора или тем, которые он упоминает). Аналогично, может потребоваться извлечение структурированной информации из фотографий с помощью методов компьютерного зрения.</p>
<p>Хотя предпринимались попытки добавить операторы машинного обучения в SQL-модель данных и построить эффективные системы машинного обучения на реляционной основе, многие дата-сайентисты предпочитают не работать в реляционных базах данных, таких как хранилище данных. Вместо этого они часто предпочитают использовать библиотеки анализа данных на <strong>Python</strong>, такие как <strong>pandas</strong> и <strong>scikit-learn</strong>, языки статистического анализа, такие как <strong>R</strong>, и распределённые фреймворки аналитики, такие как <strong>Spark</strong>. Мы обсудим это подробнее в разделе «Dataframes, Matrices, and Arrays».</p>
<p>В результате организации сталкиваются с необходимостью предоставлять данные в форме, пригодной для использования дата-сайентистами. Ответом на эту потребность стало <strong>озеро данных</strong>: централизованное хранилище, в котором содержится копия любых данных, которые могут быть полезны для анализа, полученных из операционных систем через ETL-процессы. Различие между хранилищем данных и озером данных заключается в том, что озеро данных просто содержит файлы, не накладывая никаких требований к формату файлов или модели данных. Файлы в озере данных могут быть коллекциями записей базы данных, закодированными в формате файлов, таком как Avro или Parquet (см. Главу 5), но они также могут содержать текст, изображения, видео, данные сенсоров, разреженные матрицы, векторы признаков, последовательности генома или любые другие типы данных. Помимо большей гибкости, это также зачастую дешевле, чем реляционное хранение данных, поскольку озеро данных может использовать стандартизированное файловое хранилище, такое как <strong>object storage</strong> (см. «Архитектура облачно-нативных систем»).</p>
<p><strong>ETL-процессы</strong> были обобщены в концепцию конвейеров данных (<strong>data pipelines</strong>), и в некоторых случаях озеро данных стало промежуточной остановкой на пути от операционных систем к хранилищу данных. <strong>Озеро данных</strong> содержит данные в «сыром» виде, произведённые операционными системами, без трансформации в схему реляционного хранилища данных. Такой подход имеет то преимущество, что каждый потребитель данных может преобразовать сырые данные в форму, которая наилучшим образом соответствует его потребностям. Это получило название принципа суши: «сырые данные — лучше».</p>
<p>Помимо загрузки данных из озера данных в отдельное хранилище данных, также возможно выполнение типичных рабочих нагрузок хранилища данных (SQL-запросы и бизнес-аналитика) напрямую по файлам в озере данных, параллельно с задачами в области data science и машинного обучения. Такая архитектура называется <strong>data lakehouse</strong> и требует наличия движка выполнения запросов и слоя метаданных (например, управления схемами), расширяющих файловое хранилище озера данных. Примеры такого подхода включают <strong>Apache Hive</strong>, <strong>Spark SQL</strong>, <strong>Presto</strong> и <strong>Trino</strong>.</p>
<h2>За пределами озера данных</h2>
<p>По мере развития аналитических практик организации всё больше обращают внимание на управление и эксплуатацию аналитических систем и конвейеров данных, что, например, отражено в <strong>манифесте DataOps</strong>. Частью этого являются вопросы управления, конфиденциальности и соблюдения нормативных требований, таких как <strong>GDPR</strong> и <strong>CCPA</strong>, которые мы обсуждаем в разделе «Информационные системы, право и общество».</p>
<p>Более того, аналитические данные всё чаще доступны не только в виде файлов и реляционных таблиц, но и в виде потоков событий. При анализе данных на основе файлов можно периодически повторно выполнять анализ (например, ежедневно), чтобы отреагировать на изменения в данных, но потоковая обработка позволяет аналитическим системам реагировать на события гораздо быстрее — за секунды. В зависимости от приложения и его чувствительности ко времени, подход с потоковой обработкой может быть ценным, например, для выявления и блокировки потенциально мошеннической или злоупотребляющей активности.</p>
<p>В некоторых случаях результаты работы аналитических систем передаются в операционные системы (процесс, иногда называемый <strong>обратным ETL</strong>). Например, модель машинного обучения, обученная на данных в аналитической системе, может быть развернута в продакшене, чтобы формировать рекомендации для конечных пользователей, такие как «покупатели X также покупали Y». Такие развернутые результаты аналитических систем также называют продуктами данных. Модели машинного обучения могут быть развернуты в операционных системах с помощью специализированных инструментов, таких как <strong>TFX</strong>, <strong>Kubeflow</strong> или <strong>MLflow</strong>.</p>
<h1>Системы записи и производные данные (Systems of Record and Derived Data)</h1>
<p>В дополнение к различию между операционными и аналитическими системами, эта книга также различает системы записи и системы производных данных. Эти термины полезны, потому что помогают прояснить поток данных в системе:</p>
<h2>Системы записи</h2>
<p>Система записи, также известная как источник истины, содержит авторитетную или каноническую версию каких-либо данных. Когда поступают новые данные, например, от пользователя, они сначала записываются сюда. Каждый факт представлен ровно один раз (представление, как правило, нормализовано; см. «Нормализация, денормализация и объединения»). Если существует несоответствие между другой системой и системой записи, то значение в системе записи считается (по определению) правильным.</p>
<h2>Системы производных данных</h2>
<p>Данные в производной системе являются результатом получения каких-либо существующих данных из другой системы и их преобразования или обработки определённым образом. Если вы потеряете производные данные, вы можете воссоздать их из исходного источника. Классическим примером является кэш: данные могут извлекаться из кэша, если они там есть, но если нужных данных нет, можно обратиться к основной базе данных. Денормализованные значения, индексы, материализованные представления, преобразованные представления данных и модели, обученные на наборе данных, также относятся к этой категории.</p>
<p>С технической точки зрения, производные данные являются избыточными, поскольку дублируют уже существующую информацию. Однако они часто необходимы для обеспечения высокой производительности при выполнении запросов на чтение. Из одного источника можно получить несколько различных производных наборов данных, что позволяет просматривать данные с разных «точек зрения».</p>
<p>Аналитические системы обычно являются системами производных данных, поскольку они являются потребителями данных, созданных в других местах. Операционные сервисы могут содержать как системы записи, так и системы производных данных. Системы записи — это основные базы данных, в которые данные записываются первыми, тогда как системы производных данных — это индексы и кэши, ускоряющие частые операции чтения, особенно те, которые система записи не может эффективно выполнять.</p>
<p>Большинство баз данных, движков хранения и языков запросов по своей природе не являются ни системами записи, ни производными системами. База данных — это всего лишь инструмент: как вы её используете — зависит от вас. Различие между системой записи и производной системой данных определяется не инструментом, а тем, как вы его используете в своём приложении. Чёткое понимание того, какие данные являются производными от каких, позволяет внести ясность в архитектуру системы, которая в противном случае может быть запутанной.</p>
<p>Когда данные в одной системе производятся из данных в другой, необходим процесс обновления производных данных при изменении оригинальных данных в системе записи. К сожалению, многие базы данных проектируются с предположением, что вашему приложению нужна только одна база данных, и не упрощают интеграцию нескольких систем для распространения таких обновлений.</p>
<p>Это подводит нас к завершению сравнения аналитической и транзакционной обработки. В следующем разделе мы рассмотрим ещё один компромисс, обсуждение которого вы, возможно, уже неоднократно встречали.</p>
<h1>Облако против самостоятельного размещения (Cloud versus Self-Hosting)</h1>
<p>Когда организация собирается что-либо делать, один из первых вопросов: стоит ли делать это внутри компании или передать на аутсорс? Стоит ли строить или покупать?</p>
<p>В конечном счёте это вопрос приоритетов бизнеса. Расхожая управленческая мудрость гласит, что то, что является ключевой компетенцией или конкурентным преимуществом вашей организации, должно выполняться внутри компании, тогда как рутинные, стандартные или некритичные задачи следует передать поставщику. Приведём крайний пример: большинство компаний не производят свою собственную электроэнергию (если они не энергетические компании и не учитывать аварийные источники питания), поскольку дешевле покупать электроэнергию из сети.</p>
<p>В программном обеспечении необходимо принять два важных решения: кто будет разрабатывать программное обеспечение и кто будет его развёртывать. Существует спектр вариантов, где каждое из этих решений может быть передано на аутсорсинг в разной степени, как показано на рисунке 1-2. На одном конце спектра — индивидуальное программное обеспечение, которое вы пишете и запускаете внутри компании; на другом — широко используемые облачные сервисы или SaaS-продукты, которые разрабатываются и эксплуатируются внешним поставщиком, а вы получаете к ним доступ только через веб-интерфейс или API.</p>
<p><strong>Рисунок 1-2. Спектр типов программного обеспечения и его эксплуатации</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1936" src="https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations.jpeg" alt="" width="1735" height="369" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations.jpeg 1735w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-300x64.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-1024x218.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-768x163.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-1536x327.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-450x96.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-780x166.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/07/image_1_2_types_of_software_and_its_operations-1600x340.jpeg 1600w" sizes="(max-width: 1735px) 100vw, 1735px" /></a></p>
<p>Промежуточный вариант — это готовое программное обеспечение (open source или коммерческое), которое вы размещаете самостоятельно, то есть разворачиваете собственными силами — например, если вы скачиваете MySQL и устанавливаете его на сервер, находящийся под вашим контролем. Это может быть как на вашем собственном оборудовании (часто называемом on-premises, даже если сервер физически находится в арендованной стойке в дата-центре, а не буквально у вас на территории), так и на виртуальной машине в облаке (инфраструктура как услуга, или IaaS). На этом спектре есть и другие точки, например, запуск модифицированной версии open source ПО.</p>
<p>Отдельно от этого спектра стоит вопрос, как вы размещаете сервисы — в облаке или on-premises, например, используете ли вы оркестрационную систему, такую как Kubernetes. Однако выбор инструментов развертывания выходит за рамки этой книги, поскольку на архитектуру информационных систем большее влияние оказывают другие факторы.</p>
<h2>Плюсы и минусы облачных сервисов</h2>
<p>Использование облачного сервиса, вместо самостоятельного запуска аналогичного программного обеспечения, по сути означает передачу эксплуатации этого ПО облачному провайдеру. Существуют веские аргументы как за, так и против облачных сервисов. Облачные провайдеры утверждают, что использование их сервисов экономит время и деньги, а также позволяет действовать быстрее по сравнению с настройкой собственной инфраструктуры.</p>
<p>Является ли облачный сервис на самом деле дешевле и проще, чем самостоятельное размещение, сильно зависит от ваших навыков и нагрузки на ваши системы. Если у вас уже есть опыт настройки и эксплуатации необходимых систем, и если ваша нагрузка довольно предсказуема (т.е. количество нужных машин не колеблется сильно), тогда часто дешевле купить собственные машины и запускать программное обеспечение на них самостоятельно.</p>
<p>С другой стороны, если вам нужна система, которую вы ещё не умеете разворачивать и обслуживать, тогда использование облачного сервиса часто проще и быстрее, чем изучение управления системой самостоятельно. Если вам нужно нанимать и обучать персонал специально для поддержки и эксплуатации системы, это может стать очень дорого. Команда эксплуатации вам всё равно понадобится даже при использовании облака (см. “Эксплуатация в эпоху облаков”), но передача базового системного администрирования на аутсорсинг может освободить команду для решения более высокоуровневых задач.</p>
<p>Когда вы передаёте эксплуатацию системы компании, специализирующейся на запуске этой услуги, это потенциально может привести к более качественному сервису, поскольку поставщик получает операционную экспертизу, предоставляя услугу многим клиентам. С другой стороны, если вы запускаете сервис самостоятельно, вы можете настроить и оптимизировать его под вашу конкретную нагрузку; маловероятно, что облачный сервис будет готов делать такие индивидуальные настройки за вас.</p>
<p>Облачные сервисы особенно ценны, если нагрузка на ваши системы сильно меняется со временем. Если вы резервируете мощности для пиковых нагрузок, но эти ресурсы простаивают большую часть времени, система становится менее экономичной. В такой ситуации облачные сервисы выигрывают тем, что позволяют легче масштабировать вычислительные ресурсы вверх или вниз в ответ на изменение спроса.</p>
<p>Например, аналитические системы часто имеют крайне переменную нагрузку: выполнение большого аналитического запроса быстро требует большого количества вычислительных ресурсов параллельно, но как только запрос завершён, эти ресурсы простаивают до следующего запроса пользователя. Предопределённые запросы (например, для ежедневных отчётов) можно ставить в очередь и планировать, чтобы сгладить нагрузку, но для интерактивных запросов — чем быстрее вы хотите их выполнить, тем более переменной становится нагрузка. Если ваш набор данных настолько велик, что для быстрого выполнения запроса нужны значительные вычислительные ресурсы, использование облака может сэкономить деньги, поскольку вы можете вернуть неиспользуемые ресурсы провайдеру, вместо того чтобы оставлять их без дела. Для меньших наборов данных эта разница менее значительна.</p>
<p><strong>Наибольший недостаток облачного сервиса в том, что вы не имеете над ним контроля:</strong></p>
<ul>
<li>Если не хватает нужной вам функции, всё, что вы можете сделать — это вежливо попросить поставщика её добавить; как правило, вы не можете реализовать её сами.</li>
<li>Если сервис выходит из строя, вы можете только ждать его восстановления.</li>
<li>Если вы используете сервис таким образом, что это вызывает баг или проблемы с производительностью, вам будет сложно диагностировать проблему. В случае программного обеспечения, которое вы запускаете самостоятельно, вы можете получить метрики производительности и отладочную информацию от операционной системы, чтобы понять поведение, и можете просматривать журналы сервера, но при использовании сервиса у поставщика вы обычно не имеете доступа к этим внутренностям.</li>
<li>Кроме того, если сервис прекращает работу или становится неприемлемо дорогим, или если поставщик решает изменить продукт так, как вам не нравится, вы в его власти — продолжать использовать старую версию ПО обычно нельзя, и вам придётся переходить на альтернативный сервис. Этот риск уменьшается, если существуют альтернативные сервисы с совместимым API, но для многих облачных сервисов стандартные API отсутствуют, что повышает стоимость перехода и делает проблему привязки к поставщику (vendor lock-in) актуальной.</li>
<li>Облачному провайдеру необходимо доверять хранение данных в безопасности, что может усложнить процесс соблюдения требований по конфиденциальности и безопасности.</li>
</ul>
<p>Несмотря на все эти риски, всё чаще организации предпочитают строить новые приложения на базе облачных сервисов или используют гибридный подход, при котором облако используется для некоторых компонентов системы. Однако облачные сервисы не вытеснят все локальные системы: многие старые системы появились до облака, и для сервисов со специфическими требованиями, которые существующие облачные решения не могут удовлетворить, локальные системы остаются необходимыми. Например, приложения с очень жёсткими требованиями к задержке, такие как высокочастотная торговля, требуют полного контроля над оборудованием.</p>
<h2>Архитектура облачно-ориентированных систем</h2>
<p>Помимо отличий в экономической модели (подписка на сервис вместо покупки оборудования и лицензирования ПО), распространение облака также оказало глубокое влияние на то, как информационные системы реализуются на техническом уровне. Термин облачно-ориентированный (cloud-native) используется для описания архитектуры, изначально спроектированной для использования преимуществ облачных сервисов.</p>
<p>В принципе, почти любое ПО, которое вы можете развернуть самостоятельно, также может быть предоставлено как облачный сервис, и действительно, такие управляемые сервисы сейчас доступны для многих популярных информационных систем. Однако системы, спроектированные с нуля как облачно-ориентированные, показали несколько преимуществ: лучшую производительность на том же оборудовании, более быстрое восстановление после сбоев, возможность быстро масштабировать вычислительные ресурсы в соответствии с нагрузкой и поддержку больших объёмов данных. В таблице 1-2 приведены примеры систем обоих типов.</p>
<p><strong>Таблица 1-2. Примеры самостоятельно развёртываемых и облачно-ориентированных СУБД</strong></p>
<table>
<thead>
<tr>
<th>Категория</th>
<th>Самостоятельное размещение</th>
<th>Облачно-ориентированные системы</th>
</tr>
</thead>
<tbody>
<tr>
<td>Операционные / OLTP</td>
<td>MySQL, PostgreSQL, MongoDB</td>
<td>AWS Aurora [25], Azure SQL DB Hyperscale [26], Google Cloud Spanner</td>
</tr>
<tr>
<td>Аналитические / OLAP</td>
<td>Teradata, ClickHouse, Spark</td>
<td>Snowflake [27], Google BigQuery, Azure Synapse Analytics</td>
</tr>
</tbody>
</table>
<h2>Слоистость облачных сервисов (Layering of cloud services)</h2>
<p>У многих самостоятельно развёртываемых информационных систем очень простые системные требования: они работают на обычной операционной системе, такой как Linux или Windows, хранят свои данные в виде файлов в файловой системе и обмениваются данными через стандартные сетевые протоколы, такие как <strong>TCP/IP</strong>. Некоторые системы зависят от специализированного оборудования, такого как графические процессоры (для машинного обучения) или сетевые интерфейсы с <strong>RDMA</strong>, но в целом самостоятельно развёртываемое программное обеспечение, как правило, использует очень универсальные вычислительные ресурсы: <strong>CPU</strong>, <strong>RAM</strong>, <strong>файловую систему</strong> и <strong>IP-сеть</strong>.</p>
<p>В облаке такой тип программного обеспечения может работать в среде Infrastructure-as-a-Service, используя одну или несколько виртуальных машин (или инстансов) с определённым объёмом CPU, памяти, диска и пропускной способности сети. По сравнению с физическими машинами, облачные инстансы можно быстрее задействовать, и они доступны в большем разнообразии размеров, но в остальном они схожи с традиционным компьютером: вы можете запускать на нём любое программное обеспечение, но сами отвечаете за его администрирование.</p>
<p>В противоположность этому, <strong>ключевая идея облачно-ориентированных сервисов</strong> — это использовать не только вычислительные ресурсы, управляемые вашей операционной системой, но и строить более высокоуровневые сервисы на базе низкоуровневых облачных сервисов. Например:</p>
<p><strong>Сервисы объектного хранения</strong>, такие как Amazon S3, Azure Blob Storage и Cloudflare R2, хранят большие файлы. Они предоставляют более ограниченные API по сравнению с типичной файловой системой (основные операции чтения и записи файлов), но имеют то преимущество, что скрывают под собой физические машины: сервис автоматически распределяет данные по множеству машин, так что вам не нужно беспокоиться о том, что на какой-то машине закончится место на диске. Даже если какие-то машины или их диски полностью выйдут из строя, данные не будут потеряны.</p>
<p>Многие другие сервисы, в свою очередь, построены на объектном хранилище и других облачных сервисах: например, <strong>Snowflake</strong> — это облачная аналитическая база данных (хранилище данных), которая использует S3 для хранения данных, а некоторые другие сервисы, в свою очередь, строятся на базе Snowflake.</p>
<p>Как всегда с абстракциями в вычислениях, не существует единственно правильного ответа на вопрос, что именно следует использовать. В общем случае, чем выше уровень абстракции, тем больше он ориентирован на конкретные сценарии использования. Если ваши потребности совпадают с теми, для которых предназначена высокоуровневая система, использование готовой высокоуровневой системы, скорее всего, даст вам то, что нужно, с гораздо меньшими трудозатратами, чем построение её с нуля на базе низкоуровневых компонентов. С другой стороны, если не существует высокоуровневой системы, удовлетворяющей ваши потребности, то построение её самостоятельно из низкоуровневых компонентов остаётся единственным вариантом.</p>
<h2>Разделение хранилища и вычислений (storage and compute)</h2>
<p>В традиционных вычислениях дисковое хранилище считается надёжным (мы предполагаем, что после записи на диск данные не будут потеряны); для устойчивости к отказу отдельного жёсткого диска часто используется <strong>RAID</strong>, чтобы поддерживать копии данных на нескольких дисках. В облаке вычислительные инстансы (виртуальные машины) также могут иметь подключённые локальные диски, но облачно-ориентированные системы, как правило, рассматривают эти диски скорее как эфемерный кэш, а не как долгосрочное хранилище. Это связано с тем, что локальный диск становится недоступным, если связанный с ним инстанс выходит из строя или заменяется на больший или меньший (на другой физической машине) для адаптации к изменению нагрузки.</p>
<p>В качестве альтернативы локальным дискам облачные сервисы также предлагают виртуальное дисковое хранилище, которое можно отсоединить от одного инстанса и подключить к другому (Amazon EBS, управляемые диски в Azure и постоянные диски в Google Cloud). Такой виртуальный диск на самом деле не является физическим диском, а представляет собой облачный сервис, предоставляемый отдельным набором машин, который эмулирует поведение диска (блочного устройства, где каждый блок обычно имеет размер 4 КиБ). Эта технология позволяет запускать традиционное программное обеспечение, ориентированное на диски, в облаке, но эмуляция блочного устройства создаёт накладные расходы, которых можно избежать в системах, спроектированных с нуля для облака. Она также делает приложение очень чувствительным к сетевым сбоям, поскольку каждый ввод-вывод на виртуальном блочном устройстве на самом деле является сетевым вызовом.</p>
<p>Чтобы решить эту проблему, облачно-ориентированные сервисы, как правило, избегают использования виртуальных дисков и вместо этого строятся на специализированных хранилищах, оптимизированных под конкретные типы нагрузки. Сервисы объектного хранения, такие как S3, предназначены для долговременного хранения довольно больших файлов — от сотен килобайт до нескольких гигабайт. Отдельные строки или значения, хранящиеся в базе данных, как правило, намного меньше; поэтому облачные базы данных обычно управляют меньшими значениями в отдельном сервисе, а более крупные блоки данных (содержащие множество отдельных значений) хранят в объектном хранилище. Мы увидим способы реализации этого в главе 4.</p>
<p>В традиционной архитектуре систем один и тот же компьютер отвечает как за хранение (диск), так и за вычисления (CPU и RAM), но в облачно-ориентированных системах эти две функции в некоторой степени разделены или дезагрегированы: например, S3 просто хранит файлы, и если вы хотите проанализировать эти данные, вам нужно будет запускать код анализа где-то вне S3. Это подразумевает передачу данных по сети, о чём мы поговорим далее в разделе “Распределённые и одновузловые системы”.</p>
<p>Кроме того, облачно-ориентированные системы часто являются мультитенантными, что означает, что вместо выделенной машины для каждого клиента данные и вычисления от нескольких разных клиентов обрабатываются на одном и том же общем оборудовании одним и тем же сервисом. Мультитенантность может обеспечить более эффективное использование оборудования, более лёгкую масштабируемость и более простое управление со стороны облачного провайдера, но также требует тщательной инженерной проработки, чтобы деятельность одного клиента не влияла на производительность или безопасность системы для других клиентов.</p>
<h2>Операции в эпоху облаков</h2>
<p>Традиционно люди, управлявшие серверной частью инфраструктуры данных организации, назывались <strong>администраторами баз данных (DBA)</strong> или системными администраторами (системные админы). В последнее время многие организации стремятся интегрировать роли разработки программного обеспечения и эксплуатации в команды, разделяющие ответственность как за серверные сервисы, так и за инфраструктуру данных; этой тенденцией руководствуется философия <strong>DevOps</strong>. <strong>Инженеры надёжности сайтов (Site Reliability Engineers, SRE)</strong> — это реализация этой идеи в Google.</p>
<p>Роль операций заключается в обеспечении надёжной доставки сервисов пользователям (включая настройку инфраструктуры и развёртывание приложений), а также в обеспечении стабильной производственной среды (включая мониторинг и диагностику любых проблем, которые могут повлиять на надёжность). Для самостоятельно развёрнутых систем операции традиционно включают значительное количество работы на уровне отдельных машин, например планирование ресурсов (например, мониторинг доступного места на диске и добавление новых дисков до того, как место закончится), подготовку новых машин, перенос сервисов с одной машины на другую и установку обновлений операционной системы.</p>
<p>Многие облачные сервисы представляют <strong>API</strong>, скрывающий отдельные машины, которые фактически реализуют сервис. Например, облачное хранилище заменяет диски фиксированного размера на тарификацию по объёму, где вы можете хранить данные без предварительного планирования потребностей в объёме, а затем оплачиваете только фактически используемое пространство. Кроме того, многие облачные сервисы остаются высокодоступными даже при выходе из строя отдельных машин (см. «Надёжность и устойчивость к сбоям»).</p>
<p>Этот сдвиг акцента с отдельных машин на сервисы сопровождается изменением роли операций. Высокоуровневая цель — обеспечение надёжной работы сервиса — остаётся той же, но процессы и инструменты эволюционируют. Философия <strong>DevOps/SRE</strong> делает больший упор на:</p>
<ul>
<li>автоматизацию — предпочтение повторяемым процессам вместо одноразовых ручных действий,</li>
<li>предпочтение эфемерным виртуальным машинам и сервисам вместо долго работающих серверов,</li>
<li>обеспечение частых обновлений приложений,</li>
<li>обучение на инцидентах и</li>
<li>сохранение знаний об устройстве системы внутри организации, даже когда сотрудники приходят и уходят.</li>
</ul>
<p>С ростом числа облачных сервисов произошло расщепление ролей: команды операций в инфраструктурных компаниях специализируются на деталях предоставления надёжного сервиса большому числу клиентов, тогда как сами клиенты стремятся тратить как можно меньше времени и усилий на инфраструктуру.</p>
<p>Клиентам облачных сервисов по-прежнему нужны операции, но акцент делается на других аспектах, таких как выбор наиболее подходящего сервиса для конкретной задачи, интеграция различных сервисов друг с другом и миграция с одного сервиса на другой. Хотя тарификация по объёму снимает необходимость в традиционном планировании ресурсов, всё же важно знать, какие ресурсы используются для каких целей, чтобы не тратить деньги на ненужные облачные ресурсы: планирование ресурсов становится финансовым планированием, а оптимизация производительности — оптимизацией затрат. Более того, облачные сервисы действительно имеют ограничения ресурсов или квоты (например, максимальное количество одновременно работающих процессов), о которых необходимо знать и планировать заранее, чтобы не столкнуться с проблемами.</p>
<p>Переход на облачные сервисы может быть проще и быстрее, чем развёртывание собственной инфраструктуры, хотя и здесь есть издержки на обучение работе с сервисом и, возможно, обход его ограничений. Интеграция между различными сервисами становится особенно сложной задачей, поскольку всё больше поставщиков предлагают всё более широкий спектр облачных сервисов, нацеленных на разные сценарии использования. <strong>ETL</strong> (см. «Хранилище данных») — это лишь часть картины; операционные облачные сервисы также необходимо интегрировать между собой. В настоящее время отсутствуют стандарты, которые бы способствовали такого рода интеграции, поэтому она часто требует значительных ручных усилий.</p>
<p>Другие аспекты операций, которые нельзя полностью передать на аутсорсинг облачным сервисам, включают в себя обеспечение безопасности приложения и используемых им библиотек, управление взаимодействиями между вашими собственными сервисами, мониторинг нагрузки на сервисы и поиск причин проблем, таких как снижение производительности или сбои. Хотя облако меняет роль операций, необходимость в них остаётся столь же актуальной, как и прежде.</p>
<h2>Распределённые и одно-узловые системы (Distributed versus Single-Node Systems)</h2>
<p>Система, включающая несколько машин, обменивающихся данными через сеть, называется распределённой системой. Каждый из процессов, участвующих в распределённой системе, называется узлом. Существуют различные причины, по которым может понадобиться распределённая система:</p>
<p><strong>Изначально распределённые системы</strong><br />
Если приложение включает двух или более взаимодействующих пользователей, каждый из которых использует своё собственное устройство, то система неизбежно становится распределённой: передача данных между устройствами должна происходить по сети.</p>
<p><strong>Запросы между облачными сервисами</strong><br />
Если данные хранятся в одном сервисе, а обрабатываются в другом, их необходимо передать по сети из одного сервиса в другой.</p>
<p><strong>Устойчивость к сбоям / высокая доступность</strong><br />
Если ваше приложение должно продолжать работать даже при отказе одной (или нескольких) машин, сети или даже целого дата-центра, можно использовать несколько машин для обеспечения избыточности. Когда одна выходит из строя, другая может её заменить. См. «Надёжность и устойчивость к сбоям» и главу 6 о репликации.</p>
<p><strong>Масштабируемость</strong><br />
Если объём данных или вычислительные потребности превышают возможности одной машины, можно распределить нагрузку между несколькими машинами. См. «Масштабируемость».</p>
<p><strong>Задержка</strong><br />
Если у вас есть пользователи по всему миру, вы можете разместить серверы в разных регионах, чтобы каждый пользователь обслуживался сервером, географически близким к нему. Это позволяет избежать ожидания, пока сетевые пакеты пересекут половину земного шара, чтобы ответить на запрос. См. «Описания производительности».</p>
<p><strong>Эластичность</strong><br />
Если ваше приложение в какие-то моменты активно, а в другие — простаивает, облачное развёртывание может масштабироваться вверх или вниз в зависимости от спроса, так что вы платите только за используемые ресурсы. Это сложнее реализовать на одной машине, которую нужно заранее подготовить к максимальной нагрузке, даже если большую часть времени она используется минимально.</p>
<p><strong>Использование специализированного оборудования</strong><br />
Разные части системы могут использовать различные типы оборудования в зависимости от нагрузки. Например, объектное хранилище может использовать машины с большим количеством дисков, но с малым числом процессоров, а система анализа данных — машины с большим количеством CPU и памяти, но без дисков, а система машинного обучения — машины с GPU (которые намного эффективнее CPU при обучении нейронных сетей и других задачах машинного обучения).</p>
<p><strong>Юридическое соответствие</strong><br />
Некоторые страны имеют законы о размещении данных, требующие, чтобы данные о людях в их юрисдикции хранились и обрабатывались на территории этой страны. Объём действия этих правил различается — например, в одних случаях они применяются только к медицинским или финансовым данным, а в других — более широко. Сервис с пользователями из нескольких таких юрисдикций, следовательно, должен распределить свои данные по серверам, находящимся в разных местах.</p>
<p>Эти причины применимы как к сервисам, которые вы разрабатываете сами (прикладной код), так и к сервисам, основанным на готовом программном обеспечении (например, базы данных).</p>
<h2>Проблемы распределённых систем</h2>
<p>У распределённых систем есть и недостатки. Каждый запрос и вызов API, проходящий по сети, должен учитывать возможность сбоя: соединение может прерваться, сервис может быть перегружен или упасть, и в результате запрос может завершиться по тайм-ауту без получения ответа. В этом случае мы не знаем, получил ли сервис запрос, и простая повторная отправка может быть небезопасной.</p>
<p>Хотя сети в дата-центрах быстрые, вызов другого сервиса всё равно значительно медленнее, чем вызов функции в одном процессе. При работе с большими объёмами данных вместо передачи данных с хранилища на отдельную машину для обработки может быть быстрее перенести вычисление на ту машину, где данные уже находятся. Большее количество узлов — не всегда быстрее: в некоторых случаях простая однопоточная программа на одном компьютере может работать значительно лучше, чем кластер с более чем 100 ядрами CPU.</p>
<p>Отладка распределённой системы часто бывает затруднена: если система отвечает медленно, как понять, где именно возникла проблема? Методы диагностики проблем в распределённых системах развиваются в рамках подхода под названием наблюдаемость (observability), который включает сбор данных о выполнении системы и возможность их анализа как на уровне высокоуровневых метрик, так и отдельных событий. Инструменты трассировки, такие как OpenTelemetry, Zipkin и Jaeger, позволяют отслеживать, какой клиент вызвал какой сервер и для какой операции, а также сколько времени занял каждый вызов.</p>
<p>Базы данных предоставляют различные механизмы обеспечения согласованности данных, как мы увидим в главах 6 и 8. Однако когда каждый сервис имеет свою собственную базу данных, поддержание согласованности данных между этими разными сервисами становится задачей приложения. Распределённые транзакции — это возможный способ обеспечения согласованности, но они редко применяются в контексте микросервисов, так как противоречат цели независимости сервисов, и многие базы данных их не поддерживают.</p>
<p>По всем этим причинам, если задачу можно выполнить на одной машине, это часто будет проще и дешевле, чем настройка распределённой системы. <strong>CPU</strong>, <strong>память</strong> и <strong>диски</strong> стали больше, быстрее и надёжнее. В сочетании с одноузловыми базами данных, такими как <strong>DuckDB</strong>, <strong>SQLite</strong> и <strong>KùzuDB</strong>, многие нагрузки теперь могут выполняться на одном узле. Мы подробнее рассмотрим эту тему в главе 4.</p>
<h2>Микросервисы и Serverless</h2>
<p><strong>Наиболее распространённый способ распределения системы между несколькими машинами</strong> — это разделение на клиенты и серверы, где клиенты отправляют запросы серверам. Обычно для такой коммуникации используется HTTP, как мы обсудим в разделе «Поток данных через сервисы: <strong>REST</strong> и <strong>RPC</strong>». Один и тот же процесс может быть как сервером (обрабатывая входящие запросы), так и клиентом (отправляя запросы другим сервисам).</p>
<p>Такой подход к построению приложений традиционно назывался <strong>сервис-ориентированной архитектурой (SOA)</strong>; в более новой трактовке это архитектура микросервисов. В такой архитектуре сервис выполняет одну чётко определённую функцию (например, в случае <strong>S3</strong> — это хранение файлов); каждый сервис предоставляет <strong>API</strong>, доступный по сети, и за каждый сервис отвечает одна команда. Сложное приложение можно таким образом разложить на множество взаимодействующих сервисов, каждый из которых управляется своей отдельной командой.</p>
<p>Разделение сложного ПО на несколько сервисов имеет ряд преимуществ: каждый сервис можно обновлять независимо, уменьшая необходимость координации между командами; каждому сервису можно выделить ресурсы, соответствующие его потребностям; а скрытие деталей реализации за API позволяет разработчикам менять реализацию без влияния на клиентов. Что касается хранения данных, то обычно у каждого сервиса своя база данных, и базы данных не разделяются между сервисами: совместное использование БД фактически делает её структуру частью API сервиса, а это затрудняет её изменение. Кроме того, совместное использование базы может привести к тому, что запросы одного сервиса негативно повлияют на производительность других.</p>
<p>С другой стороны, большое количество сервисов само по себе порождает сложность: каждый сервис требует инфраструктуры для развёртывания новых версий, настройки аппаратных ресурсов под нагрузку, сбора логов, мониторинга состояния и оповещения инженера по дежурству при возникновении проблем. Фреймворки оркестрации, такие как Kubernetes, стали популярным способом развёртывания сервисов, поскольку предоставляют основу для этой инфраструктуры. Тестирование сервиса во время разработки также усложняется, поскольку нужно запускать и все другие сервисы, от которых он зависит.</p>
<p>API микросервисов бывает сложно развивать. Клиенты, вызывающие API, ожидают определённые поля. Разработчики могут захотеть добавить или удалить поля в API по мере изменения бизнес-требований, но это может привести к сбоям у клиентов. Хуже всего, что такие сбои часто обнаруживаются только на поздних стадиях разработки, когда обновлённый API развёртывается в staging или production. Стандарты описания API, такие как <strong>OpenAPI</strong> и <strong>gRPC</strong>, помогают управлять отношениями между клиентами и серверами; мы рассмотрим их подробнее в главе 5.</p>
<p><strong>Микросервисы</strong> — это прежде всего техническое решение организационной проблемы: они позволяют разным командам двигаться вперёд независимо друг от друга без постоянной координации. Это особенно полезно в больших компаниях, но в маленькой компании с небольшим количеством команд микросервисы скорее будут избыточной нагрузкой, и проще реализовать приложение максимально просто.</p>
<p><strong>Serverless, или функция как сервис (FaaS)</strong> — это другой подход к развёртыванию сервисов, при котором управление инфраструктурой передаётся облачному провайдеру. При использовании виртуальных машин вы сами определяете, когда запустить или остановить экземпляр; в serverless-модели провайдер автоматически выделяет и освобождает ресурсы по мере поступления запросов к вашему сервису. Такой подход снимает часть операционной нагрузки с пользователя и предлагает гибкую модель оплаты по факту использования, а не за зарезервированные машины. Чтобы обеспечить такие преимущества, провайдеры serverless-инфраструктур часто накладывают ограничения на длительность выполнения функций, доступные среды исполнения и могут страдать от «холодного старта», когда функция вызывается впервые. Термин serverless может быть немного вводящим в заблуждение: каждая функция всё равно запускается на сервере, просто разные вызовы могут быть выполнены на разных серверах. Более того, инфраструктуры вроде BigQuery и различные предложения Kafka тоже используют термин serverless, чтобы подчеркнуть автомасштабирование и оплату за фактическое использование, а не за выделенные серверы.</p>
<p>Точно так же как облачное хранилище заменило планирование ёмкости (заранее определяя, сколько дисков купить) моделью оплаты по объёму, serverless подводит к модели оплаты за фактическое выполнение кода: вы платите только за то время, когда ваш код реально работает, без необходимости заранее резервировать ресурсы.</p>
<h2>Облачные вычисления против суперкомпьютеров</h2>
<p><strong>Облачные вычисления</strong> — не единственный способ построения крупномасштабных вычислительных систем; альтернативой являются вычисления высокой производительности (HPC, High-Performance Computing), также известные как суперкомпьютеры. Хотя между ними есть пересечения, HPC часто имеет другие приоритеты и использует иные подходы по сравнению с облачными вычислениями и корпоративными дата-центрами. Некоторые из этих различий:</p>
<p><strong>Суперкомпьютеры</strong>, как правило, используются для вычислительно сложных научных задач, таких как прогнозирование погоды, моделирование климата, молекулярная динамика (симуляция движения атомов и молекул), сложные задачи оптимизации и решение уравнений в частных производных. С другой стороны, облачные вычисления, как правило, используются для онлайн-сервисов, бизнес-систем и других систем, которым необходимо обслуживать пользовательские запросы с высокой доступностью.</p>
<p>Суперкомпьютер обычно запускает крупные пакетные задания, которые время от времени сохраняют состояние своей работы на диск (чекпоинтинг). Если узел выходит из строя, распространённым решением является остановка всей нагрузки кластера, ремонт неисправного узла и перезапуск вычислений с последнего чекпоинта. В случае с облачными сервисами, как правило, нежелательно останавливать весь кластер, поскольку сервисы должны продолжать обслуживать пользователей с минимальными перебоями.</p>
<p>Узлы суперкомпьютера, как правило, обмениваются данными через общую память и RDMA (доступ к удалённой памяти напрямую), что обеспечивает высокую пропускную способность и низкую задержку, но предполагает высокий уровень доверия между пользователями системы. В облачных вычислениях сеть и машины часто используются разными организациями, не доверяющими друг другу, что требует более надёжных механизмов безопасности, таких как изоляция ресурсов (например, виртуальные машины), шифрование и аутентификация.</p>
<p>Сети дата-центров облачных вычислений, как правило, построены на основе IP и Ethernet и организованы по топологиям Clos, обеспечивающим высокую пропускную способность между узлами — это один из ключевых показателей производительности сети. Суперкомпьютеры часто используют специализированные топологии сети, такие как многомерные сетки и торы, которые дают лучшие результаты для HPC-нагрузок с известными шаблонами обмена данными.</p>
<p>Облачные вычисления позволяют размещать узлы в разных географических регионах, тогда как суперкомпьютеры, как правило, предполагают, что все их узлы находятся рядом друг с другом.</p>
<p>Системы аналитики в крупном масштабе иногда имеют схожие черты с суперкомпьютерами, и поэтому бывает полезно знать о соответствующих подходах, если вы работаете в этой области. Однако эта книга в основном посвящена сервисам, которые должны быть постоянно доступны, как обсуждалось в разделе «Надёжность и устойчивость к сбоям».</p>
<h1>Системы работы с данными, закон и общество (Data Systems, Law, and Society)</h1>
<p>Как вы уже видели в этой главе, архитектура систем работы с данными определяется не только техническими целями и требованиями, но и потребностями людей и организаций, которые эти системы поддерживают. Всё чаще инженеры систем работы с данными понимают, что служить только интересам собственного бизнеса недостаточно: мы также несем ответственность перед обществом в целом.</p>
<p>Особую озабоченность вызывают системы, хранящие данные о людях и их поведении. С 2018 года <strong>Общий регламент по защите данных (GDPR)</strong> дал жителям многих европейских стран больше контроля и юридических прав в отношении их персональных данных, и подобные законы о конфиденциальности были приняты в ряде других стран и штатов, включая, например, <strong>Закон о конфиденциальности потребителей Калифорнии (CCPA)</strong>. Регулирование в области ИИ, такое как Закон об ИИ ЕС, вводит дополнительные ограничения на использование персональных данных.</p>
<p>Кроме того, даже в областях, не подпадающих напрямую под регулирование, всё чаще признаётся влияние компьютерных систем на людей и общество. Социальные сети изменили то, как люди получают новости, что влияет на их политические взгляды и, следовательно, может повлиять на исход выборов. Автоматизированные системы всё чаще принимают решения, оказывающие серьёзное влияние на людей, например, кому выдать кредит или страховку, кого пригласить на собеседование, или кого заподозрить в совершении преступления.</p>
<p>Каждый, кто работает над такими системами, несёт ответственность за этические последствия своей работы и за соблюдение применимого законодательства. Не обязательно быть экспертом в области права и этики, но базовое понимание правовых и этических принципов так же важно, как и, скажем, фундаментальные знания в области распределённых систем.</p>
<p>Юридические аспекты всё больше влияют на самые основы проектирования систем работы с данными. Например, GDPR предоставляет людям право требовать удаления своих данных по запросу (так называемое «право быть забытым»). Однако, как мы увидим в этой книге, многие системы работы с данными полагаются на неизменяемые конструкции, такие как журналы только для добавления. Как же тогда можно удалить данные в середине файла, который, по задумке, должен быть неизменяемым? Как мы можем удалить данные, которые были включены в производные наборы данных (см. «Системы записи и производные данные»), например, в обучающие данные для моделей машинного обучения? Ответы на эти вопросы создают новые инженерные задачи.</p>
<p>На данный момент нет чётких руководств, какие технологии или архитектуры считаются «соответствующими требованиям GDPR», а какие — нет. Регламент намеренно не предписывает конкретные технологии, поскольку они быстро меняются. Вместо этого в тексте закона изложены высокоуровневые принципы, подлежащие интерпретации. Это означает, что нет простых ответов на вопрос, как соблюдать законы о конфиденциальности, но мы рассмотрим некоторые технологии в этой книге с этой точки зрения.</p>
<p>Как правило, мы храним данные, потому что считаем, что их ценность превышает издержки на хранение. Однако стоит помнить, что затраты на хранение — это не только счёт за Amazon S3 или другой сервис: в расчёт следует включать также риски ответственности и репутационных потерь, если данные будут скомпрометированы или украдены, а также юридические издержки и штрафы, если окажется, что хранение и обработка данных не соответствуют закону.</p>
<p>Государственные органы или полиция также могут потребовать от компаний передать данные. Если есть риск, что данные могут указать на криминализированное поведение (например, гомосексуальность в ряде стран Ближнего Востока и Африки или обращение за абортом в нескольких штатах США), хранение таких данных создаёт реальные угрозы безопасности для пользователей. Поездка в клинику, делающую аборты, может быть легко раскрыта через данные о местоположении, возможно, даже через журнал IP-адресов пользователя (указывающий примерное местоположение во времени).</p>
<p>Учитывая все риски, разумным решением может быть признание того, что некоторые данные просто не стоит хранить, и их следует удалить. <strong>Этот принцип минимизации данных (иногда называемый немецким термином Datensparsamkeit)</strong> противоречит философии «больших данных», предполагающей хранение огромных объёмов данных на случай, если они окажутся полезными в будущем. Но он соответствует положениям GDPR, который требует, чтобы персональные данные собирались только для конкретной, явной цели, не использовались в дальнейшем для других целей, и не хранились дольше, чем это необходимо для выполнения заявленных целей.</p>
<p>Бизнес также начал учитывать вопросы конфиденциальности и безопасности. Компании, обрабатывающие платежи, обязаны соблюдать строгие <strong>стандарты индустрии платёжных карт (PCI)</strong>. Они проходят частые проверки сторонними аудиторами для подтверждения соответствия. Поставщики ПО также подвергаются всё большей проверке. Многие заказчики теперь требуют, чтобы поставщики соответствовали стандартам <strong>SOC (Service Organization Control)</strong> типа 2. Как и с PCI, поставщики проходят аудит третьих сторон для подтверждения соответствия.</p>
<p>В целом, важно сбалансировать потребности вашего бизнеса с потребностями людей, чьи данные вы собираете и обрабатываете.</p>
<h1>Краткая сводка по 1 главе</h1>
<p>Тема этой главы — <strong>понимание компромиссов</strong>: признание того, что в большинстве случаев нет одного единственного правильного решения, а есть несколько разных подходов с их плюсами и минусами. Мы рассмотрели ключевые выборы, влияющие на архитектуру систем работы с данными, и ввели терминологию, которая будет использоваться в дальнейшем.</p>
<p>Начали с различия между операционными (OLTP) и аналитическими (OLAP) системами, их особенностями, разными типами данных и разными пользователями. Познакомились с концепциями хранилища данных (data warehouse) и озера данных (data lake), которые получают данные из операционных систем через ETL. В следующей главе будет показано, что внутреннее устройство данных в этих системах сильно различается из-за разных типов запросов.</p>
<p>Сравнили облачные сервисы, сравнительно новую технологию, с традиционным подходом — собственным размещением ПО. Выбор более экономичного варианта зависит от конкретной ситуации, но облачные технологии меняют архитектуру систем, например, разделяя хранение и вычисления.</p>
<p>Облачные системы по своей природе распределённые, и мы кратко рассмотрели компромиссы распределённых систем по сравнению с использованием одной машины. Иногда распределённость неизбежна, но если можно обойтись одной машиной, лучше не спешить с распределением.</p>
<p>Наконец, архитектура систем определяется не только потребностями бизнеса, но и требованиями законодательства о конфиденциальности, которые защищают права людей, чьи данные обрабатываются. Как переводить юридические требования в технические решения — вопрос ещё не до конца изученный, но важный для дальнейшего изучения темы.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-1-trade-offs-in-data-systems-architecture/">Глава 1. Компромиссы в архитектуре систем данных</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-1-trade-offs-in-data-systems-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Перевод 7 Главы &#8212; Dimensional Modeling (Data Vault 2.0)</title>
		<link>https://datatalks.ru/chapter-7-data-vault-2-0-dimensional-modeling/</link>
					<comments>https://datatalks.ru/chapter-7-data-vault-2-0-dimensional-modeling/#comments</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Thu, 03 Jul 2025 09:06:35 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Data Vault 2.0]]></category>
		<category><![CDATA[Conformed Dimensions]]></category>
		<category><![CDATA[Dimension Design]]></category>
		<category><![CDATA[Dimensional Modeling]]></category>
		<category><![CDATA[Multiple Stars]]></category>
		<category><![CDATA[SCD]]></category>
		<category><![CDATA[Slowly Changing Dimensions]]></category>
		<category><![CDATA[Snowflake Design]]></category>
		<category><![CDATA[Многомерное моделирование]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1729</guid>

					<description><![CDATA[<p>Перевод книги &#171;Building a Scalable Data Warehouse with Data Vault 2.0&#187; подготовлен автором сайта ГЛАВА 7. Многомерное моделирование (Dimensional Modeling) Аннотация Моделирование Data Vault не является заменой Dimensional Modeling, которое является отраслевым стандартом для определения витрины данных (уровня, используемого для предоставления данных конечному пользователю). Поскольку данная книга предназначена для охвата всего процесса построения хранилища данных [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-7-data-vault-2-0-dimensional-modeling/">Перевод 7 Главы &#8212; Dimensional Modeling (Data Vault 2.0)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод книги &#171;Building a Scalable Data Warehouse with Data Vault 2.0&#187; подготовлен автором сайта</em></p>
<h1>ГЛАВА 7. Многомерное моделирование (Dimensional Modeling)</h1>
<h2>Аннотация</h2>
<p>Моделирование Data Vault не является заменой Dimensional Modeling, которое является отраслевым стандартом для определения витрины данных (уровня, используемого для предоставления данных конечному пользователю). Поскольку данная книга предназначена для охвата всего процесса построения хранилища данных от начала до конца, она также охватывает и Dimensional Modeling.</p>
<p>Первая часть этой главы сосредоточена на базовых сущностях <strong>Dimensional Modeling</strong> — фактовых сущностях и размерных сущностях.</p>
<ul>
<li><strong>Fact Entities (Сущности фактов)</strong> &#8212; хранят измерения, метрики или факты бизнес-процесса (иными словами содержат числовые показатели бизнес-операций &#8212; факты и ключи к соответствующим таблицам размерностей).</li>
<li><strong>Dimension Entities (Cущности измерений)</strong> &#8212; содержат описательные атрибуты для фактов.</li>
</ul>
<p>Будут рассмотрены различные типы размерностей, такие как медленно изменяющиеся измерения, а также способы выполнения запросов к данным из многомерной модели. Вторая часть этой главы объясняет, как работать с несколькими звёздными схемами, особенно с использованием согласованных измерений. Последняя часть сосредоточена на техниках многомерного проектирования и представляет снежинку и схему &#171;Звезда&#187; как частный случай схемы снежинки.</p>
<p><strong>Ключевые слова</strong></p>
<ul>
<li>Dimension modeling (Многомерное моделирование)</li>
<li>Fact Entities (Фактовые сущности)</li>
<li>Техники проектирования</li>
<li>Snowflake Schemas (Схема &#171;Снежинка&#187;)</li>
<li>Star Schemas (Схема &#171;Звезда&#187;)</li>
</ul>
<p>Наилучшее применение <strong>моделирования Data Vault 2.0</strong> — это уровень корпоративного хранилища данных. Оно было специально разработано для этой цели и является оптимальным выбором, когда требуется расширяемая, функционально-ориентированная модель, позволяющая отслеживать историю и проводить аудит, а также интегрируемая в среды реального времени и NoSQL.</p>
<p>Однако большинство бизнес-пользователей не знакомы с моделированием Data Vault 2.0. Во многих случаях конечным пользователям сначала требуется надлежащее обучение, чтобы они могли напрямую обращаться к <strong>Raw Data Vault</strong> или <strong>Business Vault</strong>, которые моделируются по тем же принципам. Им также необходимо понимать, как соединять многочисленные сущности, чтобы получать ценные и пригодные для использования сырые данные, которые могут быть обработаны в полезную информацию. Поэтому прямой доступ к уровню корпоративного хранилища данных ограничен опытными пользователями, которые хотят использовать собственные запросы и которым нужны сырые данные для этих целей. Большинство же конечных пользователей будут использовать информационные витрины для доступа к подготовленной информации, которую они могут напрямую использовать для выполнения своих задач.</p>
<p>Ещё одна проблема заключается в том, что большинство фронтенд-инструментов, используемых бизнесом для анализа информации, предоставляемой хранилищем данных, не могут напрямую использовать структуры Data Vault. Например, построение <strong>OLAP-куба в Microsoft SQL Server Analysis Services (SSAS)</strong> лучше всего работает, если источник данных смоделирован в виде звёздной схемы, которая является реляционной версией Dimension модели.</p>
<p>Многомерное моделирование было представлено широкой аудитории в индустрии хранилищ данных Ральфом Кимбаллом в 1997 году. Однако он не был его изобретателем. Термины &#171;размерности&#187; и &#171;факты&#187;, которые являются основными конструкциями в многомерном моделировании, восходят к 1960-м годам, когда проводился совместный исследовательский проект между Дартмутским университетом и General Mills. Первая размерная модель (dimension model) была представлена компаниями AC Nielsen и IRI для описания ранних многомерных витрин данных для данных о розничных продажах. Но именно серия книг Ральфа Кимбалла способствовала популяризации размерного/многомерного моделирования в индустрии хранилищ данных, и оно стало стандартом моделирования. В результате многомерное моделирование поддерживается многими фронтенд-инструментами сегодня. Поэтому оно хорошо известно и понятно конечным пользователям и является оптимальным выбором для моделирования информационных витрин, которые служат в качестве фронтенд-слоя.</p>
<h1>Введение</h1>
<p><strong>Системы хранилищ данных</strong> поддерживают бизнес, помогая решать аналитические задачи — то есть, отвечать на вопросы, касающиеся бизнес-процессов. Например, бизнес-пользователи в авиапромышленности могут захотеть проанализировать следующие вопросы:</p>
<ul>
<li>Как изменялась степень загрузки на определённых рейсах за последний финансовый год?</li>
<li>Какова эффективность нашей бонусной программы для часто летающих пассажиров в отношении продаж?</li>
<li>Кто наши часто летающие пассажиры? Каков их средний объём продаж?</li>
</ul>
<p>Такие вопросы не касаются отдельных транзакций, а связаны с <strong>метриками (measurement)</strong> по всему процессу. Обычно для этого метрики по отдельным транзакциям агрегируются (например, суммируются или усредняются). Ещё одной характеристикой аналитических запросов является то, что не требуется модификация данных в операционных системах для получения ответов.</p>
<p>По всем этим причинам имеет смысл организовать данные в слое информационных витрин (information mart) таким образом, чтобы бизнес-пользователи могли быстро агрегировать информацию, а не быстро её модифицировать. Именно поэтому <strong>размерная модель (dimensional model)</strong> проектируется с учётом того, как измеряются данные. Поскольку <strong>метрики (measurement)</strong> часто основаны на бизнес-процессе, сам бизнес-процесс должен быть центром моделирования. Кроме того, необходим контекст, чтобы придать метрикам смысл, например, чтобы упорядочить метрики по аэропортам, как показано в Таблице 7.1.</p>
<p><strong>Таблица 7.1 Пропорция пассажиров, работников, посетителей и провожающих/встречающих в выбранных аэропортах (с использованием искусственных данных)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/table_7_1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1780" src="https://datatalks.ru/wp-content/uploads/2025/07/table_7_1.png" alt="" width="807" height="689" srcset="https://datatalks.ru/wp-content/uploads/2025/07/table_7_1.png 807w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_1-300x256.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_1-768x656.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_1-450x384.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_1-780x666.png 780w" sizes="(max-width: 807px) 100vw, 807px" /></a></p>
<p>В этой таблице представлено одна метрика — число лиц, которые были либо пассажирами, либо провожающими/встречающими, либо работниками, либо посетителями аэропорта, выраженное в виде соотношения между этими четырьмя ролями. Четыре роли — это первое измерение (dimension), а аэропорт — второе. Обе размерности охватывают данные и определяют уровень детализации для подсчёта лиц.</p>
<p>Эти две фундаментальные концепции — <strong>метрики (measurements)</strong> и <strong>контекст (context)</strong> &#8212; являются основой размерного моделирования.</p>
<h1>Схема &#171;Звезда&#187; (Star Schemas)</h1>
<p>Когда размерная модель реализована в реляционной базе данных, она называется <strong>звёздной схемой</strong>. Это объясняется тем, что размерная модель реляционных таблиц при взгляде сверху выглядит как звезда (см. Рисунок 7.1).</p>
<p><strong>Рисунок 7.1 Пример схемы &#171;Звезда&#187; для посещений аэропортов (физическая реализация)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1787" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema.png" alt="" width="1027" height="798" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema.png 1027w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema-300x233.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema-1024x796.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema-768x597.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema-450x350.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema-780x606.png 780w" sizes="(max-width: 1027px) 100vw, 1027px" /></a></p>
<p>Исходный рисунок (в плохом качестве)</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1789" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial.png" alt="" width="881" height="943" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial.png 881w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial-280x300.png 280w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial-768x822.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial-450x482.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_1_star_schema_initial-780x835.png 780w" sizes="(max-width: 881px) 100vw, 881px" /></a></p>
<p>Модель на Рисунке 7.1 представляет модель, лежащую в основе данных Таблицы 7.1. В центре модели находится таблица фактов, окружённая четырьмя таблицами измерений.</p>
<p>Основное отличие схемы &#171;Звезда&#187; на Рисунке 7.1 от нормализованной модели в третьей нормальной форме (3NF), которая часто используется в операционных системах, заключается в том, что размерная модель обычно использует денормализованные таблицы, особенно:</p>
<ul>
<li><strong>Таблицы Фактов:</strong> таблица в центре этой схемы содержит метрики/меры/показатели бизнеса, которые в случае Рисунка 7.1 представляют собой транзакции полётов. Эти measurements, как правило, являются метриками, которые бизнес желает проанализировать.</li>
<li><strong>Таблицы Измерений:</strong> таблицы размерностей группируют факты по категориям и предоставляют дополнительные описательные атрибуты к этим категориям. Эти категории называются размерностями и помогают конечному пользователю ориентироваться в модели и выбирать только нужное подмножество данных для анализа.</li>
</ul>
<p>В дополнение к этим основным таблицам, в размерном моделировании часто используются и другие типы таблиц, например, <strong>связующие таблицы (bridge tables)</strong>. В большинстве случаев можно создать размерную модель, которая содержит тот же контент, что и модель в 3NF. Однако, будучи представленной как звёздная схема, она легче воспринимается бизнес-пользователями и оптимизирована для выполнения запросов.</p>
<p>Это связано с денормализацией, которая обычно происходит при преобразовании модели 3NF в размерную модель. Иерархии и справочники предобъединены, что требует от оптимизатора СУБД учитывать меньшее количество объединений и временных таблиц. Также становится проще агрегировать данные, поскольку все измерения уже содержатся в центральной таблице фактов. Некоторые редакции Microsoft SQL Server также поддерживают <strong>оптимизацию звёздного объединения (star join optimization)</strong>, функцию, которая значительно сокращает объём данных, считываемых с диска для ответа на запросы.</p>
<p>Следующие разделы более подробно объясняют основные сущности размерного моделирования и звёздных схем.</p>
<h2>Таблицы фактов (Fact Tables)</h2>
<p><strong>Таблицы Фактов</strong> содержат информацию о конкретных бизнес-процессах или событиях в рамках этих процессов. Примерами таких бизнес-процессов и событий являются рейсы, заказы, телефонные звонки или обращения к веб-сайту. Каждая запись в таблице фактов представляет одно из этих событий и предоставляет метрики, связанные с этим бизнес-событием. Обычно это числовые значения, которые количественно выражают, например, продолжительность рейса, время до взлета, количество отклонений от маршрута или сколько товаров было заказано и по какой цене. Эти значения называются фактами таблицы.</p>
<p>В других случаях таблицы фактов могут содержать отношения между бизнес-объектами, например, уровни запасов на определённые даты. Эти уровни запасов являются формой отношений, потому что они показывают, какой продукт доступен в каком складском расположении в определённый момент времени. Каждое такое отношение представлено в таблице фактов одной записью. На рисунке 7.2 показан пример таблицы фактов, представляющей рейс.</p>
<p><strong>Рисунок 7.2 Таблица фактов для рейсов (физическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_2.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1791" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_2.jpeg" alt="" width="358" height="675" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_2.jpeg 358w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_2-159x300.jpeg 159w" sizes="(max-width: 358px) 100vw, 358px" /></a></p>
<p><strong>Таблица фактов содержит два разных типа столбцов:</strong></p>
<ul>
<li>Во-первых,<strong> внешние ключи на таблицы измерений,</strong> которые рассматриваются в следующем разделе. Сюда входят все четыре ссылки, которые являются частью составного первичного ключа, а также <code>TailNumberKey</code>, <code>CancelledKey</code> и <code>DivertedKey</code>.</li>
<li>Во-вторых, <strong>значения измерений, сами факты.</strong> Многие из них указывают продолжительность задержек, например, <code>DepartureDelay</code>, <code>WeatherDelay</code> или <code>SecurityDelay</code>. Другие показывают продолжительность подпроцессов, например, время руления при въезде и выезде, время в полёте и пройденное расстояние.</li>
</ul>
<p>Не все факты являются числовыми значениями, но именно числовые наиболее полезны для бизнес-пользователей, потому что их легко агрегировать по размерностям. Таблица фактов идентифицируется подмножеством внешних ключей к таблицам измерений. Это подмножество становится составным ключом, который представляет собой первичный ключ таблицы. Не все внешние ключи обязательно должны быть включены в первичный ключ, потому что во многих случаях размерность лишь описывает факт, но не идентифицирует его. В других случаях может потребоваться добавить ещё один идентифицирующий факт в первичный ключ фактовой таблицы, чтобы гарантировать уникальность, например, номер транзакции, такой как номер счёта или код бронирования.</p>
<p>Ввод ключей для уникальной идентификации строк в качестве первичного ключа таблицы фактов следует избегать, из-за необходимого объёма хранения для хранения значений ключей, а также из-за необходимости поддержки индекса. Сам индекс бесполезен, так как он не может быть использован конечным пользователем для идентификации отдельных фактов. Только в некоторых ограниченных случаях, в основном по техническим причинам, сгенерированный или производный ключ, используемый как первичный ключ, действительно имеет смысл.</p>
<p>Примером такой причины может быть требование со стороны бизнеса загружать идентичные строки в фактовую таблицу (например, при работе с историей) или необходимость перекрёстных ссылок с другими фактовыми таблицами.</p>
<h3><strong>Гранулярность таблицы фактов (Grain of a Fact Table)</strong></h3>
<p>Количество ссылок на измерения в таблице фактов определяет уровень детализации таблицы фактов. Этот уровень детализации называется <strong>гранулярностью (Grain)</strong>.</p>
<p>Хорошей практикой является сохранение уровня детализации как можно ниже, в идеале — на самом низком уровне, который предоставляет источник данных. Этот исходный уровень детализации также известен как атомарный уровень. Предоставление таких атомарных таблиц фактов бизнес-пользователям обеспечивает им наибольшую гибкость, потому что они могут агрегировать данные самостоятельно, исключая ненужные измерения из запроса и группируя данные по оставшимся измерениям.</p>
<p>Такой подход называется <strong>свёрткой (roll-up)</strong> и используется для обобщения данных по <strong>подмножеству измерений факта (subset of fact dimensions)</strong>. Однако, хранение таблиц фактов на самом низком уровне гранулярности не всегда осуществимо, если серверная инфраструктура не предоставляет достаточных ресурсов для хранения самого низкого уровня детализации. В этом случае предварительная агрегация данных до более высокого уровня детализации обеспечивает более быстрые ответы бизнес-пользователям при прямом запросе к таблицам фактов. Обратите внимание, что это утверждение уже не актуально при использовании многомерных OLAP-кубов, потому что они агрегируют данные внутри куба прозрачно для конечного пользователя.</p>
<h2>Таблицы размерностей/измерений (Dimension Tables)</h2>
<p><strong>Измерения/Размерности/Dimensions (а значит, и реляционные таблицы размерностей) предоставляют контекст для фактов.</strong> Они очень важны для понимания хранилища данных. Без измерений (dimensions) невозможно было бы понять метрики, предоставляемые таблицей фактов, потому что все метки и прочая описательная информация берутся из таблиц измерений. Как уже объяснялось в предыдущем разделе, они также используются для определения, как факты будут агрегироваться (сворачиваться). Кроме того, они могут использоваться для фильтрации фактов в соответствии с записью dimension или одним из её описательных атрибутов. Также возможно сортировать агрегированные метрики по измерению или одному из её атрибутов. На рисунке 7.3 показан пример таблицы размерности.</p>
<p><strong>Рисунок 7.3 Таблица измерений пассажиров (физическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_3.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1793" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_3.jpeg" alt="" width="245" height="470" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_3.jpeg 245w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_3-156x300.jpeg 156w" sizes="(max-width: 245px) 100vw, 245px" /></a></p>
<p>В отличие от таблиц фактов, таблица измерения использует значение ключа <code>PassengerKey</code> в качестве первичного ключа таблицы. Это связано с тем, что этот ключ часто используется в таблице фактов, ссылающейся на таблицу измерения. Чтобы сократить объём необходимого хранилища и повысить производительность соединений, имеет смысл поступать именно так и избегать использования натурального ключа. В остальном этот ключ не имеет значения для бизнеса. В дополнение к этому техническому ключу <strong>Измерения (Dimensions)</strong> часто содержат бизнес-ключи, которые идентифицируют записи в <strong>таблице dimension</strong>. На рисунке 7.3 это атрибут <code>PassportNumber</code>. Записи часто представляют собой бизнес-объекты и изменения этих объектов в операционной системе. Хотя натуральный ключ для одного и того же бизнес-объекта остаётся неизменным, даже если описательные данные были изменены в операционной системе, ключ изменяется при каждом изменении данных. Этот эффект используется таблицей фактов для ссылки на версию данных, которая была активна в момент, когда произошло бизнес-событие. Хеш-ключ из модели Data Vault 2.0 используется в качестве ключа в многомерной модели. Это упрощает процесс загрузки и помогает виртуализировать информационные витрины.</p>
<p>Несмотря на ключи в таблице, таблица измерения содержит описательные данные, которые описывают <strong>dimension entry (сущность измерения)</strong>. Очень часто таблицы размерностей состоят из множества описательных атрибутов. Их количество может достигать 50 или 100 столбцов. С другой стороны, такие таблицы часто не содержат большого количества строк. Существуют некоторые размерности, такие как размерность <code>Passenger</code>, представленная на рисунке 7.3, которые могут содержать много записей, но большинство измерений довольно малы, содержащие, возможно, не более сотни записей.</p>
<p>Не все числовые атрибуты должны быть мерами в таблице фактов. Например, номер ряда сидений в самолёте не используется ни в каких вычислениях. Он может использоваться как натуральный ключ, но не агрегируется бизнес-пользователем. То же самое относится к возрасту пассажира, если только бизнес не хочет рассчитывать средний возраст пассажиров самолёта. Существуют различные типы мер в таблице фактов многомерной модели данных:</p>
<ul>
<li><strong>Полностью аддитивные меры (Fully additive measures):</strong> полностью аддитивны в том смысле, что такие значения, как суммы и количества, могут быть просуммированы до допустимого общего значения.</li>
<li><strong>Полуаддитивные меры (Semi-additive measures):</strong> могут быть просуммированы по некоторым доступным размерностям, но не по всем.</li>
<li><strong>Неаддитивные меры (Nonadditive measures):</strong> не поддаются сложению.</li>
</ul>
<p>Таким образом, это также зависит от бизнес-потребностей, станет ли атрибут мерой в таблице фактов или описательным атрибутом в одной из её размерностей.</p>
<h2>Запросы к схемам «звезда»</h2>
<p>Конечные пользователи, знакомые со схемами типа «звезда», часто следуют простой схеме при прямом запросе данных из размерной модели:</p>
<ul>
<li><strong>Выбор необходимых фактов:</strong> сначала конечный пользователь решает, какие факты следует выбрать, определяя используемые таблицы фактов.</li>
<li><strong>Выбор необходимых измерений:</strong> добавляя размерности к запросу через соединения (join), конечные пользователи добавляют необходимый контекст.</li>
<li><strong>Ограничение области фактов:</strong> факты фильтруются в соответствии со значениями измерений (либо из таблицы фактов по ключам, либо из присоединённых размерностей).</li>
<li><strong>Агрегация фактов:</strong> исходные факты агрегируются с помощью агрегатных функций, таких как <code>SUM()</code> или <code>COUNT()</code>.</li>
</ul>
<p>Поскольку такой подход настолько распространён, многие производители СУБД оптимизировали свои реляционные движки под подобные запросы, включая Microsoft <strong>за счёт поддержки оптимизации соединений типа «звезда»</strong>.</p>
<p>Следующий оператор может быть использован для запроса к таблице фактов на рисунке 7.2:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT
    OriginAirport.Code, AVG(DepartureDelay)
FROM 
    FactFlight
INNER JOIN
    DimAirport OriginAirport ON OriginAirport.AirportKey = FactFlight.OriginAirportKey
WHERE
    OriginAirport.State = 'CA'
GROUP BY
    OriginAirport.Code</pre><p>Этот оператор сначала соединяет таблицу измерений <code>DimAirport</code> с таблицей фактов, используя <code>OriginAirportKey</code>. Обратите внимание, что таблица <code>DimAirport</code> может быть использована дважды:</p>
<ul>
<li>сначала для аэропорта отправления,</li>
<li>затем для аэропорта назначения.</li>
</ul>
<p>Поэтому в операторе для таблицы размерности используется псевдоним <code>OriginAirport</code>.<br />
После того как размерность была присоединена, <strong>состояние аэропорта</strong> используется для фильтрации данных по всем аэропортам Калифорнии. <strong>Штат аэропорта</strong> является описательным атрибутом в таблице измерений. Кроме того, <strong>код аэропорта</strong> используется для группировки данных, чтобы представить пользователю список известных аэропортов Калифорнии со средней задержкой отправления для каждого аэропорта.</p>
<p>Оператор также показывает, как отдельные части SQL-оператора используются в запросах к Dimension модели:</p>
<ul>
<li><strong>Оператор <code>SELECT</code></strong> определяет меры и размерности, которые должны быть представлены конечному пользователю. Также указываются любые формулы, включая агрегатные функции, которые должны быть применены к результату.</li>
<li><strong>Оператор <code>FROM</code></strong> определяет таблицу фактов, к которой выполняется запрос. Во многих случаях это только одна таблица фактов. Однако также возможно выполнение запроса к нескольким таблицам фактов.</li>
<li><strong>Оператор <code>JOIN</code></strong> присоединяет контекст фактов к итоговому результату. Во многих случаях имеется несколько операторов JOIN, каждый из которых соединяет отдельную таблицу измерения.</li>
<li><strong>Оператор <code>WHERE</code></strong> ограничивает область фактов, используемых в результирующем наборе. Во многих случаях доступны данные из дополнительных записей размерности (например, регионы продаж), которые не имеют отношения к выполняемой задаче. Поэтому конечные пользователи обычно ограничивают данные интересующими их записями размерностей.</li>
<li><strong>Оператор <code>GROUP BY</code></strong> определяет, как данные группируются перед агрегацией и представлением конечному пользователю. Агрегация в операторе <strong>SELECT</strong> основана на этом <strong>GROUP BY</strong>. Таким образом, оператор <strong>GROUP BY</strong> используется для изменения гранулярности (grain) результата по сравнению с гранулярностью исходной таблицы фактов.</li>
</ul>
<p><strong>Хотя оператор GROUP BY</strong> может использоваться для повышения уровня гранулярности фактов, невозможно получить результат с более низким уровнем гранулярности. Рассмотрим таблицу фактов, содержащую данные об итогах рейсов: количество пассажиров на борту, аэропорт отправления и назначения, дата рейса и т.д. Если таблица фактов не содержит информации на уровне пассажиров, проанализировать такую информацию невозможно. Поэтому важно правильно выбрать гранулярность таблицы фактов при проектировании <strong>многомерной модели данных</strong>.</p>
<h1>Несколько звёзд (Multiple Stars)</h1>
<p>Оператор в предыдущем разделе использовал только одну таблицу фактов в качестве основы для результата. Однако уже упоминалось, что также возможно использование нескольких таблиц фактов в качестве источника запроса. Это поведение поддерживается <strong>согласованными измерениями (Conformed Dimensions)</strong>, которые используются несколькими звёздами (таблицей фактов с её таблицами измерений) и, следовательно, соединяют эти отдельные звёзды через согласованное измерение.</p>
<h2>Согласованные размерности/измерения (Conformed Dimensions)</h2>
<p><strong>Согласованные измерения</strong> &#8212; это размерности, которые используются несколькими звёздами. Они применяются для сравнения мер из каждой схемы звезда. Повторное использование согласованных размерностей является очень распространённым для «поддержки подлинного анализа бизнес-процессов в разных областях». Это возможно только в том случае, если все схемы звезда, которые должны анализироваться в рамках такого сквозного анализа, используют одну и ту же <strong>таблицу измерений</strong> с точно такими же значениями ключей (первичными ключами). Использование хеш-ключей из модели Data Vault 2.0 упрощает соблюдение этого требования. <strong>Такой анализ называется drill-across</strong> и объединяет информацию из нескольких бизнес-процессов или событий. Только тогда становится возможным анализ данных из различных таблиц фактов с помощью согласованного измерения. На рисунке 7.4 приведён пример.</p>
<p><strong>Рисунок 7.4 Две схемы звезда, соединённые согласованными измерениями (физический дизайн)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1795" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4.jpeg" alt="" width="1494" height="810" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4.jpeg 1494w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4-300x163.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4-1024x555.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4-768x416.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4-450x244.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4-780x423.jpeg 780w" sizes="(max-width: 1494px) 100vw, 1494px" /></a></p>
<p><strong>Исходная картинка:</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1798" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial.jpeg" alt="" width="1186" height="787" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial.jpeg 1186w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial-300x199.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial-1024x680.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial-768x510.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial-450x299.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_4_initial-780x518.jpeg 780w" sizes="(max-width: 1186px) 100vw, 1186px" /></a></p>
<p>В этом примере имеются две схемы звезда с их таблицами фактов в центре:<br />
<code>FactConnection</code> слева и <code>FactAirportVisit</code> с рисунка 7.1 — справа. Каждая из них имеет ряд подключённых измерений. Обе используют две общие таблицы согласованных измерений &#8212; <code>DimAirport</code> и <code>DimDate</code>, которые показаны в центре схемы. Для поддержки сквозного анализа между <code>FactConnection</code> и <code>FactAirportVisit</code> крайне важно, чтобы обе согласованные размерности имели абсолютно одинаковую структуру и данные.</p>
<p>Чтобы поддерживать такие согласованные размерности, необходимо, чтобы любые изменения в согласованной размерности не нарушали работу одной из размерных звёзд, использующих эту размерность. Это может — и, скорее всего, произойдёт — на последующих спринтах проекта. Всякий раз при изменении структуры или содержания согласованной размерности необходимо пересмотреть влияние на каждую зависимую схему звезда и провести соответствующее тестирование. Поэтому использование согласованных размерностей влечёт за собой влияние изменений, которое может ограничить гибкость команды разработки. Этот избыточный объём сопровождения согласованных размерностей часто приводит к практике, при которой разработчики не изменяют существующие согласованные размерности, чтобы избежать тестирования. Вместо этого они создают новые дополнительные согласованные размерности, чтобы избежать затрат на переинжиниринг, что приводит к несогласованным размерностям. В других случаях они копируют структуру и данные существующих согласованных размерностей в новые схемы звезда и расширяют их новыми функциями, оставляя старую согласованную размерность без изменений (и, следовательно, без необходимости тестирования), снова добавляя несогласованные размерности. Мы называем эту практику <strong>&#171;dimension-itis&#187;</strong>, что приводит к неконтролируемому количеству размерностей, которые вроде бы одинаковые, но… несогласованные.</p>
<p><strong>Обратная практика</strong> — это добавление большего количества атрибутов и данных в существующие согласованные размерности, чтобы избежать создания новых. Этот подход применяется, когда разработчики хотят избежать создания новых согласованных размерностей из-за объёма необходимого тестирования существующих фактов и измерений. Конечный результат такого подхода — так называемая <strong>«деформированная» размерность (“deformed” dimension):</strong> такая размерность не может больше устойчиво изменяться из-за неясных и неконтролируемых изменений в структуре и сложных или запутанных зависимостей, и становится кошмаром с огромными затратами на переинжиниринг при необходимости изменений в будущем. Команды, создающие такие деформированные размерности, как правило, избегают их документирования, что создаёт дополнительные сложности для их преемников.</p>
<p>Мы не рекомендуем отказываться от использования согласованных размерностей. То, что действительно необходимо — это способ быстро создавать и изменять их с низкими затратами на сопровождение. Наша рекомендация — использовать виртуальные информационные витрины.</p>
<h1>Проектирование измерений (Dimension Design)</h1>
<p>Последний раздел этой главы охватывает некоторые дополнительные концепции <strong>многомерного моделирования (Dimension Modeling)</strong>, включая медленно изменяющиеся измерения, иерархии и краткое введение в схему &#171;Снежинка&#187; — более расширенную размерную модель.</p>
<h2>Медленно изменяющиеся измерения (Slowly Changing Dimensions, SCD)</h2>
<p>До этого момента <strong>измерения (dimensions)</strong> рассматривались как сущности, обеспечивающие контекст для фактов. Это верно, но важно понимать, что этот контекст со временем меняется. Например, домашний адрес пассажиров может изменяться из-за переезда, фамилия — из-за брака, а обращение — из-за исправления ошибки. Поскольку связь между схемой звезда и измерением основана на значениях ключей, модель может учитывать изменения в системе-источнике наилучшим образом для удовлетворения требований анализа: изменения могут либо отслеживаться внутри измерения, либо перезаписывать существующие данные в измерении.</p>
<p><strong>Медленно изменяющиеся измерения</strong> используются для обработки изменений в таблицах измерений. Это связано с тем, что темп изменения размерностей относительно медленный, особенно по сравнению с быстро меняющимися таблицами фактов. Поскольку существует несколько способов отслеживания истории в измерениях, Кимбалл ввёл классификацию для таблиц измерений.</p>
<p><strong>Slowly Changing Dimension Type 0 (Медленно изменяющееся измерение Типа 0)</strong> сохраняет исходные значения атрибутов измерения. Если в системе-источнике происходит изменение, значение в таблице измерений не изменяется. Хотя этот тип встречается реже, чем следующие три, он может использоваться для хранения исходного кредитного рейтинга клиента или любого другого первоначального значения.</p>
<p>Изменения в системе-источнике перезаписывают данные в <strong>Slowly Changing Dimension Type 1 (Медленно изменяющемся измерении Типа 1)</strong>. Атрибут измерения всегда отражает наиболее актуальное значение. Следовательно, история не отслеживается. Если на этом атрибуте основана агрегация (например, в предложении <code>GROUP BY</code> оператора), результаты этих агрегаций будут меняться в зависимости от нового значения. В таблице 7.2 приведён пример SCD типа 1.</p>
<p><strong>Таблица 7.2 Медленно изменяющаяся размерность типа 1</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/table_7_2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1781" src="https://datatalks.ru/wp-content/uploads/2025/07/table_7_2.png" alt="" width="1267" height="223" srcset="https://datatalks.ru/wp-content/uploads/2025/07/table_7_2.png 1267w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_2-300x53.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_2-1024x180.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_2-768x135.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_2-450x79.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_2-780x137.png 780w" sizes="(max-width: 1267px) 100vw, 1267px" /></a></p>
<p>Таблица размерности, представлена в таблице 7.2, содержит адресную информацию о пассажирах.<br />
Каждый пассажир идентифицируется по ключу, который является первичным ключом и используется таблицей фактов, и по натуральному ключу — номеру паспорта. Все остальные атрибуты в измерении имеют описательный характер и подробно описывают текущий адрес.<br />
Когда пассажир переезжает, текущая запись перезаписывается. Таким образом, история в таблице измерения не сохраняется. Таблица отражает только текущий адрес, а не предыдущие адреса пассажира.</p>
<p>Вместо перезаписи существующих данных, изменения в системе-источнике добавляют новую строку в таблицу <strong>Slowly Changing Dimension Type 2 (Медленно изменяющееся измерение Типа 2)</strong>. Используя такой подход, часто можно корректно отслеживать историю. Поэтому это является преобладающей техникой отслеживания истории в информационных витринах. Пример размерности типа 2 представлен в таблице 7.3.</p>
<p><strong>Таблица 7.3. Медленно изменяющаяся размерность типа 2</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/table_7_3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1783" src="https://datatalks.ru/wp-content/uploads/2025/07/table_7_3.png" alt="" width="1285" height="275" srcset="https://datatalks.ru/wp-content/uploads/2025/07/table_7_3.png 1285w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_3-300x64.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_3-1024x219.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_3-768x164.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_3-450x96.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_3-780x167.png 780w" sizes="(max-width: 1285px) 100vw, 1285px" /></a></p>
<p>Таблица (модифицирована из таблицы 7.2 и адаптирована для цели) показывает, как изменения отслеживаются в медленно изменяющейся размерности типа 2: каждая новая версия записи добавляется как дополнительная строка в таблицу размерности.</p>
<p>В этом примере Эми Миллер, с ключом <code>1b3ba82...</code>, вышла замуж за Петера Хайнца, с ключом <code>28ab342...</code>, и переехала к нему в Калифорнию. Поэтому её новая запись с ключом <code>444bbaa...</code> отражает эти изменения. Старая запись остаётся нетронутой, чтобы сохранить историческую информацию. Старые факты продолжают использовать запись с ключом <code>1b3ba82...</code>, но новые факты ссылаются на запись размерности с ключом <code>444bbaa...</code>.</p>
<p>Изменения в <strong>медленно изменяющейся размерности типа 3</strong> не добавляют дополнительных строк, а отслеживают историю в дополнительных столбцах. Для каждого атрибута, подлежащего историзации, добавляется новый столбец для каждого изменения, произошедшего в системе-источнике и подлежащего отображению бизнес-пользователю. Это упрощает анализ изменений со временем. Пример приведён в таблице 7.4.</p>
<p><strong>Таблица 7.4. Медленно изменяющаяся размерность типа 3</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/table_7_4.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1784" src="https://datatalks.ru/wp-content/uploads/2025/07/table_7_4.png" alt="" width="1290" height="242" srcset="https://datatalks.ru/wp-content/uploads/2025/07/table_7_4.png 1290w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_4-300x56.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_4-1024x192.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_4-768x144.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_4-450x84.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_4-780x146.png 780w" sizes="(max-width: 1290px) 100vw, 1290px" /></a></p>
<p>Этот пример показывает, как бизнес отслеживает предыдущее состояние пассажира с помощью дополнительного столбца. Когда пассажир переезжает, как в случае с Эми Миллер (теперь Эми Хайнц) в записи с ключом <code>1b3ba82...</code>, старый адрес перезаписывается, а атрибуты, которые должны быть исторически отслежены, копируются в дополнительные столбцы. В данном случае столбец <strong>Previous State</strong> обновляется, чтобы отразить предыдущее состояние пассажира.</p>
<p>История не ограничивается только одной записью. Обычно используется ограниченное количество столбцов для хранения нескольких изменений — <strong>в большинстве случаев только текущего и предыдущего значения</strong>. В других случаях ключевые показатели эффективности (KPI) отслеживаются со временем, как в примере в таблице 7.5.</p>
<p><strong>Таблица 7.5. Медленно изменяющаяся размерность типа 3 с несколькими столбцами</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/table_7_5.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1785" src="https://datatalks.ru/wp-content/uploads/2025/07/table_7_5.png" alt="" width="1198" height="201" srcset="https://datatalks.ru/wp-content/uploads/2025/07/table_7_5.png 1198w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_5-300x50.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_5-1024x172.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_5-768x129.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_5-450x76.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/table_7_5-780x131.png 780w" sizes="(max-width: 1198px) 100vw, 1198px" /></a></p>
<p>Таблица 7.5 показывает размерность, которая отслеживает доступные рейсы, их отправные точки и аэропорты назначения. В дополнение к этой основной информации, таблица измерения содержит три значения KPI, которые показывают, как изменялось среднее количество пассажиров за последние три года (включая текущий год).</p>
<p>В дополнение к представленным типам размерностей, в некоторых случаях встречаются и следующие:</p>
<ul>
<li><strong>Slowly changing dimension type 4 (Медленно изменяющаяся размерность типа 4):</strong> этот тип выносит изменчивые атрибуты в отдельную мини-размерность.</li>
<li><strong>Slowly changing dimension type 5 (Медленно изменяющаяся размерность типа 5):</strong> гибрид типов 4 и 1 (4 + 1 = 5). Позволяет получить доступ к текущим атрибутам мини-размерности вместе с другими из основной размерности без связывания через таблицу фактов.</li>
<li><strong>Slowly changing dimension type 6 (Медленно изменяющаяся размерность типа 6):</strong> этот тип добавляет текущие атрибуты к размерности типа 2.</li>
<li><strong>Slowly changing dimension type 7 (Медленно изменяющаяся размерность типа 7):</strong> этот тип обеспечивает ту же функциональность, что и тип 6, но с использованием двух внешних ключей в таблице фактов: один ссылается на размерность типа 2 с отслеживаемыми атрибутами, а другой — на текущую строку.</li>
</ul>
<h2>Иерархии</h2>
<p><strong>Иерархии (Hierarchies)</strong> &#8212; ещё одна важная концепция в размерном моделировании. Они позволяют производить детализацию данных для более глубокого анализа. Рассмотрим следующий пример:<br />
В Excel PivotTable представлена мировая выручка от пассажиров за несколько лет в виде общих значений (см. рисунок 7.5).</p>
<p><strong>Рисунок 7.5 Выручка от пассажиров по годам и регионам</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_5.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1801" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_5.jpeg" alt="" width="980" height="331" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_5.jpeg 980w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_5-300x101.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_5-768x259.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_5-450x152.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_5-780x263.jpeg 780w" sizes="(max-width: 980px) 100vw, 980px" /></a></p>
<p>Столбцы на рисунке 7.5 показывают финансовые годы (FY) из размерности даты, доступные в сводной таблице. Строки отображают географические регионы из размерности географии. Кроме того, присутствуют общие итоги, показывающие совокупное значение для каждого столбца или строки. Например, выручка от пассажиров за финансовый год 2008 в Соединённых Штатах составила $19,471,989.04.</p>
<p><strong>Географическая иерархия состоит из следующих уровней:</strong></p>
<ul>
<li>Страна</li>
<li>Штат/Провинция</li>
<li>Город</li>
<li>Почтовый индекс</li>
</ul>
<p>Возможно <strong>выполнение детализации (drill-down) до любого уровня</strong>, который доступен в сводной таблице (которая основана на OLAP-кубе), но не глубже. Рисунок 7.6 показывает логическую модель, в которой представлена размерность географии с её иерархией.</p>
<p><strong>Рисунок 7.6 Размерность географии (логическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_6.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1802" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_6.jpeg" alt="" width="220" height="644" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_6.jpeg 220w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_6-102x300.jpeg 102w" sizes="(max-width: 220px) 100vw, 220px" /></a></p>
<p>Обозначения следуют <strong>методологии моделирования ADAPT (Application Design for Analytical Processing Technologies)</strong> для размерных баз данных. Эта методология часто используется в индустрии для моделирования и документирования размерных моделей. Первый элемент изображает саму размерность Geography. Размерность включает одну иерархию, также называемую Geography. Остальные элементы представляют собой отдельные уровни иерархии и должны следовать в указанном порядке. Мы будем использовать методологию моделирования ADAPT на протяжении всей книги.</p>
<p>Аналогично иерархии Geography, иерархия Fiscal Calendar состоит из нескольких уровней:</p>
<ul>
<li>Финансовый год</li>
<li>Финансовое полугодие</li>
<li>Финансовый квартал</li>
<li>Месяц</li>
<li>Дата</li>
</ul>
<p>Эта иерархия очень распространена в системах хранилищ данных и часто используется конечными пользователями. Она использует другую дату начала года, что часто практикуется коммерческими организациями. Например, предприятия в сфере торговли часто начинают финансовый год не с 31 декабря. Это связано с большим числом возвратов после Рождества, совершаемых клиентами, недовольными подарками. Поскольку объём таких возвратов достаточно велик, он оказывает влияние на общую выручку за год. Однако в большинстве случаев возвраты происходят в январе следующего календарного года. Если компания закрывает отчётный год 31 декабря, возвраты будут отнесены к следующему финансовому периоду и исказят финансовые результаты. Поэтому такие компании часто выбирают другой месяц в качестве окончания финансового периода. Один из примеров — Best Buy, розничный продавец электроники в США. Финансовый год этой компании заканчивается 1 февраля. Рисунок 7.7 показывает логическую модель размерности Date.</p>
<p><strong>Рисунок 7.7 Размерность даты (логическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_7.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1803" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_7.jpeg" alt="" width="341" height="620" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_7.jpeg 341w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_7-165x300.jpeg 165w" sizes="(max-width: 341px) 100vw, 341px" /></a></p>
<p>Обратите внимание, что в размерности доступны две иерархии: Calendar и Fiscal Calendar. Обе имеют свои собственные уровни Год, Полугодие и Квартал, но разделяют уровни Месяц и Дата. Обе иерархии используют все атрибуты размерности, но организованы по-разному. В зависимости от потребностей бизнес-аналитика используется та или иная иерархия. Обе являются корректными; ценность каждой иерархии зависит от контекста бизнес-анализа.</p>
<p>Чтобы более подробно проанализировать информацию на рисунке 7.5, можно нажать знак «плюс» рядом с меткой строки и выполнить детализацию по иерархии географии, как показано на рисунке 7.8.</p>
<p><strong>Рисунок 7.8 Выручка от пассажиров в Соединённых Штатах по годам</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_8.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1804" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_8.jpeg" alt="" width="477" height="754" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_8.jpeg 477w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_8-190x300.jpeg 190w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_8-450x711.jpeg 450w" sizes="(max-width: 477px) 100vw, 477px" /></a></p>
<p>Общая выручка от пассажиров, которая всё ещё отображается в строке, выделенной полужирным шрифтом, разбивается по штатам — следующий географический уровень ниже уровня страны. Для каждого штата показывается выручка от пассажиров по годам. Можно дополнительно разбить эти данные, выполнив детализацию до следующего уровня (рисунок 7.9).</p>
<p><strong>Рисунок 7.9 Выручка от пассажиров в штате Алабама по годам</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_9.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1805" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_9.jpeg" alt="" width="477" height="799" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_9.jpeg 477w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_9-179x300.jpeg 179w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_9-450x754.jpeg 450w" sizes="(max-width: 477px) 100vw, 477px" /></a></p>
<p>На рисунке 7.9 проводится более глубокий анализ выручки от пассажиров для штата Алабама. Показан каждый город в Алабаме, и выручка от пассажиров для каждого города представлена по годам. Столбец даты представляет собой аналогичную иерархию. Также возможно выполнить детализацию этой иерархии.</p>
<p>На рисунке 7.10 показаны только данные за финансовый год 2007, но более детализировано. Обратите внимание, что отображаются первые три месяца финансового года, который начинается в июле. Для каждого месяца, квартала и полугодия в правой части таблицы показаны итоговые значения. Такой подход позволяет бизнес-аналитикам глубоко понять представленную информацию.</p>
<p><strong>Рисунок 7.10 Выручка от пассажиров в Алабаме по годам, в деталях</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_10.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1806" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_10.jpeg" alt="" width="980" height="872" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_10.jpeg 980w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_10-300x267.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_10-768x683.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_10-450x400.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_10-780x694.jpeg 780w" sizes="(max-width: 980px) 100vw, 980px" /></a></p>
<p>Иерархия географии реализована физически из таблицы размерности, как показано на рисунке 7.11.</p>
<p><strong>Рисунок 7.11 Размерность географии (физическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_11.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1807" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_11.jpeg" alt="" width="473" height="534" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_11.jpeg 473w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_11-266x300.jpeg 266w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_11-450x508.jpeg 450w" sizes="(max-width: 473px) 100vw, 473px" /></a></p>
<p>Каждый уровень иерархии основан на одном из физических атрибутов таблицы размерности. Связь между атрибутом и уровнем иерархии задаётся в Microsoft SQL Server Analysis Services. Аналогичные иерархии будут реализованы в этой главе при построении OLAP-куба и добавлении размерности даты как иерархии.</p>
<h2>Дизайн &#171;Снежинка&#187; (Snowflake Design)</h2>
<p>В этой главе была представлена схема «звезда», основанная на таблице фактов в центре и сопровождающих её таблицах размерностей, которые предоставляют контекст для фактов. Эти таблицы измерений напрямую соединяются с таблицей фактов. Косвенное соединение таблиц измерений &#8212; то есть, когда одна таблица измерения ссылается на другую таблицу измерения — невозможно в схеме звезда. Это справедливо даже в случае наличия связи между атрибутами измерений.</p>
<p>Рассмотрим пример корпоративных групп в авиационной индустрии и отдельных авиаперевозчиков. Каждая авиакомпания входит в состав более крупной группы, которая владеет этим перевозчиком. Например, United Continental Holdings, Inc. владеет как United Airlines, так и Continental Airlines, а также другими, более мелкими перевозчиками.</p>
<p>Если схема звезда должна отразить эту связь, обе размерности — <code>DimGroup</code> и <code>DimCarrier</code> — должны быть напрямую связаны с таблицей фактов, как показано на рисунке 7.12.</p>
<p><strong>Рисунок 7.12 Схема звезда с таблицей фактов в центре и только напрямую связанными размерностями (физическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_12.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1808" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_12.jpeg" alt="" width="691" height="806" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_12.jpeg 691w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_12-257x300.jpeg 257w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_12-450x525.jpeg 450w" sizes="(max-width: 691px) 100vw, 691px" /></a></p>
<p>Этот пример повторяет представленный на рисунке 7.4. Однако он показывает только одну схему звезда. Обратите внимание на две размерности <code>DimGroup</code> и <code>DimCarrier</code>, которые независимо связаны с таблицей фактов. Тем не менее, между этими двумя размерностями существует неявная связь. Эта связь реализована в ETL-задании, которое загружает таблицу. ETL-задание гарантирует, что в таблицу фактов загружаются только допустимые комбинации <code>CarrierKey</code> и <code>GroupKey</code>. Модель не реализует никаких правил, которые препятствовали бы загрузке недопустимых комбинаций. С другой стороны, эта модель проста в использовании для бизнес-пользователей, потому что они могут напрямую соединять всю необходимую контекстную информацию. Именно поэтому схема звезда допускает только напрямую связанные таблицы измерений.</p>
<p><strong>Например, чтобы получить среднюю задержку группы, можно использовать следующий оператор:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT
    DimGroup.Name, AVG(DepDelay)
FROM 
    FactConnection
INNER JOIN
    DimGroup ON DimGroup.GroupKey = FactConnection.GroupKey
GROUP BY
    DimGroup.Name</pre><p>Как видно из запроса, не требуется больших усилий, чтобы добавить группу в результирующий набор. Соединение осуществляется напрямую с использованием поля <code>GroupKey</code> в таблице фактов.</p>
<p><strong>Схема «снежинка»</strong>, с другой стороны, допускает косвенные таблицы измерений. Это полезно, если необходимо явно смоделировать отношения между атрибутами измерений. Если бы модель на рисунке 7.12 была реализована как схема «снежинка», она выглядела бы примерно как на рисунке 7.13.</p>
<p><strong>Рисунок 7.13 Схема снежинка с таблицей фактов в центре и косвенно связанными измерениями (физическая модель)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/image_7_13.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1809" src="https://datatalks.ru/wp-content/uploads/2025/07/image_7_13.jpeg" alt="" width="691" height="809" srcset="https://datatalks.ru/wp-content/uploads/2025/07/image_7_13.jpeg 691w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_13-256x300.jpeg 256w, https://datatalks.ru/wp-content/uploads/2025/07/image_7_13-450x527.jpeg 450w" sizes="(max-width: 691px) 100vw, 691px" /></a></p>
<p>Как видно из диаграммы, <strong>термин «снежинка»</strong> происходит от внешнего вида модели. Таблицы измерений отходят от центральной таблицы фактов, подобно лучам снежинки. Вместо того чтобы напрямую ссылаться на <code>DimGroup</code> из <code>FactConnection</code>, группа ссылается косвенно через <code>DimCarrier</code>. <code>GroupKey</code>, который идентифицирует группу в таблице фактов, перемещён в <code>DimCarrier</code>. Чтобы вернуть среднюю задержку группы, как в предыдущем запросе, необходимо сначала соединить <code>DimCarrier</code>, а затем <code>DimGroup</code>.</p>
<p>Это не представляет проблемы, если вы умеете писать SQL-запросы и не испытываете трудностей при косвенном соединении таблиц, как в этом случае. Для опытного разработчика баз данных такой запрос не представляет никакой угрозы. Однако если бизнес-аналитики с меньшими знаниями и опытом работы с SQL-запросами должны работать со схемами снежинка, они могут почувствовать себя перегруженными и не справиться с базой данных.</p>
<p>Несмотря на эту проблему, <strong>схемы снежинка</strong> на самом деле используются довольно часто.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-7-data-vault-2-0-dimensional-modeling/">Перевод 7 Главы &#8212; Dimensional Modeling (Data Vault 2.0)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-7-data-vault-2-0-dimensional-modeling/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
			</item>
		<item>
		<title>Требования ACID. BASE модель. CAP теорема</title>
		<link>https://datatalks.ru/acid-base-cap-theorem-for-databases/</link>
					<comments>https://datatalks.ru/acid-base-cap-theorem-for-databases/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 09 Feb 2025 07:50:06 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[DWH]]></category>
		<category><![CDATA[ACID]]></category>
		<category><![CDATA[atomicity]]></category>
		<category><![CDATA[BASE]]></category>
		<category><![CDATA[Basically Available]]></category>
		<category><![CDATA[CAP]]></category>
		<category><![CDATA[CAP Теорема]]></category>
		<category><![CDATA[consistency]]></category>
		<category><![CDATA[durability]]></category>
		<category><![CDATA[Eventually consistent]]></category>
		<category><![CDATA[Soft state]]></category>
		<category><![CDATA[Write-Ahead Logging]]></category>
		<category><![CDATA[Требования ACID]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=975</guid>

					<description><![CDATA[<p>Что такое ACID? Представьте, что вы запускаете приложение электронной коммерции. Клиент размещает заказ, и ваша система должна вычесть товар из запасов, списать средства с кредитной карты клиента и зарегистрировать продажу в вашей системе учета — и все это одновременно. Что произойдет, если платеж не пройдет, но ваш счет в инвентаре уже будет уменьшен? Или если [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/acid-base-cap-theorem-for-databases/">Требования ACID. BASE модель. CAP теорема</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Что такое ACID?</h1>
<p>Представьте, что вы запускаете приложение электронной коммерции.</p>
<p>Клиент размещает заказ, и ваша система должна вычесть товар из запасов, списать средства с кредитной карты клиента и зарегистрировать продажу в вашей системе учета — и все это одновременно.</p>
<p>Что произойдет, если платеж не пройдет, но ваш счет в инвентаре уже будет уменьшен? Или если ваше приложение зависнет на полпути процесса?</p>
<p>Вот тут-то и вступают в игру транзакции ACID. Они гарантируют, что все шаги в таких критических операциях происходят надежно и последовательно.</p>
<p><strong>ACID (atomicity, consistency, isolation, durability)</strong> — набор требований к транзакционной системе, обеспечивающий наиболее надёжную и предсказуемую её работу — атомарность, согласованность, изоляцию, устойчивость. Сформулированы в конце 1970-х годов Джимом Греем.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-978" src="https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details.jpg" alt="" width="1200" height="1202" srcset="https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details.jpg 1200w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-300x300.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-1022x1024.jpg 1022w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-150x150.jpg 150w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-768x769.jpg 768w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-450x451.jpg 450w, https://datatalks.ru/wp-content/uploads/2025/02/acid_properties_in_dbms_details-780x781.jpg 780w" sizes="(max-width: 1200px) 100vw, 1200px" /></a></p>
<p>В этой статье мы подробно рассмотрим значение каждого свойства ACID, почему они важны и как они реализованы в базах данных.</p>
<h2>Что такое транзакция базы данных?</h2>
<p>Транзакция в контексте баз данных — это последовательность из одной или нескольких операций (например, вставка, обновление или удаление записей), которые база данных рассматривает как одно действие. Она либо полностью завершается успешно, либо полностью терпит неудачу, без промежуточных состояний.</p>
<p><strong>Пример: Банковский перевод</strong></p>
<p>Когда вы отправляете деньги другу, происходят две вещи:</p>
<ul>
<li>Деньги списываются с вашего счета.</li>
<li>Деньги зачисляются на их счет.</li>
</ul>
<p>Эти два шага образуют одну транзакцию. Если один из шагов не выполнен, оба отменяются.</p>
<p>Без транзакций базы данных могут оказаться в несогласованном состоянии.</p>
<p><strong>Например:</strong></p>
<ul>
<li><strong>Частичные обновления:</strong> Ваши деньги списываются, но Ваш друг их не получает.</li>
<li><strong>Конфликты:</strong> два человека бронируют последний билет в кино одновременно.</li>
</ul>
<p>Транзакции решают эти проблемы, применяя такие правила, как свойства ACID (атомарность, согласованность, изолированность, долговечность).</p>
<p>Теперь давайте рассмотрим каждое из свойств ACID.</p>
<h2>Atomicity (Атомарность)</h2>
<p>Атомарность гарантирует, что транзакция, включающая несколько операций, выполняется как единая и неделимая единица работы: она либо полностью завершается успешно (фиксируется), либо полностью завершается неудачей (откатывается).</p>
<p>Если какая-либо часть транзакции завершается неудачей, вся транзакция откатывается, а база данных восстанавливается до состояния, в котором она была до начала транзакции.</p>
<p><strong>Пример:</strong> В транзакции денежного перевода, если кредитный шаг не удается, дебетовый шаг не может быть оставлен сам по себе. Это предотвращает несоответствующие состояния, такие как «деньги исчезают» с одного счета, не появляясь на другом.</p>
<p>Атомарность устраняет сложность ручной отмены изменений, если что-то пойдет не так.</p>
<h3>Как базы данных реализуют атомарность</h3>
<p>Базы данных используют два ключевых механизма для обеспечения атомарности.</p>
<p><strong>1. Журналы транзакций (журналы упреждающей записи &#8212; Write-Ahead Log, WAL)</strong></p>
<hr />
<p>У <strong>WAL</strong> есть несколько используемых переводов:</p>
<ul>
<li>Журнал предзаписи</li>
<li>Журнал упреждающей записи</li>
<li>Транзакционный журнал</li>
<li>Упреждающее журналирование</li>
</ul>
<hr />
<p>Реализация:</p>
<ul>
<li>Каждая операция записывается в журнал (WAL), прежде чем она будет применена к фактической таблице базы данных.</li>
<li>В случае сбоя база данных использует этот журнал для отмены незавершенных изменений.</li>
</ul>
<p>Пример:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transaction_log.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1002" src="https://datatalks.ru/wp-content/uploads/2025/02/transaction_log.jpeg" alt="" width="330" height="268" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transaction_log.jpeg 330w, https://datatalks.ru/wp-content/uploads/2025/02/transaction_log-300x244.jpeg 300w" sizes="(max-width: 330px) 100vw, 330px" /></a></p>
<p>После того, как запись WAL безопасно помещена на диск, база данных приступает к изменению страниц в памяти, содержащих строки для учетной записи A и учетной записи B.</p>
<p><strong>В случае успеха операций:</strong></p>
<ul>
<li>База данных отмечает идентификатор транзакции 12345 как зафиксированный в журнале транзакций.</li>
<li>Недавно обновленные балансы для A и B в конечном итоге будут сброшены из памяти в соответствующие файлы данных на диске.</li>
</ul>
<p>Если база данных выходит из строя после записи в журнал, но до полного обновления файлов данных, WAL предоставляет способ восстановления:</p>
<ul>
<li>При перезапуске база данных проверяет WAL.</li>
<li>Видно, что транзакция 12345 была зафиксирована.</li>
<li>Он повторно применяет операции UPDATE, чтобы гарантировать правильность окончательных балансов в файлах данных.</li>
<li>Если транзакция не была зафиксирована (или была помечена как &#171;in progress&#187;) на момент сбоя, база данных откатит эти изменения, используя информацию в журнале, оставив таблицу такой, как будто транзакция никогда не происходила.</li>
</ul>
<p><strong>2. Протоколы фиксации и отката (Commit/Rollback Protocols)</strong></p>
<p>Базы данных предоставляют такие команды <code>BEGIN TRANSACTION</code>, как <code>COMMIT</code>, и <code>ROLLBACK</code>.</p>
<ul>
<li><code>BEGIN TRANSACTION</code> — начало транзакции.</li>
<li><code>COMMIT</code> — фиксация изменений.</li>
<li><code>ROLLBACK</code> — откат изменений.</li>
</ul>
<p>Любые изменения, внесенные между BEGIN TRANSACTION и COMMIT считаются «выполняемыми» и не будут применены навсегда, если транзакция не будет успешно зафиксирована.</p>
<p>Если какой-либо шаг не выполнен или вы явно указали ROLLBACK, все изменения, внесенные с момента начала транзакции, будут отменены.</p>
<p><strong>Пример:</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/begin_transaction_commit.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1004" src="https://datatalks.ru/wp-content/uploads/2025/02/begin_transaction_commit.jpeg" alt="" width="612" height="401" srcset="https://datatalks.ru/wp-content/uploads/2025/02/begin_transaction_commit.jpeg 612w, https://datatalks.ru/wp-content/uploads/2025/02/begin_transaction_commit-300x197.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/begin_transaction_commit-450x295.jpeg 450w" sizes="(max-width: 612px) 100vw, 612px" /></a></p>
<h2>Consistency (Согласованность)</h2>
<p>Согласованность в контексте ACID-транзакций гарантирует, что любая транзакция переводит базу данных из одного допустимого состояния в другое допустимое состояние — никогда не оставляя её в некорректном или «невалидном» состоянии.</p>
<p>Это означает, что все ограничения целостности данных, такие как ограничения первичного ключа (отсутствие дублирующихся идентификаторов), ограничения внешнего ключа (связанные записи должны существовать в родительских таблицах) и ограничения проверки (возраст не может быть отрицательным), соблюдаются до и после выполнения транзакции.</p>
<p>Если транзакция пытается нарушить эти правила, она не будет зафиксирована, и база данных вернётся в предыдущее состояние.</p>
<p>Пример:</p>
<p>В базе данных электронной коммерции есть две таблицы:</p>
<ul>
<li>products (со столбцами: product_id, stock_quantity, и т.д.)</li>
<li>orders (со столбцами: order_id, product_id, quantity, и т.д.)</li>
<li>Ограничение: Нельзя разместить заказ на товар, если запрашиваемое количество (quantity) превышает stock_quantity в таблице products.</li>
</ul>
<p>Transaction Flow (Последовательность транзакции):</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transaction_flow_begin.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1007" src="https://datatalks.ru/wp-content/uploads/2025/02/transaction_flow_begin.jpeg" alt="" width="632" height="333" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transaction_flow_begin.jpeg 632w, https://datatalks.ru/wp-content/uploads/2025/02/transaction_flow_begin-300x158.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/transaction_flow_begin-450x237.jpeg 450w" sizes="(max-width: 632px) 100vw, 632px" /></a></p>
<p>Если stock_quantity товара было 8 (меньше, чем количество, которое мы пытаемся заказать), база данных обнаружит, что новое значение будет -2, что нарушает правило согласованности (значение не должно становиться отрицательным).</p>
<p>Транзакция завершится с ошибкой или вызовет откат (rollback), предотвращая попадание базы данных в некорректное состояние.</p>
<h3>Как реализовать согласованность?</h3>
<p><strong>Ограничения схемы базы данных</strong></p>
<ul>
<li>Ограничения NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK и другие определения схемы предотвращают ввод недопустимых данных.</li>
</ul>
<p><strong>Триггеры и хранимые процедуры</strong></p>
<ul>
<li>Триггеры могут автоматически проверять дополнительные правила при вставке, обновлении или удалении строк.</li>
<li>Хранимые процедуры могут содержать логику для проверки данных перед фиксацией изменений.</li>
</ul>
<p><strong>Защита на уровне приложения</strong></p>
<ul>
<li>В то время как база данных обеспечивает соблюдение ограничений на более низком уровне, приложения часто добавляют дополнительные проверки, например, для обеспечения соблюдения бизнес-правил или проверки данных еще до того, как они попадут на уровень базы данных.</li>
</ul>
<h2>Isolation (Изоляция)</h2>
<p>Изоляция гарантирует, что одновременно выполняющиеся транзакции не вмешиваются в промежуточные состояния друг друга.</p>
<p>По сути, пока транзакция выполняется, её обновления (или промежуточные данные) остаются невидимыми для других параллельно выполняющихся транзакций, создавая иллюзию того, что каждая транзакция выполняется последовательно, одна за другой.</p>
<p>Без изоляции две или более транзакции могли бы читать и записывать частичные или незавершённые данные друг от друга, что привело бы к некорректным или несогласованным результатам.</p>
<p>Благодаря изоляции разработчики могут более надёжно прогнозировать, как изменения данных будут видны другим транзакциям.</p>
<h3>Аномалии конкурентного доступа / Аномалии параллелизма</h3>
<p>Чтобы понять, как работает изоляция, полезно рассмотреть, какие проблемы могут возникнуть при её отсутствии. Основные аномалии конкурентного доступа включают:</p>
<p><strong>Грязное чтение (Dirty Read)</strong></p>
<ul>
<li>Транзакция A читает данные, которые были изменены, но ещё не зафиксированы транзакцией B.</li>
<li>Если затем транзакция B выполняет откат (ROLLBACK), транзакция A окажется с недействительным или «грязным» значением, которое фактически никогда не существовало в зафиксированном состоянии.</li>
</ul>
<p><strong>Неповторяемое чтение (Non-Repeatable Read)</strong></p>
<ul>
<li>Транзакция A считывает одни и те же строки несколько раз во время своего выполнения, но видит разные данные, поскольку другая транзакция обновила или удалила эти строки между чтениями A.</li>
</ul>
<p><strong>Фантомное чтение (Phantom Read)</strong></p>
<ul>
<li>Транзакция A несколько раз читает одну и ту же строку (или строки) во время выполнения.</li>
<li>Между этими чтениями другая транзакция изменяет или удаляет соответствующие строки.</li>
<li>В результате транзакция A видит разные значения при повторных чтениях одной и той же строки.</li>
</ul>
<h3>Уровни изоляции</h3>
<p>Базы данных обычно позволяют выбрать уровень изоляции, который обеспечивает баланс между корректностью данных и производительностью.</p>
<p>Более высокие уровни изоляции обеспечивают более высокую согласованность данных, но могут снизить производительность системы за счет увеличения времени ожидания транзакций.</p>
<p>Давайте рассмотрим четыре распространенных уровня изоляции.</p>
<h4><strong>Read Uncommitted (Чтение неподтверждённых данных)</strong></h4>
<p>Разрешает грязное чтение; транзакции могут видеть незафиксированные изменения.</p>
<p>Используется редко, так как может привести к серьезным аномалиям.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transactions.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1008" src="https://datatalks.ru/wp-content/uploads/2025/02/transactions.jpeg" alt="" width="581" height="112" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transactions.jpeg 581w, https://datatalks.ru/wp-content/uploads/2025/02/transactions-300x58.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/transactions-450x87.jpeg 450w" sizes="(max-width: 581px) 100vw, 581px" /></a></p>
<h4><strong>Read Committed (Чтение подтверждённых данных)</strong></h4>
<p>Транзакция видит только те данные, которые были зафиксированы на момент чтения.</p>
<p>Предотвращает «грязное» чтение, однако неповторяющиеся чтения и фантомные чтения все еще могут иметь место.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transactions_2.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1009" src="https://datatalks.ru/wp-content/uploads/2025/02/transactions_2.jpeg" alt="" width="400" height="86" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transactions_2.jpeg 400w, https://datatalks.ru/wp-content/uploads/2025/02/transactions_2-300x65.jpeg 300w" sizes="(max-width: 400px) 100vw, 400px" /></a></p>
<h4><strong>Repeatable Read (Повторяемое чтение)</strong></h4>
<p>Гарантирует, что при многократном чтении одних и тех же строк в рамках транзакции вы получите одни и те же значения (если только вы явно не измените их).</p>
<p>Предотвращает грязное чтение и неповторяющееся чтение, но фантомное чтение все равно может произойти (в зависимости от ядра базы данных).</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transactions_3.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1010" src="https://datatalks.ru/wp-content/uploads/2025/02/transactions_3.jpeg" alt="" width="477" height="82" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transactions_3.jpeg 477w, https://datatalks.ru/wp-content/uploads/2025/02/transactions_3-300x52.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/transactions_3-450x77.jpeg 450w" sizes="(max-width: 477px) 100vw, 477px" /></a></p>
<h4><strong>Serializable (Сериализуемость)</strong></h4>
<p>Самый высокий уровень изоляции, при котором все транзакции происходят последовательно, по одной за раз.</p>
<p>Предотвращает грязное чтение, неповторяющееся чтение и фантомное чтение.</p>
<p>Самый затратный с точки зрения производительности и параллелизма, поскольку может потребоваться больше блокировок или больше проверок конфликтов.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/transactions_4.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1013" src="https://datatalks.ru/wp-content/uploads/2025/02/transactions_4.jpeg" alt="" width="508" height="152" srcset="https://datatalks.ru/wp-content/uploads/2025/02/transactions_4.jpeg 508w, https://datatalks.ru/wp-content/uploads/2025/02/transactions_4-300x90.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/transactions_4-450x135.jpeg 450w" sizes="(max-width: 508px) 100vw, 508px" /></a></p>
<h3>Как базы данных обеспечивают изоляцию</h3>
<h4><strong>Блокировки (Locking)</strong></h4>
<ul>
<li>Пессимистичный контроль конкурентности (Pessimistic Concurrency Control)</li>
<li>Строки или таблицы блокируются, чтобы никакая другая транзакция не могла их читать или изменять, пока блокировка не будет снята.</li>
<li>Может привести к блокировке (blocking) или взаимоблокировке (deadlock), если несколько транзакций конкурируют за одни и те же блокировки.</li>
</ul>
<h4><strong>MVCC (Многоверсионное управление конкурентным доступом — Multi-Version Concurrency Control)</strong></h4>
<ul>
<li>Оптимистичный контроль конкурентности (Optimistic Concurrency Control)</li>
<li>Вместо блокировки чтения база данных хранит несколько версий одной и той же строки.</li>
<li>Читающие транзакции видят согласованный снимок данных (как если бы они смотрели на состояние в определённый момент времени), а записывающие транзакции создают новую версию строки при обновлении.</li>
<li>Этот подход уменьшает конфликты блокировок, но требует тщательного управления версиями строк и их очистки (например, механизма vacuum в PostgreSQL).</li>
</ul>
<h4><strong>Изоляция снапшотов (Snapshot Isolation)</strong></h4>
<ul>
<li>Вариант MVCC, при котором каждая транзакция видит данные в том виде, в каком они были в начале её выполнения (или на согласованной точке во времени).</li>
<li>Предотвращает грязные чтения (Dirty Reads) и неповторяемые чтения (Non-Repeatable Reads).</li>
<li>Фантомные чтения (Phantom Reads) всё ещё возможны, если уровень изоляции не установлен в Serializable.</li>
</ul>
<h2>Durability (Устойчивость/Надежность/Долговечность)</h2>
<p>Надежность гарантирует, что после завершения транзакции внесенные изменения сохранятся даже в случае сбоев питания, сбоев или других катастрофических событий.</p>
<p>Другими словами, как только транзакция завершается, данные фиксируются навсегда и не могут просто исчезнуть.</p>
<h3>Как базы данных обеспечивают долговечность</h3>
<h4><strong>Журнал транзакций (Write-Ahead Logging, WAL)</strong></h4>
<p>Большинство реляционных баз данных используют журнал упреждающей записи (WAL) для сохранения изменений до их записи в основные файлы данных:</p>
<ul>
<li><strong>Write Changes to WAL (Запись изменений в WAL):</strong> предполагаемые операции (обновления, вставки, удаления) записываются в WAL на долговременном носителе (диске).</li>
<li><strong>Commit the Transaction (Зафиксировать транзакцию):</strong> как только запись WAL будет безопасно сохранена, база данных может пометить транзакцию как зафиксированную.</li>
<li><strong>Apply Changes to Main Data Files (Применить изменения к основным файлам данных):</strong> обновленные данные в конечном итоге записываются в основные файлы — возможно, сначала в память, а затем сбрасываются на диск.</li>
</ul>
<p>В случае сбоя базы данных во время восстановления она использует WAL :</p>
<ul>
<li><strong>Redo (Повторить):</strong> любые зафиксированные транзакции, еще не отраженные в основных файлах, применяются повторно.</li>
<li><strong>Undo (Отменить):</strong> все незавершенные (незафиксированные) транзакции откатываются для сохранения согласованности базы данных.</li>
</ul>
<h4><strong>Репликация / Избыточность (Replication / Redundancy)</strong></h4>
<p>Помимо WAL, многие системы используют репликацию, чтобы гарантировать сохранность данных даже в случае отказа оборудования или всего центра обработки данных.</p>
<ul>
<li><strong>Синхронная репликация:</strong> записи немедленно копируются на несколько узлов или центров обработки данных. Транзакция помечается как зафиксированная, только если первичный узел и по крайней мере одна реплика подтверждают, что она безопасно сохранена.</li>
<li><strong>Асинхронная репликация:</strong> изменения в конечном итоге синхронизируются с другими узлами, но существует (небольшое) окно, в котором может произойти потеря данных, если основной узел выйдет из строя до обновления реплики.</li>
</ul>
<h4><strong>Backups (Резервные копии)</strong></h4>
<p>Регулярные резервные копии обеспечивают сеть безопасности за пределами журналов и репликации. В случае серьезного повреждения, человеческой ошибки или катастрофического сбоя:</p>
<ul>
<li><strong>Полные резервные копии:</strong> сохранение всей базы данных на определенный момент времени.</li>
<li><strong>Инкрементное/дифференциальное резервное копирование:</strong> сохранение изменений с момента последнего резервного копирования для более быстрого и частого резервного копирования.</li>
<li><strong>Внешнее хранение:</strong> обеспечивает сохранность резервных копий в случае локальных сбоев, позволяя восстанавливать данные даже в случае повреждения оборудования.</li>
</ul>
<h2>Сравнительная таблица требований ACID в различных транзакционных СУБД</h2>


<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>База данных</th><th>Atomicity (Атомарность)</th><th>Consistency (Согласованность)</th><th>Isolation (Изоляция)</th><th>Durability (Надёжность)</th></tr></thead><tbody><tr><td>MSSQL</td><td>Использует журналы транзакций (Write-Ahead Logging, WAL), транзакции полностью атомарны</td><td>Гарантирует согласованность через строгие ограничения и триггеры</td><td>Поддерживает уровни изоляции: Read Uncommitted, Read Committed, Repeatable Read, Serializable, Snapshot</td><td>Журнал транзакций обеспечивает сохранность данных даже при сбоях</td></tr><tr><td>PostgreSQL</td><td>Использует WAL для атомарности транзакций</td><td>Поддерживает строгие ограничения целостности</td><td>MVCC (многоверсионность), уровни изоляции: Read Committed, Repeatable Read, Serializable</td><td>WAL обеспечивает надежность, данные сохраняются даже при сбое</td></tr><tr><td>MySQL (InnoDB)</td><td>InnoDB использует двухфазную фиксацию (2PC) и WAL</td><td>Ограничения целостности и внешние ключи поддерживаются</td><td>Поддержка уровней изоляции: Read Uncommitted, Read Committed, Repeatable Read, Serializable</td><td>Использует WAL, поддерживает автоматическое восстановление</td></tr><tr><td>Oracle</td><td>Использует механизмы undo/redo для атомарности</td><td>Строгая согласованность через ограничения и триггеры</td><td>Использует MVCC, уровни изоляции: Read Committed, Serializable</td><td>Надежное восстановление через redo-логи</td></tr><tr><td>IBM Db2</td><td>Использует WAL, гарантирует атомарность</td><td>Ограничения целостности, поддержка ACID-совместимых операций</td><td>Поддерживает уровни изоляции: Cursor Stability (CS), Repeatable Read (RR), Serializable</td><td>Надежность обеспечивается за счет WAL и резервного копирования</td></tr><tr><td>MariaDB (InnoDB, Aria)</td><td>InnoDB аналогичен MySQL, Aria поддерживает транзакции с журналированием</td><td>Поддержка ограничений целостности</td><td>MVCC (InnoDB), разные уровни изоляции</td><td>Журнал транзакций и восстановление</td></tr><tr><td>Firebird</td><td>Использует журнал изменений и механизм Undo</td><td>Строгая целостность данных</td><td>Поддерживает Snapshot Isolation, Read Committed, Serializable</td><td>Журнал транзакций защищает данные</td></tr><tr><td>SQLite</td><td>Использует механизм журнала транзакций (WAL)</td><td>Ограниченная поддержка ограничений целостности</td><td>Поддерживает уровни: Read Uncommitted, Serializable</td><td>Данные сохраняются в файле базы данных, но надежность зависит от настроек</td></tr></tbody></table></figure>


<h3>Пояснение по некоторой терминологии СУБД</h3>
<p><strong>Write-Ahead Logging (WAL) &#8212; Журнал опережающей записи</strong> – это метод, при котором изменения записываются сначала в журнал (лог), а затем применяются к основной базе данных. Это обеспечивает надежность транзакций, так как в случае сбоя можно восстановить данные из журнала.</p>
<p><strong>Уровни изоляции транзакций</strong></p>
<p><em>Чем выше уровень, тем меньше конкуренция, но выше задержки и блокировки</em></p>
<ul>
<li><strong>Cursor Stability (CS)</strong> – промежуточный уровень изоляции в IBM Db2, удерживает блокировку только на текущей строке, а не на всей транзакции.</li>
<li><strong>Read Uncommitted</strong> – самый низкий уровень, позволяет читать &#171;грязные&#187; (незафиксированные) данные других транзакций.</li>
<li><strong>Read Committed</strong> – транзакция видит только зафиксированные данные, но могут появляться &#171;фантомные&#187; чтения (изменения других транзакций).</li>
<li><strong>Repeatable Read</strong> – предотвращает &#171;фантомные&#187; чтения, но другие транзакции могут добавлять новые строки.</li>
<li><strong>Serializable</strong> – самый строгий уровень, транзакции выполняются последовательно, исключая все виды аномалий.</li>
<li><strong>Snapshot</strong> – транзакция видит данные в момент начала работы (используется в MVCC), обеспечивая изоляцию без блокировок.</li>
</ul>
<p><strong>MVCC (Multi-Version Concurrency Control)</strong></p>
<p>Многоверсионное управление конкурентным доступом – механизм, позволяющий транзакциям работать с разными версиями данных без блокировок. Это улучшает производительность и снижает конфликты.</p>
<p><strong>Двухфазная фиксация (2PC &#8212; Two-Phase Commit)</strong></p>
<p>Протокол, используемый при распределённых транзакциях. Он работает в два шага:</p>
<ul>
<li><strong>Подготовка</strong> – все узлы подтверждают готовность зафиксировать транзакцию.</li>
<li><strong>Фиксация</strong> – если все согласны, транзакция фиксируется. Если хоть один отказывает, транзакция откатывается.</li>
</ul>
<p><strong>Механизмы undo/redo для атомарности</strong></p>
<ul>
<li><strong>Undo (отмена)</strong> – используется для отката изменений, если транзакция прервана.</li>
<li><strong>Redo (повторное выполнение)</strong> – применяется для восстановления завершённых транзакций после сбоя.</li>
</ul>
<p><strong>Redo-логи</strong></p>
<p>Логи, содержащие информацию о зафиксированных транзакциях, чтобы можно было повторить изменения после сбоя системы.</p>
<p><strong>Журнал транзакций и восстановление</strong></p>
<p>Файл или структура данных в базе, записывающая все операции изменения данных. В случае сбоя система может восстановить последнюю корректную версию базы, используя этот журнал.</p>
<h1>BASE-модель для NoSQL баз данных</h1>
<p>В отличие от <strong>ACID-модели</strong>, которая ориентирована на <span style="color: #ff6600;"><strong>строгую согласованность</strong></span>, <strong>BASE (Basically Available, Soft state, Eventually consistent)</strong> подходит для распределённых NoSQL баз данных, где важнее доступность и масштабируемость.</p>
<p>Во второй половине 2000-х годов сформулирован подход к построению распределённых систем, в которых требования целостности и доступности выполнены не в полной мере, названый акронимом BASE (Basically Available, Soft-state, Eventually consistent — базовая доступность, неустойчивое состояние, согласованность в конечном счёте), при этом такой подход напрямую противопоставляется ACID.</p>
<p><strong>Под базовой доступностью</strong> подразумевается такой подход к проектированию приложения, чтобы сбой в некоторых узлах приводил к отказу в обслуживании только для незначительной части сессий при сохранении доступности в большинстве случаев.</p>
<p><strong>Неустойчивое состояние</strong> подразумевает возможность жертвовать долговременным хранением состояния сессий (таких как промежуточные результаты выборок, информация о навигации, контексте), при этом концентрируясь на фиксации обновлений только критичных операций.</p>
<p><strong>Согласованности</strong> в конечном счёте, трактующейся как возможность противоречивости данных в некоторых случаях, но при обеспечении согласования в практически обозримое время, посвящено значительное количество самостоятельных исследований.</p>
<h2><strong>BASE: <span style="color: #ff6600;">Basically Available</span> / <span style="color: #003366;">Soft state</span> / <span style="color: #339966;">Eventually consistent</span></strong></h2>
<p><strong>Basically Available (Базовая доступность)</strong></p>
<ul>
<li>Система гарантирует доступность данных даже в случае частичных отказов.</li>
<li>Данные могут быть не полностью согласованными, но запросы не блокируются.</li>
<li>Например, если один сервер недоступен, другой всё равно может выдать старую версию данных.</li>
</ul>
<p><strong>Soft state (Гибкое состояние)</strong></p>
<ul>
<li>Состояние системы может изменяться со временем даже без новых запросов.</li>
<li>Это значит, что данные в разных узлах могут временно отличаться из-за асинхронной репликации.</li>
<li>Например, если запись обновилась на одном сервере, изменения могут дойти до других с задержкой.</li>
</ul>
<p><strong>Eventually consistent (Окончательная согласованность)</strong></p>
<ul>
<li>Система со временем достигает согласованности, но не сразу.</li>
<li>Это позволяет масштабировать базы данных и повышать отказоустойчивость.</li>
<li>Например, в DynamoDB или Cassandra данные могут быть сначала разными на разных узлах, но через некоторое время синхронизируются.</li>
</ul>
<h2>Когда выбирают BASE вместо ACID?</h2>
<p><strong>BASE подходит, когда:</strong></p>
<ul>
<li>Важнее скорость и отказоустойчивость, чем мгновенная согласованность.</li>
<li>Данные могут быть временно несогласованными, но постепенно синхронизируются.</li>
<li>Используются распределённые NoSQL базы (Cassandra, DynamoDB, Riak, CouchDB).</li>
</ul>
<p>В отличие от ACID, BASE жертвует строгой целостностью ради масштабируемости и высокой доступности.</p>
<h2>Примеры популярных NoSQL баз данных</h2>
<h3>Документные базы данных (хранят данные в виде JSON, BSON, XML)</h3>
<ul>
<li><strong>MongoDB</strong> – самая популярная документная NoSQL БД, поддерживает гибкие схемы данных.</li>
<li><strong>CouchDB</strong> – использует JSON-документы и репликацию, оптимизирована для распределённых систем.</li>
<li><strong>Firebase Firestore</strong> – облачная NoSQL база от Google, активно используется в мобильных и веб-приложениях.</li>
</ul>
<h3>Ключ-значение (Key-Value) базы данных (быстрый доступ по ключу)</h3>
<ul>
<li><strong>Redis</strong> – сверхбыстрая in-memory NoSQL база, часто используется для кеширования.</li>
<li><strong>Amazon DynamoDB</strong> – облачная NoSQL база от AWS, хорошо масштабируется.</li>
<li><strong>Riak KV</strong> – распределённая база данных с высокой отказоустойчивостью.</li>
</ul>
<h3>Графовые базы данных (оптимизированы для работы с графами и связями)</h3>
<ul>
<li><strong>Neo4j</strong> – самая популярная графовая база, используется в социальных сетях, рекомендательных системах.</li>
<li><strong>ArangoDB</strong> – мультипарадигменная база (графы + документы + ключ-значение).<br />Amazon Neptune – облачная графовая БД от AWS, поддерживает Gremlin и SPARQL.</li>
</ul>
<h3>Колонночные базы данных (работают с огромными объёмами данных, оптимизированы для аналитики)</h3>
<ul>
<li><strong>Apache Cassandra</strong> – масштабируемая распределённая NoSQL БД от Facebook.</li>
<li><strong>HBase</strong> – построена на Hadoop, подходит для Big Data.</li>
<li><strong>Google Bigtable</strong> – облачная колонночная база от Google.</li>
</ul>
<h3>Time-Series базы данных (оптимизированы для работы с временными рядами)</h3>
<ul>
<li><strong>InfluxDB</strong> – популярная база для сбора и анализа данных о временных рядах (например, IoT-устройства, логи).</li>
<li><strong>TimescaleDB</strong> – расширение PostgreSQL для работы с временными рядами.</li>
<li><strong>OpenTSDB</strong> – работает поверх HBase, предназначена для хранения больших объёмов временных данных.</li>
</ul>
<h2>Как принцип BASE обеспечивается в СУБД?</h2>
<p>Вместо строгой ACID-согласованности базы BASE обеспечивают &#171;размытую&#187; согласованность, используя асинхронную репликацию, механизмы конфликтного разрешения и компромиссные модели консистентности.</p>
<p>Ниже будут рассмотрены некоторые из приемов, которые обеспечивают тот или иной пункт модели BASE.</p>
<h3><strong>Basically Available (Базовая доступность)</strong></h3>
<p><em><strong>Базовая доступность</strong> означает, что база данных должна быть доступна одновременно для всех пользователей в любое время. Пользователю не нужно ждать, пока завершатся другие транзакции, прежде чем обновлять запись. Например, в случае внезапного роста трафика система электронной коммерции может отдавать предпочтение выдаче списков и приему заказов. Не страшно, если обновление количества запасов будет выполнено с небольшой задержкой, зато пользователи продолжают покупать товары.</em></p>
<hr />
<p>Даже если часть системы выходит из строя, база продолжает работать.</p>
<ul>
<li><strong>Репликация данных</strong> – копии хранятся на нескольких узлах.</li>
<li><strong>Eventual Consistency</strong> – данные синхронизируются позже, но система не блокируется.</li>
<li><strong>Шардирование (разделение данных)</strong> – нагрузка делится на множество узлов.</li>
</ul>
<p>Пример:</p>
<ul>
<li>Cassandra или DynamoDB используют quorum-репликацию, где большинство узлов должны подтвердить запись.</li>
<li>Redis Cluster распределяет данные по узлам, гарантируя высокую доступность.</li>
</ul>
<h3><strong>Soft State (Гибкое состояние)</strong></h3>
<p><em><strong>Гибкое/Мягкое состояние</strong> здесь означает, что любые данные могут находиться в промежуточных или временных состояниях и в некоторый момент изменяться «сами по себе», без внешних событий или поступления новых данных. Эта концепция отражает неопределенное состояние записи, которую обновляют несколько приложений одновременно. Значение такой записи будет окончательно определено только после завершения всех транзакций. Например, если пользователи редактируют сообщение в социальной сети, внесенные ими изменения могут быть не сразу видны другим пользователям. Через некоторое время система учтет все внесенные изменения, и произойдет обновление сообщения, хотя ни один пользователь его не инициировал.</em></p>
<hr />
<p>Система может изменяться даже без новых транзакций.</p>
<ul>
<li><strong>Асинхронная репликация</strong> – данные распространяются между узлами не сразу, а с задержкой.</li>
<li><strong>Кэширование</strong> – данные могут храниться в распределённых узлах и устаревать.</li>
<li><strong>Механизмы разруливания конфликтов</strong> – например, last-write-wins (LWW), CRDT (Conflict-Free Replicated Data Types).</li>
</ul>
<p>Пример:</p>
<ul>
<li>В Riak можно настроить стратегию разрешения конфликтов – брать последнее изменение или объединять данные.</li>
<li>В MongoDB при репликации вторичные узлы могут временно содержать устаревшие данные.</li>
</ul>
<h3><strong>Eventually Consistent (Окончательная согласованность)</strong></h3>
<p><em><strong>Окончательная согласованность</strong> означает, что запись достигнет согласованности не сразу, а после завершения всех одновременных обновлений. После этого все приложения, запрашивающие эту запись, увидят одно и то же значение. Давайте рассмотрим в качестве примера распределенную систему редактирования документов, в которой несколько пользователей могут одновременно редактировать документ. Если пользователь A и пользователь B одновременно редактируют один и тот же раздел документа, их локальные копии могут временно различаться, пока не завершатся процессы распространения и синхронизации. Однако со временем, «в конечном счете», система достигает согласованности, распространяя и объединяя все изменения от разных пользователей.</em></p>
<hr />
<p>Данные на всех узлах рано или поздно становятся одинаковыми.</p>
<ul>
<li>Гибкие уровни согласованности – например, &#171;читаем свежие данные, если возможно&#187;.</li>
<li>Механизмы Gossip Protocol – узлы обмениваются информацией о последних изменениях.</li>
<li>Read Repair – если запрос вернул устаревшие данные, база может их обновить в фоновом режиме.</li>
</ul>
<p>Пример:</p>
<ul>
<li>Cassandra использует hinted handoff (хранит &#171;подсказки&#187; для недоступных узлов и передаёт их позже).</li>
<li>DynamoDB применяет vector clocks (временные метки для согласования версий данных).</li>
</ul>
<h1>Краткое описание различий между ACID и BASE</h1>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1026" src="https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base.jpeg" alt="" width="1078" height="376" srcset="https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base.jpeg 1078w, https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base-300x105.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base-1024x357.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base-768x268.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base-450x157.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/02/acid_vs_base-780x272.jpeg 780w" sizes="(max-width: 1078px) 100vw, 1078px" /></a></p>
<h1>Теорема CAP (Consistency, Availability, Partition Tolerance)</h1>
<p>В мире распределённых баз данных ключевую роль играет теорема<strong> CAP (Consistency, Availability, Partition Tolerance)</strong>, предложенная <strong>Эриком Брюэром (Eric Brewer)</strong> в 2000 году.</p>
<p>Теорема CAP в информатике гласит, что распределенное хранилище данных не может предоставить более двух из трех следующих гарантий:</p>
<ul>
<li><strong>C – Consistency (Согласованность):</strong> каждое чтение получает самую последнюю запись или ошибку. Все узлы видят одни и те же данные в одно и то же время.</li>
<li><strong>A – Availability (Доступность):</strong> каждый запрос получает ответ (без ошибок), без гарантии, что он содержит самую последнюю запись. Система отвечает на запросы даже при сбоях, но может выдавать устаревшие данные.</li>
<li><strong>P – Partition Tolerance (Устойчивость к разделению сети):</strong> система продолжает работать, несмотря на произвольное количество сообщений, удаленных (или задержанных) сетью между узлами. Система продолжает работать, даже если сеть между узлами разорвана.</li>
</ul>
<p><strong>Главный вывод теоремы CAP:</strong> в случае сетевых проблем (P) база данных должна выбрать между строгой согласованностью (C) или доступностью (A).</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-997" src="https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph.jpeg" alt="" width="1946" height="720" srcset="https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph.jpeg 1946w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-300x111.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-1024x379.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-768x284.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-1536x568.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-450x166.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-780x289.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/02/CAP_Theorem_graph-1600x592.jpeg 1600w" sizes="(max-width: 1946px) 100vw, 1946px" /></a></p>
<hr />
<p><strong>CAP теорема указывает, что распределенная система может иметь максимум два свойства из трех одновременно:</strong></p>
<ol>
<li><strong>Согласованность (атомарность)</strong> &#8212; Каждая операция чтения, должна возвращать значение, установленное в последней операции записи: то есть у каждого узла есть актуальные данные о определенном объекте (или данные во всех узлах не противоречат друг другу).</li>
<li><strong>Доступность</strong> &#8212; Каждый запрос, полученный исправным узлом, влечет за собой ответ. Изначально было что: &#171;почти все запросы должны получать ответы&#187;. Отсутствие ответа &#8212; неответ по таймауту, и все ошибки формата &#171;сервер занят&#187;.</li>
<li><strong>Устойчивость к распределению:</strong> Кластер сохраняет работоспособность при условии потери произвольного количества сообщений, посылаемых между узлами сети. Распределение означает, что все пакеты от узлов одной партиции не доходят до узлов другой партиции.</li>
</ol>
<p><em>В 2002 году Сет Джилберт и Нэнси Линч из Массачусетского технологического института подобрали формальные модели асинхронных и синхронных распределённых вычислений, в рамках которых показано выполнение теоремы CAP в условиях отсутствия синхронизации (общих часов) у узлов распределённой системы и принципиальную возможность компромисса в частично синхронных системах. В этой работе «согласованность» в смысле теоремы CAP соотнесена с выполнением первых двух требований ACID — атомарности и согласованности. В дальнейшем, многие практики ссылались на данную работу как на доказательство теоремы CAP.</em></p>
<h2>Как теорема CAP влияет на выбор баз данных?</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-993 size-full" src="https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases.jpeg" alt="" width="1155" height="590" srcset="https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases.jpeg 1155w, https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases-300x153.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases-1024x523.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases-768x392.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases-450x230.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/02/cap_theorem_databases-780x398.jpeg 780w" sizes="(max-width: 1155px) 100vw, 1155px" /></a></p>
<p>Распределённые базы данных выбирают две из трёх характеристик:</p>
<ul>
<li><strong>CP (Consistency + Partition Tolerance)</strong>
<ul>
<li>Строгая согласованность важнее доступности (во время сетевых проблем система может отказать в обслуживании)</li>
<li>Если важны точные, актуальные данные (банковские транзакции, финансовые операции).</li>
<li>Примеры СУБД: MongoDB (с Strong Consistency), HBase, Zookeeper</li>
</ul>
</li>
<li><strong>AP (Availability + Partition Tolerance)</strong>
<ul>
<li>Доступность важнее строгой согласованности (временно возможны устаревшие данные)</li>
<li>Если важнее всегда получать ответ, даже если данные могут быть устаревшими (соцсети, системы логирования)</li>
<li>Cassandra, DynamoDB, Riak</li>
</ul>
</li>
<li><strong>CA (Consistency + Availability)</strong>
<ul>
<li>Теоретически невозможна в распределённых системах, потому что сеть всегда может дать сбой</li>
<li>В реальном мире сеть не идеальна, поэтому отказоустойчивость (P) нельзя игнорировать.<br />Если сеть рвётся, приходится жертвовать либо C, либо A.</li>
<li>Реляционные базы в локальной сети (PostgreSQL, MySQL, MSSQL в монолитной архитектуре)</li>
</ul>
</li>
</ul>
<h2>Теорема CAP в отношении ACID и BASE</h2>
<ul>
<li><strong>ACID (Atomicity, Consistency, Isolation, Durability)</strong> – это не противоречие CAP, а подход для локальных транзакций.</li>
<li><strong>BASE (Basically Available, Soft State, Eventually Consistent)</strong> – модель NoSQL, строится вокруг AP-решений из CAP.</li>
</ul>
<blockquote>
<p>В реальности все системы балансируют между CAP, BASE и ACID, используя кванты согласованности и кэширование.</p>
</blockquote>
<hr />
<h2>В чем разница между базами данных ACID и BASE?</h2>
<blockquote>
<p>Аббревиатуры BASE и ACID специально подбирались так, чтобы в английском языке они противопоставлялись друг другу, поскольку эти же слова обозначают химически противоположные термины «щелочь» и «кислота».</p>
</blockquote>
<hr />
<p><strong>ACID и BASE</strong> – это модели транзакций для баз данных, которые определяют структуру данных и порядок работы с ними в базе данных. В контексте баз данных транзакцией называют любую операцию, которую база данных обрабатывает как единую единицу работы. Чтобы база данных оставалась согласованной, транзакция должна быть полностью завершена. Например, если вы переводите деньги с одного банковского счета на другой, нужно отразить эти изменения и на вашем счете, и на счете получателя. Такую транзакцию невозможно считать завершенной без выполнения обоих шагов.</p>
<p>В базах данных ACID приоритет отдается согласованности в ущерб доступности. Если на любом этапе транзакции возникает ошибка, вся транзакция полностью отменяется. Напротив, базы данных BASE отдают приоритет доступности в ущерб согласованности. При неудачном завершении транзакции пользователи могут временно получать несогласованные данные. Согласованность данных восстанавливается с некоторой задержкой.</p>
<h2>Для чего важны ACID и BASE?</h2>
<p>Например, когда покупатель добавляет товар в корзину на веб-сайте электронной коммерции, все остальные покупатели должны увидеть снижение уровня запасов этого товара. Если очередной покупатель добавит в корзину последний экземпляр, все остальные пользователи должны увидеть, что товара нет в наличии. Разработчикам баз данных нужно сделать выбор о поведении базы данных в том случае, если какая-либо операция в транзакции не может быть выполнена. База данных может действовать по одному из следующих принципов.</p>
<p>Отменить транзакцию и возвратить сообщение об ошибке, что снизит доступность, но гарантирует согласованность. Покупатель в таком случае не сможет добавить товар в корзину или другие покупатели не получат данных по некоторым товарам, пока не завершатся все операции добавления в корзину.</p>
<p>Продолжать остальные операции, что обеспечит максимально возможную доступность, но может приводить к несогласованности. В этом случае покупатель успешно добавит товар в корзину, а другие покупатели получат сведения об уровне запасов, но эти сведения будут неверными по крайней мере в течение некоторого времени.</p>
<p>В некоторых случаях согласованность имеет решающее значение, и тогда предпочтение отдается архитектуре ACID. Но есть и другие сценарии использования, в которых согласованность не критична. Например, когда вы принимаете запрос на добавление в друзья в социальной сети, никого не беспокоит, что другие пользователи некоторое время видят неточное количество друзей в вашем профиле. Вам более важно, чтобы доступ к ленте сообщений не пропадал на период синхронизации данных. В таких сценариях предпочтительным будет модель BASE.</p>
<h2>Может ли база данных быть одновременно ACID и BASE?</h2>
<p>Согласно теореме CAP, база данных может удовлетворять только двум из трех требований: согласованность, доступность и устойчивость к разделению. Обе модели баз данных ACID и BASE обеспечивают устойчивость к разделению, то есть не могут быть одновременно высокосогласованными и постоянно доступными. Это означает, что любая база данных может больше или меньше соответствовать концепции ACID или BASE, но не может использовать оба подхода одновременно. Например, базы данных SQL структурированы по модели ACID, а базы данных NoSQL используют архитектуру BASE. Некоторые базы данных NoSQL могут обладать определенными свойствами, характерными для ACID, но никогда не могут полностью соответствовать концепции ACID.</p>
<h2>Когда что использовать: ACID в сравнении с BASE</h2>
<p>Несмотря на существенные различия, системы баз данных ACID и BASE применяются в разных приложениях. ACID идеально подходит для корпоративных приложений, в которых требуются согласованность, надежность и предсказуемость данных. Например, банки всегда используют для хранения транзакций клиентов базу данных с архитектурой ACID, поскольку целостность данных здесь является главным приоритетом. В свою очередь, базы данных BASE лучше подходят для аналитической обработки слабо структурированных данных в больших объемах. Например, веб-сайты электронной коммерции используют базы данных с архитектурой BASE для хранения цен на товары, которые часто меняются. В этом случае точность отображения цен менее важна, чем своевременное информирование клиентов об изменении цены.</p>
<h2>Ключевые различия между ACID и BASE</h2>
<p><span style="color: #ff6600;"><strong>При выборе между моделями транзакций ACID и BASE для баз данных нужно искать компромиссы.</strong></span></p>
<p><strong>Масштабирование</strong></p>
<p>База данных с моделью транзакций ACID масштабируется хуже, поскольку она ориентирована на согласованность. Для любой записи в любой момент времени может выполняться только одна транзакция, что затрудняет горизонтальное масштабирование.</p>
<p>С другой стороны, базу данных с архитектурой BASE легко масштабировать по горизонтали, поскольку нет необходимости поддерживать строгую согласованность. Простое добавление нескольких узлов в кластер позволяет базе данных с архитектурой BASE повысить доступность данных, которая является основополагающим принципом этой архитектуры базы данных.</p>
<p><strong>Гибкость</strong></p>
<p>Базы данных ACID менее гибки в обработке данных. Такая база данных должна гарантировать немедленную согласованность, поэтому она может ограничивать доступ к некоторым приложениям в случае сбоев в сети или в электроснабжении. Аналогичным образом, приложениям приходится ждать своей очереди на обновление данных, если другие программные модули уже обрабатывают некоторую запись. Базы данных BASE намного более гибкие. В архитектуре BASE не используются строгие ограничения, и приложения могут изменять записи по мере появления обновлений.</p>
<p><strong>Производительность</strong></p>
<p>База данных ACID может испытывать проблемы с производительностью при обработке больших объемов данных или параллельных запросов. Поскольку все операции обрабатываются в строгом порядке, накладные расходы на поддержание целостности транзакций приводят к дополнительным задержкам, которые влияют на доступ всех приложений к затронутой записи.</p>
<p>Напротив, к базе данных BASE приложения могут обращаться для обработки записей в любое время. Это позволяет избежать длительного времени ожидания и повышает пропускную способность базы данных.</p>
<p><strong>Синхронизация</strong></p>
<p>База данных с архитектурой ACID нуждается в механизме синхронизации, чтобы выполненные в транзакции изменения были зафиксированы одновременно во всех связанных записях. В то же время, каждая изменяемая запись должны быть заблокирована от доступа других сторон до завершения или отмены транзакции. База данных BASE, в свою очередь, работает без блокировки записей и поддерживает только согласованность «в конечном счете», без каких-либо гарантий по времени ее достижения. При работе с базами данных BASE разработчики понимают, что при обработке записей могут и будут возникать несоответствия, и принимают необходимые меры предосторожности в самом приложении.</p>
<h1>Колоночные базы данных</h1>
<p><strong>ClickHouse, Vertica, Teradata</strong> – это колонночные базы данных, предназначенные для аналитики и обработки больших данных (Big Data). Они не совсем NoSQL, но и не классические реляционные базы, потому что работают по другой модели, ориентированной на быструю агрегацию и аналитические запросы.</p>
<p><strong>Как они работают?</strong></p>
<ul>
<li><strong>Колонночное хранение</strong> – данные хранятся не построчно (как в классических реляционных БД), а по колонкам. Это ускоряет агрегатные операции (SUM, AVG, COUNT).</li>
<li><strong>Массовая параллельная обработка (MPP &#8212; Massively Parallel Processing)</strong> – база распределяет нагрузку между узлами кластера, что ускоряет вычисления.</li>
<li><strong>Жесткая схема</strong> – в отличие от NoSQL, эти базы обычно требуют заранее заданной структуры данных.</li>
</ul>
<p><strong>ACID или BASE?</strong></p>
<p>Эти системы не используют ни чистый ACID, ни BASE, а скорее компромиссный подход:</p>


<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>База данных</th><th>Модель согласованности</th><th>Доступность</th><th>Транзакции</th></tr></thead><tbody><tr><td><strong>ClickHouse</strong></td><td>Eventual Consistency (не гарантирует строгую ACID-согласованность, данные могут быть &#171;размазанными&#187; в кластере)</td><td>Высокая (оптимизирована для чтения)</td><td>Поддерживает &#171;атомарные&#187; вставки, но нет полноценной поддержки ACID</td></tr><tr><td><strong>Vertica</strong></td><td>Strong Consistency (более строгая согласованность, чем ClickHouse)</td><td>Высокая, но зависит от кластера</td><td>Ограниченная поддержка транзакций</td></tr><tr><td><strong>Teradata</strong></td><td>Strong Consistency (ACID-like)</td><td>Высокая, но заточена под масштабируемость</td><td>Поддерживает транзакции, но основное назначение – аналитика</td></tr></tbody></table></figure>


<p>Они ближе к Eventual Consistency (Окончательная согласованность), но не совсем BASE и не ACID. Они оптимизированы под скорость аналитики, жертвуя полной транзакционной поддержкой (как в ACID) ради высокой производительности.</p>
<h1>А как работает GreenPlum?</h1>
<p><strong>Greenplum</strong> – это масштабируемая MPP (Massively Parallel Processing) реляционная СУБД, основанная на PostgreSQL. Она предназначена для аналитических задач и хранилищ данных (DWH – Data Warehouse).</p>
<ul>
<li><strong>Тип:</strong> Колонно-строчная реляционная СУБД (по структуре ближе к классическим РСУБД, но заточена под аналитику).</li>
<li><strong>Используемая модель:</strong> ACID для отдельных сегментов, но в целом ориентирована на MPP-анализ (компромисс между ACID и Eventual Consistency).</li>
<li><strong>Область применения:</strong> Big Data, хранилища данных, аналитические запросы (OLAP).</li>
</ul>
<blockquote>
<p>Greenplum – это гибрид между классической ACID-реляционной СУБД (PostgreSQL) и MPP-анализом больших данных.</p>
<hr /></blockquote>
<p><strong style="font-family: Roboto; font-size: revert;">Как работает Greenplum?</strong></p>
<ul>
<li><strong>Massively Parallel Processing (MPP)</strong> – база данных делится на узлы (Data Nodes), каждый из которых выполняет свою часть запроса параллельно.</li>
<li><strong>Segment-based Architecture</strong> – данные хранятся в разделах (сегментах), а главный узел (Master) управляет запросами.</li>
<li><strong>ACID внутри сегментов</strong> – каждая нода гарантирует ACID на своем уровне, но согласованность между узлами достигается за счёт распределенных транзакций.</li>
<li><strong>Ориентация на аналитические запросы (OLAP)</strong> – поддержка SQL, индексов, распределённой агрегации.</li>
</ul>
<blockquote>
<p>По модели – не совсем ACID и не BASE, а гибрид ACID + MPP, где каждый сегмент ACID, но согласованность распределённых транзакций не моментальная.</p>
</blockquote>
<h1>Компромиссы в системном дизайне: 2PC, 3PC, Saga, BASE</h1>
<p><strong>Компромисс в инженерии</strong> — это осознанный выбор одного свойства системы в ущерб другому. Это не «ошибка» и не «плохое решение», а неизбежная плата за достижение определённой цели.</p>
<p>Компромиссы возникают, потому что ресурсы ограничены (время, сеть, память, обработка), а свойства системы часто противоречат друг другу.</p>
<p><strong>Примеры противоречий:</strong></p>
<ul>
<li>Быстродействие <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Надёжность</li>
<li>Согласованность <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Доступность (CAP-теорема)</li>
<li>Масштабируемость <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Простота архитектуры</li>
<li>Атомарность <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Распределённость</li>
</ul>
<h2>Сводка по компромиссам</h2>
<table>
<thead>
<tr>
<th>Подход</th>
<th>Консистентность</th>
<th>Доступность</th>
<th>Отказоустойчивость</th>
<th>Производительность</th>
<th>Сложность</th>
<th>Атомарность</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>2PC</strong></td>
<td>Высокая</td>
<td>Низкая</td>
<td>Низкая</td>
<td>Низкая</td>
<td>Средняя</td>
<td>Да</td>
</tr>
<tr>
<td><strong>3PC</strong></td>
<td>Высокая</td>
<td>Ниже средней</td>
<td>Средняя</td>
<td>Ниже средней</td>
<td>Высокая</td>
<td>Да</td>
</tr>
<tr>
<td><strong>Saga</strong></td>
<td>Средняя</td>
<td>Высокая</td>
<td>Высокая</td>
<td>Средняя/высокая</td>
<td>Средняя</td>
<td>Нет (компенсации)</td>
</tr>
<tr>
<td><strong>BASE</strong></td>
<td>Низкая (eventual)</td>
<td>Высокая</td>
<td>Высокая</td>
<td>Высокая</td>
<td>Низкая</td>
<td>Нет</td>
</tr>
</tbody>
</table>
<ul>
<li>Если важна целостность и критичность (банки, финансы): ACID + 2PC (или 3PC).</li>
<li>Если нужны микросервисы и высокая доступность: Saga.</li>
<li>Если важна масштабируемость и отказоустойчивость (соцсети, IoT): BASE.</li>
</ul>
<p><strong>Компенсация</strong> — это обратная операция, предназначенная для отмены или нейтрализации эффекта уже выполненной операции.</p>
<blockquote>
<p>Если ты выполнил шаг, но позже вся последовательность должна быть отменена, ты не можешь просто «откатить» как в SQL — ты должен выполнить противоположное действие, которое компенсирует последствия.</p>
</blockquote>
<h2>Еще раз вернемся к САР теореме</h2>
<p>Это фундаментальная концепция распределённых систем, утверждающая, что в условиях сетовых сбоев система может гарантировать только два из трёх свойств:</p>
<ul>
<li><strong>Consistency (согласованность):</strong> все узлы видят одинаковые данные одновременно.</li>
<li><strong>Availability (доступность):</strong> каждый запрос получает ответ (успешный или нет).</li>
<li><strong>Partition tolerance (устойчивость к разделению):</strong> система продолжает работу при потере связи между частями сети.</li>
</ul>
<h2>Протокол согласования 2PC (Two-Phase Commit)</h2>
<p><strong>Компромисс:</strong><br /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Консистентность ↑<br />✘ Производительность ↓<br />✘ Отказоустойчивость ↓</p>
<p><strong>Описание:</strong><br />Протокол с двумя фазами:</p>
<ul>
<li><strong>Prepare</strong> — координатор опрашивает участников, готовы ли они зафиксировать транзакцию.</li>
<li><strong>Commit</strong> — если все &#171;за&#187;, координатор отправляет &#171;commit&#187;, иначе &#171;abort&#187;.</li>
</ul>
<p><strong>Недостатки:</strong></p>
<ul>
<li>Координатор — Single Point of Failure.</li>
<li>Участники могут зависнуть в неопределённом состоянии при сбое координатора между фазами.</li>
<li>Блокирующий протокол — участники не могут принять решение самостоятельно, ждут координатора.</li>
</ul>
<h2>Протокол согласования 3PC (Three-Phase Commit)</h2>
<p><strong>Компромисс:</strong><br /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Отказоустойчивость ↑<br />✘ Сложность ↑<br /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Более «неблокирующий», чем 2PC</p>
<p><strong>Описание:</strong><br />Добавляется промежуточная фаза между prepare и commit:</p>
<ul>
<li><strong>Can Commit</strong> — координатор спрашивает, могут ли участники зафиксировать.</li>
<li><strong>Pre-Commit</strong> — если все согласны, координатор даёт «готовьтесь к коммиту».</li>
<li><strong>Do Commit</strong> — координатор сообщает финальное решение.</li>
</ul>
<p><strong>Преимущества:</strong></p>
<ul>
<li>Уменьшение вероятности блокировки за счёт промежуточной фазы.</li>
<li>Участники могут принять решение сами, если координатор пропал после 2-й фазы.</li>
</ul>
<p><strong>Недостатки:</strong></p>
<ul>
<li>Более сложная реализация.</li>
<li>Больше сетевых взаимодействий → выше задержки.</li>
</ul>
<h2>Протокол согласования Saga Pattern (длинные/компенсируемые транзакции)</h2>
<p>В классических ACID-транзакциях ты можешь сделать <code>ROLLBACK</code>, и всё вернётся в исходное состояние. Но в распределённых системах (микросервисы, веб-сервисы) нет общей транзакции. Поэтому вместо отмены используется Saga, где каждая операция:</p>
<ul>
<li>Или завершается успешно,</li>
<li>Или компенсируется вручную.</li>
</ul>
<p><strong>Компромисс:</strong><br /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Доступность ↑<br />✘ Атомарность ↓</p>
<p><strong>Описание:</strong><br />Сага разбивает большую транзакцию на серию локальных (атомарных в пределах сервиса) операций с компенсациями. Если одна операция неудачна, запускается обратная последовательность для отката предыдущих шагов.</p>
<p><strong>Пример:</strong></p>
<ul>
<li>Зарезервировать номер в отеле.</li>
<li>Купить билет.</li>
<li>Забронировать такси.<br />Если шаг 2 не удался — отменить шаг 1 (компенсация).</li>
</ul>
<table>
<thead>
<tr>
<th>Шаг</th>
<th>Операция</th>
<th>Компенсация</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Зарезервировать номер</td>
<td>Отменить бронь</td>
</tr>
<tr>
<td>2</td>
<td>Списать деньги</td>
<td>Вернуть деньги</td>
</tr>
<tr>
<td>3</td>
<td>Забронировать трансфер</td>
<td>Отменить трансфер</td>
</tr>
</tbody>
</table>
<p><strong>Типы:</strong></p>
<ul>
<li>Orchestration (централизованный управляющий)</li>
<li>Choreography (каждый сервис реагирует на события)</li>
</ul>
<p><strong>Недостатки:</strong></p>
<ul>
<li>Нет строгой атомарности.</li>
<li>Не всегда возможна полная компенсация.</li>
<li>Сложность логики откатов.</li>
</ul>
<h2>BASE-модель (противопоставляется ACID)</h2>
<p>Компромисс:</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Масштабируемость ↑<br />✘ Нет строгой консистентности (eventual consistency)</p>
<p><strong>Расшифровка:</strong></p>
<ul>
<li><strong>Basically Available</strong> — система отвечает даже при сбоях.</li>
<li><strong>Soft state</strong> — состояние может меняться со временем (в т.ч. без ввода данных).</li>
<li><strong>Eventual consistency</strong> — данные станут согласованными в будущем.</li>
</ul>
<p><strong>Применяется в:</strong></p>
<ul>
<li>NoSQL-базы (Cassandra, DynamoDB)</li>
<li>Высоконагруженные распределённые системы</li>
</ul>
<p><strong>Плюсы:</strong></p>
<ul>
<li>Высокая масштабируемость.</li>
<li>Подходит для отказоустойчивых систем с миллионами пользователей.</li>
</ul>
<p><strong>Минусы:</strong></p>
<ul>
<li>Отсутствие строгой согласованности — требует проектировать бизнес-логику с этим учетом.</li>
</ul>
<h2>Видео Филипп Вагнер «Распределенные транзакции в условиях микросервисной архитектуры»</h2>
<p><iframe title="Филипп Вагнер «Распределенные транзакции в условиях микросервисной архитектуры»" width="1170" height="658" src="https://www.youtube.com/embed/P98oMiVCnq8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>Видео Паттерн «Saga» в бронировании отелей · Антон Цитульский</h2>
<p><iframe title="Паттерн «Saga» в бронировании отелей · Антон Цитульский" width="1170" height="658" src="https://www.youtube.com/embed/owCEP2rKV9I?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h1>Использованные ссылки для написания статьи</h1>
<ul>
<li><a href="https://blog.algomaster.io/p/what-are-acid-transactions-in-databases" target="_blank" rel="noopener">What are ACID Transactions in Databases?</a></li>
<li><a href="https://aws.amazon.com/ru/compare/the-difference-between-acid-and-base-database/" target="_blank" rel="noopener">В чем разница между базами данных ACID и BASE?</a></li>
</ul><p>Сообщение <a href="https://datatalks.ru/acid-base-cap-theorem-for-databases/">Требования ACID. BASE модель. CAP теорема</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/acid-base-cap-theorem-for-databases/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Data Pipeline Design Patterns &#8212; Шаблоны проектирования потоков данных</title>
		<link>https://datatalks.ru/data-pipeline-design-patterns/</link>
					<comments>https://datatalks.ru/data-pipeline-design-patterns/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 02 Feb 2025 11:30:49 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Batch Processing Pattern]]></category>
		<category><![CDATA[Data Lake Architecture]]></category>
		<category><![CDATA[data lakehouse]]></category>
		<category><![CDATA[data mesh]]></category>
		<category><![CDATA[Data Pipeline]]></category>
		<category><![CDATA[Data Pipeline Design Patterns]]></category>
		<category><![CDATA[ELT]]></category>
		<category><![CDATA[ETL]]></category>
		<category><![CDATA[Kappa Architecture]]></category>
		<category><![CDATA[Lambda Architecture]]></category>
		<category><![CDATA[Stream Processing Pattern]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=834</guid>

					<description><![CDATA[<p>Анатомия платформы данных Прежде чем мы перейдем к рассмотрению Data Pipeline Design Patterns, мы рассмотрим различные термины из архитектуры данных (платформа данных, DWH, Data Lake и т.д.). Различия между платформой данных, хранилищем данных и озером данных Прежде чем углубляться в мир платформ данных, давайте разберем три часто путаемых понятия: платформа данных, хранилище данных и озеро [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/data-pipeline-design-patterns/">Data Pipeline Design Patterns &#8212; Шаблоны проектирования потоков данных</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Анатомия платформы данных</h1>
<p><em>Прежде чем мы перейдем к рассмотрению Data Pipeline Design Patterns, мы рассмотрим различные термины из архитектуры данных (платформа данных, DWH, Data Lake и т.д.).</em></p>
<h2>Различия между платформой данных, хранилищем данных и озером данных</h2>
<p>Прежде чем углубляться в мир платформ данных, давайте разберем три часто путаемых понятия: платформа данных, хранилище данных и озеро данных.</p>
<h3><strong>Озеро данных</strong></h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_lake.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-948 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_lake.jpeg" alt="" width="1069" height="555" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_lake.jpeg 1069w, https://datatalks.ru/wp-content/uploads/2025/01/data_lake-300x156.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_lake-1024x532.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/data_lake-768x399.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_lake-450x234.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_lake-780x405.jpeg 780w" sizes="(max-width: 1069px) 100vw, 1069px" /></a></p>
<p><strong>Озеро данных</strong> — это хранилище, которое содержит огромные объемы необработанных, неструктурированных, полуструктурированных и структурированных данных в их собственном формате. Оно предназначено для хранения данных из различных источников, таких как устройства IoT, социальные сети, web logs, датчики и многое другое.</p>
<p>Самая простая аналогия для понимания datalake — это сравнение с папкой Windows: вы можете хранить и организовывать гибким образом различные файлы независимо от их формата. Озеро данных поддерживает широкий спектр типов и форматов данных, включая текст, изображения, видео и т.д. (JSON, XML, CSV, Parquet, Avro), обеспечивая при этом основу для использования, например, в advanced analytics , машинном обучении или в проектах Data Science.</p>
<h3>Хранилища данных</h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-947 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2.jpeg" alt="" width="1108" height="685" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2.jpeg 1108w, https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2-300x185.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2-1024x633.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2-768x475.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2-450x278.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_warehouse2-780x482.jpeg 780w" sizes="(max-width: 1108px) 100vw, 1108px" /></a></p>
<p><strong>Хранилище данных</strong> — это централизованный репозиторий, в котором хранятся структурированные (или полуструктурированные), очищенные и обработанные данные из различных источников. Так же, как библиотека систематически хранит и организует книги для легкого доступа и поиска, хранилище данных организует структурированные данные из различных источников в централизованном репозитории.</p>
<p>Данные очищаются, преобразуются и структурируются в предопределенные форматы, что делает их оптимизированными для аналитических запросов и отчетов, обеспечивая при этом высокую производительность для бизнес-аналитики (BI) и процессов принятия решений.</p>
<h3>Платформа данных</h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-950 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture.jpeg" alt="" width="913" height="471" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture.jpeg 913w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture-300x155.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture-768x396.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture-450x232.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_architecture-780x402.jpeg 780w" sizes="(max-width: 913px) 100vw, 913px" /></a></p>
<p>Платформа данных служит единой системой для эффективного управления и анализа больших наборов данных. Она объединяет такие компоненты, как базы данных, озера данных и хранилища данных для обработки структурированных и/или неструктурированных данных в зависимости от вариантов использования.</p>
<p>Эта инфраструктура предлагает инструменты и услуги для различных задач, связанных с данными, таких как прием, интеграция, преобразование, хранение, обработка, анализ и визуализация. Кроме того, используя облачные технологии, платформы данных обеспечивают масштабируемость, гибкость и экономическую эффективность во всех операциях компании с данными.</p>
<h2>Общие компоненты платформы данных</h2>
<p>Хотя архитектурные особенности платформ данных могут различаться, они обычно разделяют фундаментальные слои. Хотя эти слои могут быть организованы и вложены по-разному для решения конкретных вариантов использования (различных потоков данных), они, как правило, имеют одни и те же фундаментальные компоненты:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-951 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components.jpeg" alt="" width="1679" height="549" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components.jpeg 1679w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-300x98.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-1024x335.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-768x251.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-1536x502.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-450x147.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-780x255.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/data_platform_components-1600x523.jpeg 1600w" sizes="(max-width: 1679px) 100vw, 1679px" /></a></p>
<ul>
<li><strong>Data sources (Источники данных):</strong> На этом уровне вы найдете все ваши источники данных. Они могут быть: структурированными, например, данные из вашей ERP, CRM, базы данных клиентов и т.д., полуструктурированными, например, NoSQL, Json, XML и т.д. или неструктурированными, например, PDF, изображения, видео, социальные сети&#8230;</li>
<li><strong>Ingestion:</strong> Платформам данных нужны механизмы для приема данных из различных источников данных в уровень хранения. Это может включать методы пакетного приема, потокового приема (в реальном времени) или их комбинацию. Распространенные инструменты и технологии, используемые для приема данных, включают Apache Kafka, AWS Kinesis или пользовательские процессы ETL.</li>
<li><strong>Storage (Хранение):</strong> Цель уровня хранения, как следует из его названия, заключается в хранении данных, при этом обрабатывая различные типы, поступившие из разных источников. Хранилище может включать традиционные реляционные базы данных, распределенные файловые системы, объектное хранилище или специализированные базы данных, такие как базы данных временных рядов.</li>
<li><strong>Processing (Обработка):</strong> После того, как данные сохранены (или связаны с приемом), их часто необходимо обработать и преобразовать, прежде чем их можно будет проанализировать или использовать приложениями. Обработка данных может выполняться пакетами или в режиме реального времени, часто путем создания ELT или ETL.</li>
<li><strong>Consumption (Потребление):</strong> На этом уровне предоставляются механизмы для запроса, анализа и визуализации данных для получения информации или принятия решений на основе данных. Такие технологии, как SQL-движки, инструменты визуализации данных, такие как Tableau или Power BI, и фреймворки машинного обучения, такие как TensorFlow или PyTorch, обычно используются для аналитики и BI.</li>
</ul>
<h1>8 ключевых паттернов проектирования data pipeline, которые нужно знать</h1>
<p>Давайте представим компанию, которая собирает данные. Будь то транзакции клиентов, данные с IoT-датчиков или бесконечный поток мнений или комментариев из соцсетей. Для этого нужен надежный способ перемещать эти данные из точки А в точку Б, обрабатывая их по пути. Вот тут и вступают в игру паттерны проектирования data pipeline. Это, по сути, архитектурные чертежи для перемещения и обработки данных.</p>
<p>Почему выбор правильного паттерна для Data Pipeline так важен? Нужно подбирать подходящий паттерн под задачу: если вы используете batch-процессинг, то сможете сэкономить деньги, но пожертвуете скоростью; выберете потоковую обработку в реальном времени – получите мгновенные инсайты, но это может потребовать большего бюджета.</p>
<p>В этом руководстве мы разберем паттерны, которые помогут проектировать Data Pipeline.</p>
<h2>Batch Processing Pattern (Паттерн пакетной обработки)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-854 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks.jpeg" alt="" width="918" height="450" srcset="https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks.jpeg 918w, https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks-300x147.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks-768x376.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks-450x221.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/batch_processing_datatalks-780x382.jpeg 780w" sizes="(max-width: 918px) 100vw, 918px" /></a></p>
<p>Представьте, что вы копите грязное белье всю неделю, чтобы устроить большую стирку на выходных. По сути, это и есть batch-процессинг для данных. Вместо обработки каждого элемента данных сразу по его поступлению вы собираете данные в одном месте и обрабатываете их по расписанию. Это как выделить «день стирки» для ваших данных.</p>
<p>Этот подход очень экономичен, так как системы не работают постоянно. Кроме того, он проще в управлении – идеально подходит для таких задач, как создание месячных отчетов или анализ исторических данных. Можно сказать, это подход «тише едешь – дальше будешь» в обработке данных.</p>
<h2>Stream Processing Pattern (Паттерн потоковой обработки)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_streaming.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-855 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_streaming.jpeg" alt="" width="918" height="450" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_streaming.jpeg 918w, https://datatalks.ru/wp-content/uploads/2025/01/data_streaming-300x147.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_streaming-768x376.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_streaming-450x221.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_streaming-780x382.jpeg 780w" sizes="(max-width: 918px) 100vw, 918px" /></a></p>
<p>Теперь представьте, что у вас есть волшебная стиральная машина, которая может стирать каждый предмет одежды сразу, как только он загрязнился. Это и есть потоковая обработка. Она обрабатывает данные в реальном времени, по мере их поступления.</p>
<p>Этот паттерн нужен, когда важна мгновенная реакция – например, для обнаружения мошеннических транзакций или анализа настроений в соцсетях во время крупного события. Да, поддержка таких систем, работающих 24/7, может быть дороже, но если нужны мгновенные инсайты, альтернатив нет.</p>
<h2>Паттерн проектирования Lambda Architecture</h2>
<p><strong>Вариант 1 &#8212; Унифицированный/Объединенный Serving слой</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-862 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min.jpeg" alt="" width="1837" height="712" srcset="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min.jpeg 1837w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-300x116.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-1024x397.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-768x298.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-1536x595.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-450x174.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-780x302.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_1_min-1600x620.jpeg 1600w" sizes="(max-width: 1837px) 100vw, 1837px" /></a></p>
<p><strong>Вариант 2 &#8212; Отдельные Serving слои</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-863 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min.jpeg" alt="" width="1820" height="796" srcset="https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min.jpeg 1820w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-300x131.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-1024x448.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-768x336.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-1536x672.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-450x197.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-780x341.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/lambda_architecture_option_2_min-1600x700.jpeg 1600w" sizes="(max-width: 1820px) 100vw, 1820px" /></a></p>
<p>Вот где начинается интересное.</p>
<p><strong>Lambda-архитектура</strong> – это как иметь обычную стиральную машину для еженедельных стирок и ту самую волшебную машину для мгновенной стирки. Вы запускаете две системы параллельно – одну для пакетной обработки, другую для потоковой.</p>
<p>Это здорово, потому что вы получаете лучшее из обоих миров: обновления в реальном времени, когда это необходимо, плюс глубокая обработка данных в пакетном режиме для аналитики.</p>
<p>Минус? Вам придется поддерживать две системы, поэтому ваша команда должна быть достаточно гибкой, чтобы работать с разными технологиями, при этом сохраняя согласованность определений данных.</p>
<h2>Паттерн проектирования Kappa Architecture</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-865 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks.jpeg" alt="" width="1817" height="639" srcset="https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks.jpeg 1817w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-300x106.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-1024x360.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-768x270.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-1536x540.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-450x158.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-780x274.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/01/kappa_architecture_datatalks-1600x563.jpeg 1600w" sizes="(max-width: 1817px) 100vw, 1817px" /></a></p>
<p>А что если есть способ получить что-то похожее на Lambda, но более минималистичное?</p>
<p>Разработчики Apache Kafka задали себе тот же вопрос и придумали архитектуру Kappa. Вместо того чтобы иметь оба слоя – пакетной обработки и потоковой – всё происходит в реальном времени, а весь поток данных хранится в центральном логе, таком как Kafka.</p>
<p>Это значит, что по умолчанию вы обрабатываете всё так же, как в паттерне Stream Processing. Но если вам нужна пакетная обработка для исторических данных, вы просто воспроизводите соответствующие логи.</p>
<p>Этот подход идеально подходит для работы с IoT-датчиками или аналитикой в реальном времени, где исторические данные – это просто коллекция прошлых событий, обработанных в реальном времени. Простота – в её главной силе: одна система для всех задач!</p>
<h2>Паттерн ETL (Extract, Transform, Load)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/etl.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-961" src="https://datatalks.ru/wp-content/uploads/2025/01/etl.jpeg" alt="" width="904" height="338" srcset="https://datatalks.ru/wp-content/uploads/2025/01/etl.jpeg 904w, https://datatalks.ru/wp-content/uploads/2025/01/etl-300x112.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/etl-768x287.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/etl-450x168.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/etl-780x292.jpeg 780w" sizes="(max-width: 904px) 100vw, 904px" /></a></p>
<p>Еще один классический подход к работе с данными – это ETL. Это похоже на подготовку еды: вы собираете все продукты (extract), готовите их (transform), а затем раскладываете по контейнерам (load).</p>
<p>Этот паттерн отлично подходит, когда вы точно знаете, что хотите сделать с данными, и вам нужно добиться стабильности при каждом запуске. Например, финансовая отчетность или соответствие нормативным требованиям, где нельзя допускать неожиданных изменений. Да, это немного старомодно, но традиционные методы остаются таковыми не просто так – они проверены временем.</p>
<h2>Паттерн ELT (Extract, Load, Transform)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/elt.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-960" src="https://datatalks.ru/wp-content/uploads/2025/01/elt.jpeg" alt="" width="904" height="338" srcset="https://datatalks.ru/wp-content/uploads/2025/01/elt.jpeg 904w, https://datatalks.ru/wp-content/uploads/2025/01/elt-300x112.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/elt-768x287.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/elt-450x168.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/elt-780x292.jpeg 780w" sizes="(max-width: 904px) 100vw, 904px" /></a></p>
<p>ELT меняет подход ETL с ног на голову. После того как вы «закупили продукты» (extract), вы сначала складываете их в холодильник (load), а уже потом решаете, что из них приготовить (transform). Вы загружаете данные в хранилище сразу, а затем определяете, как с ними работать.</p>
<p>Этот подход великолепен, если вы пока не знаете, как именно будете использовать данные, или если разные команды могут преобразовывать их по-разному. Он более гибкий, чем ETL, и отлично работает с низкой стоимостью современных систем хранения данных.</p>
<h2>Паттерн проектирования Data Mesh</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-962 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture.jpeg" alt="" width="1434" height="462" srcset="https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture.jpeg 1434w, https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture-300x97.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture-1024x330.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture-768x247.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture-450x145.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/data_mesh_architecture-780x251.jpeg 780w" sizes="(max-width: 1434px) 100vw, 1434px" /></a></p>
<p>Здесь мы подходим к действительно современным подходам. Data Mesh превращает вашу организацию данных в федерацию независимых «государств». Вместо того чтобы одна центральная команда управляла всеми данными (и становилась узким местом!), каждая команда или отдел самостоятельно управляет своими пайплайнами данных.</p>
<p>Это идеально для крупных компаний, где, например, маркетинговая команда хочет использовать данные клиентов для своих целей, а продуктовая команда нуждается в совершенно других данных для аналитики функционала. Главное – убедитесь, что у вас достаточно процессов, чтобы избежать появления data silos (изолированных хранилищ данных).</p>
<h2>Паттерн проектирования Medallion Architecture: Data Lakehouse</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-965" src="https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture.jpeg" alt="" width="858" height="376" srcset="https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture.jpeg 858w, https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture-300x131.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture-768x337.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture-450x197.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/01/Medallion_Architecture-780x342.jpeg 780w" sizes="(max-width: 858px) 100vw, 858px" /></a></p>
<p><strong>Data Lakehouse</strong> – это как «вилка-ложка» среди архитектурных паттернов, объединяющая лучшие черты data warehouse и data lake. Вы получаете структуру и производительность хранилища с гибкостью и масштабируемостью озера данных. Хотите запускать SQL-запросы на структурированных данных, при этом сохраняя сырые файлы для экспериментов ваших дата-сайентистов? Data Lakehouse справится с этим!</p>
<p><strong>Данные в Medallion Architecture обычно проходят через три стадии:</strong></p>
<ul>
<li><strong>Bronze:</strong> Сырые данные попадают сюда первыми, сохраняясь в их изначальном виде. Это как цифровая зона разгрузки – данные сохраняются точно так, как были получены, со всеми «шероховатостями».</li>
<li><strong>Silver:</strong> На этом этапе данные очищаются, проверяются и приводятся в соответствие со схемами. Средний слой удаляет дубликаты, обрабатывает пропущенные значения и гарантирует качество данных.</li>
<li><strong>Gold:</strong> Финальная, доработанная стадия, где данные преобразуются в формат, готовый для аналитики. Здесь находятся агрегированные таблицы, производные метрики и бизнес-ориентированные представления, оптимизированные под конкретные задачи.</li>
</ul>
<h3><strong>Event-Driven Data Architecture</strong></h3>
<p><em>К Data Lake можно также отнести и EDA подход.</em></p>
<p>Следующая диаграмма иллюстрирует идею использования микросервисов для приема данных в режиме реального времени и предоставления их для Data LakeHouse или Data Lake, используя возможности полуструктурированных данных.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture.jpeg" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-971 size-full" src="https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture.jpeg" alt="" width="1374" height="490" srcset="https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture.jpeg 1374w, https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture-300x107.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture-1024x365.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture-768x274.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture-450x160.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/02/event_driven_data_architecture-780x278.jpeg 780w" sizes="(max-width: 1374px) 100vw, 1374px" /></a></p>
<p>Большинство новых исходных систем управляются через API, что означает, что они могут работать по принципу push/pull:</p>
<ul>
<li><strong>Логика push-уведомлений</strong> работает с использованием потоковых API, которые экспортируют события (обычно в формате JSON) в тот момент, когда они происходят в источнике.</li>
<li><strong>Логика извлечения (pull)</strong> работает очень похоже на то, что мы называем «пакетной обработкой», где наши конвейеры данных отвечают за запрос информации из исходной системы, используя параметры для уменьшения объема данных, перемещаемых через конвейеры данных (дельты).</li>
</ul>
<p>Сосредоточившись в основном на логике push, микросервисы, которые могут быть построены на многих языках, например, Python, .NET, Java и т. д., будут отвечать за подписку на конечные точки/вебхуки и передачу событий в определенный топик. Эти топики (topics) специфичны для каждого события и должны иметь уникальное соглашение об именовании или определение. Для этих частей некоторые технологии гарантируют постановку в очередь, репликацию и разбиение на разделы, например, Apache Kafka.</p>
<h1>YouTube Videos</h1>
<h2>10 ETL Design Patterns (Data Architecture | Data Warehouse)</h2>
<ul>
<li>Рассматриваются десять различных шаблонов проектирования ETL.</li>
<li>Знание этих шаблонов необходимо для создания хранилища данных.</li>
</ul>
<p><iframe title="10 ETL Design Patterns (Data Architecture | Data Warehouse)" width="1170" height="658" src="https://www.youtube.com/embed/EmBwKA6vI14?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>Data Architecture 101: The Modern Data Warehouse</h2>
<ul>
<li>Важность выбора стратегии для обработки данных.</li>
<li>Описание стереотипного подхода к современному хранилищу данных.</li>
<li>Процесс пакетной загрузки данных в озеро данных.</li>
</ul>
<p><iframe title="Data Architecture 101: The Modern Data Warehouse" width="1170" height="658" src="https://www.youtube.com/embed/NARrRbFKHeI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>What is a Headless Data Architecture?</h2>
<p><iframe title="What is a Headless Data Architecture?" width="1170" height="658" src="https://www.youtube.com/embed/dahs6jpCYQs?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>What is Data Pipeline? | Why Is It So Popular?</h2>
<p><iframe title="What is Data Pipeline? | Why Is It So Popular?" width="1170" height="658" src="https://www.youtube.com/embed/kGT4PcTEPP8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>Database vs Data Warehouse vs Data Lake | What is the Difference?</h2>
<p><iframe title="Database vs Data Warehouse vs Data Lake | What is the Difference?" width="1170" height="658" src="https://www.youtube.com/embed/-bSkREem8dM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h2>Data Lake Architecture</h2>
<p><iframe title="Data Lake Architecture" width="1170" height="658" src="https://www.youtube.com/embed/1x0uJzMd1Pg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<h1>Ссылки, использованные при написании этой статьи</h1>
<ul>
<li><a href="https://medium.com/@lou_adam/anatomy-of-a-data-platform-how-to-choose-your-data-architecture-bc36472e7783" target="_blank" rel="noopener">Anatomy of a Data Platform — How to choose your data architecture</a></li>
<li><a href="https://www.kai-waehner.de/blog/2021/09/23/real-time-kappa-architecture-mainstream-replacing-batch-lambda/" target="_blank" rel="noopener">Kappa Architecture is Mainstream Replacing Lambda</a></li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/data-pipeline-design-patterns/">Data Pipeline Design Patterns &#8212; Шаблоны проектирования потоков данных</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/data-pipeline-design-patterns/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 2 &#171;Типы архитектур данных&#187;</title>
		<link>https://datatalks.ru/types-of-data-architectures/</link>
					<comments>https://datatalks.ru/types-of-data-architectures/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Tue, 07 Jan 2025 07:14:24 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[data architecture]]></category>
		<category><![CDATA[data fabric]]></category>
		<category><![CDATA[data lake]]></category>
		<category><![CDATA[data lakehouse]]></category>
		<category><![CDATA[data mesh]]></category>
		<category><![CDATA[modern data warehouse]]></category>
		<category><![CDATA[relational data warehouse]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=520</guid>

					<description><![CDATA[<p>Перевод второй главы из книги Deciphering Data Architectures Глава 2 &#171;Типы архитектур данных&#187; Крайне важно с самого начала уделить время проектированию и созданию правильной архитектуры данных. Я усвоил это на собственном опыте в начале своей карьеры. Мне так хотелось поскорее приступить к разработке своего решения, что я упустил из виду важные решения относительно архитектуры и [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/types-of-data-architectures/">Глава 2 &#171;Типы архитектур данных&#187;</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод второй главы из книги Deciphering Data Architectures</em></p>
<h1>Глава 2 &#171;Типы архитектур данных&#187;</h1>
<p>Крайне важно с самого начала уделить время проектированию и созданию правильной архитектуры данных. Я усвоил это на собственном опыте в начале своей карьеры. Мне так хотелось поскорее приступить к разработке своего решения, что я упустил из виду важные решения относительно архитектуры и выбора продуктов. Спустя три месяца после начала проекта я понял, что архитектура не поддерживает некоторые из необходимых источников данных. Нам фактически пришлось начать проект заново, разработав новую архитектуру и выбрав другие инструменты, что привело к огромным временным и финансовым потерям. Без правильного планирования пользователи не смогут получить ценность от вашего решения, будут недовольны нарушением сроков, а ваша компания рискует всё больше отставать от конкурентов.</p>
<p><strong>При создании решения для работы с данными вам нужен тщательно продуманный план.</strong> Здесь и приходит на помощь архитектура данных. <span style="color: #ff6600;"><strong>Архитектура данных определяет высокоуровневый подход и концепцию, описывает набор технологий и указывает поток данных, который будет использоваться для создания вашего решения для работы с большими данными.</strong></span> Выбор подходящей архитектуры может быть весьма сложной задачей, так как не существует универсального решения. Вы не найдёте в книге готовый перечень архитектур с соответствующими продуктами. Нет простого алгоритма с деревом решений, который приведёт вас к идеальной архитектуре. Ваш подход к архитектуре и используемые технологии будут сильно варьироваться в зависимости от клиентов и сценариев использования.</p>
<p>Ключевые типы высокоуровневых архитектурных подходов и концепций – именно то, о чём эта книга. Это не готовый перечень решений, но он даст вам общее представление о каждой из них. Хотя полезно классифицировать архитектуры данных по их характеристикам, что я и сделаю в этой книге, это не значит, что вам нужно выбирать из заранее определённых шаблонов. Каждая архитектура данных уникальна и требует индивидуального подхода для удовлетворения конкретных бизнес-потребностей.</p>
<p><strong>Архитектура данных</strong> – это общая концепция проектирования и организации данных в информационной системе. Использование заранее определённых шаблонов архитектуры может показаться лёгким способом быстро настроить новую систему. Однако такие шаблоны часто не учитывают специфические требования и ограничения системы, для которой они применяются, что может привести к проблемам с качеством данных, производительностью системы и её обслуживанием. Кроме того, потребности организации и системы обработки данных, скорее всего, будут меняться со временем, требуя обновлений и корректировок архитектуры данных. Стандартизированный шаблон может оказаться недостаточно гибким, чтобы адаптироваться к этим изменениям, что создаст неэффективность и ограничения в системе.</p>
<p>Эта глава предлагает <strong>краткий обзор основных типов архитектур</strong>, которые будут подробно рассмотрены в книге:</p>
<ul>
<li>реляционные хранилища данных (relational data warehouse),</li>
<li>озёра данных (data lake),</li>
<li>современные хранилища данных (modern data warehouse),</li>
<li>фабрика данных (data fabric),</li>
<li>озеро-хранилище данных или гибридное хранилище данных (data lakehouse),</li>
<li>и сетка данных (data mesh).</li>
</ul>
<p>Каждому типу будет посвящена отдельная глава с детальным описанием.</p>
<h2>Эволюция архитектур данных</h2>
<p>Реляционная база данных хранит данные в структурированном виде, где связи между элементами данных определяются с помощью ключей. Данные обычно организованы в таблицы, каждая из которых состоит из строк и столбцов. Каждая строка представляет отдельный экземпляр данных, а каждый столбец — конкретный атрибут данных.</p>
<p>Реляционные базы данных предназначены для работы со структурированными данными и предоставляют инструментарий для создания, изменения и запроса данных с использованием стандартизированного языка, известного как <strong>SQL (Structured Query Language)</strong>. Реляционная модель была впервые предложена Эдгаром Ф. Коддом в 1970 году, и с середины 1970-х годов она стала доминирующей моделью для систем управления базами данных. Большинство операционных приложений требуют постоянного хранения данных, и для этого чаще всего используются реляционные базы данных.</p>
<p>В реляционных базах данных, где на первом месте стоят согласованность и целостность данных, используется подход, называемый <span style="color: #ff6600;"><strong>schema-on-write (схема на запись)</strong></span>. <strong>Схема</strong> — это формальная структура, которая определяет организацию и взаимосвязи между таблицами, полями, типами данных и ограничениями. Она служит чертежом для хранения и управления данными, обеспечивая их согласованность, целостность и эффективную организацию внутри базы данных. Реляционные базы данных (и реляционные хранилища данных) требуют некоторой предварительной работы перед тем, как данные попадут в систему. Сначала необходимо создать базу данных, её таблицы, поля и схему, а затем написать код для передачи данных в базу. <span style="color: #ff6600;"><strong>При подходе schema-on-write</strong></span> схема данных определяется и применяется на этапе записи или загрузки данных в базу. Данные должны соответствовать заранее определённой схеме, включая типы данных, ограничения и связи, прежде чем они могут быть сохранены.</p>
<p>Для сравнения, <strong>при подходе schema-on-read</strong> схема применяется на этапе чтения или доступа к данным, а не записи. Данные могут загружаться в систему хранения без строгого соответствия схеме, и структура определяется только при запросе или использовании данных. Этот подход обеспечивает большую гибкость при работе с неструктурированными или полуструктурированными данными и обычно используется в озёрах данных, которые будут рассмотрены позже в этой главе.</p>
<p>На высоком уровне архитектуры данных предоставляют рамки для организации и управления данными, чтобы удовлетворять потребности организации.</p>
<p><strong>Это включает:</strong></p>
<ul>
<li>Определение способов сбора, хранения, обработки и доступа к данным.</li>
<li>Поддержание качества, безопасности и конфиденциальности данных.</li>
</ul>
<h2>Общие элементы архитектур данных</h2>
<h3><strong>Хранение данных</strong></h3>
<p>Все архитектуры данных должны указывать, как данные хранятся, включая физический носитель (например, жёсткие диски или облачное хранилище) и структуры данных, используемые для их организации.</p>
<h3><strong>Обработка данных</strong></h3>
<p>Архитектуры данных должны определять, как обрабатываются данные, включая трансформации или вычисления, выполняемые над данными до их хранения или анализа.</p>
<h3><strong>Доступ к данным</strong></h3>
<p>Архитектуры данных должны предоставлять механизмы доступа к данным, включая пользовательские интерфейсы и API, которые позволяют запрашивать и анализировать данные.</p>
<h3><strong>Безопасность и конфиденциальность данных</strong></h3>
<p>Архитектуры данных должны включать механизмы для обеспечения безопасности и конфиденциальности данных, такие как контроль доступа, шифрование и маскирование данных.</p>
<h3><strong>Управление данными</strong></h3>
<p>Архитектуры данных должны предоставлять рамки для управления данными, включая стандарты качества, отслеживание происхождения данных и политики хранения.</p>
<h2>Основная цель архитектуры данных</h2>
<p><strong>Главная задача архитектуры данных</strong> — позволить организации эффективно управлять своими данными и использовать их для поддержки бизнес-целей и процесса принятия решений.</p>
<p>В Таблице 2-1 приведено высокоуровневое сравнение характеристик архитектур данных, описанных в этой книге. Это поможет вам начать определять, какая архитектура может лучше всего подойти для вашего сценария использования.</p>
<p><strong>Таблица 2-1. Сравнение архитектур данных</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-617 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures.png" alt="" width="1054" height="950" srcset="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures.png 1054w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures-300x270.png 300w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures-1024x923.png 1024w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures-768x692.png 768w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures-450x406.png 450w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures-780x703.png 780w" sizes="(max-width: 1054px) 100vw, 1054px" /></a></p>
<p>или переведенная версия таблицы:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-619 size-full" src="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated.png" alt="" width="1082" height="776" srcset="https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated.png 1082w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated-300x215.png 300w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated-1024x734.png 1024w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated-768x551.png 768w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated-450x323.png 450w, https://datatalks.ru/wp-content/uploads/2025/01/datatalks_Comparison_of_data_architectures_translated-780x559.png 780w" sizes="(max-width: 1082px) 100vw, 1082px" /></a></p>
<h2>Реляционное хранилище данных (Relational Data Warehouse, RDW)</h2>
<p>Реляционные базы данных были основным инструментом хранения данных в течение нескольких десятилетий. Первое реляционное хранилище данных, использовавшееся в производстве, было разработано в Стэнфордском университете доктором Джеком Э. Шемером, основателем корпорации Teradata в 1979 году. Первую систему Teradata установил банк Wells Fargo в 1983 году для анализа финансовых данных.</p>
<p>По мере того как организации начали генерировать огромные объёмы данных, становилось всё сложнее обрабатывать и анализировать эти данные без значительных задержек. Ограничения реляционных баз данных привели к созданию <strong>реляционных хранилищ данных (RDW)</strong>, которые стали популярными в конце 1980-х годов, примерно через 15 лет после появления реляционных баз данных.</p>
<p><strong>Реляционное хранилище данных (RDW)</strong> — это специфический тип реляционной базы данных, предназначенный для приложений хранилища данных и бизнес-аналитики, с оптимизированной производительностью запросов и поддержкой крупномасштабного анализа данных. Хотя как реляционные хранилища, так и транзакционные системы используют реляционную модель для организации данных, хранилища данных обычно больше по масштабу и оптимизированы для аналитических запросов.</p>
<p>RDW имеет вычислительный механизм (compute engine) и хранилище. Вычислительный механизм предоставляет мощность для выполнения запросов к данным, а хранилище — это реляционное хранилище, организованное в таблицы, строки и столбцы. Мощность RDW может использоваться только для данных, находящихся в его реляционном хранилище — они связаны друг с другом.</p>
<p><strong>Ключевые функции RDW включают:</strong></p>
<ul>
<li>Поддержку транзакций (обеспечение надёжной и согласованной обработки данных).</li>
<li>Журналирование операций (запись всей активности в системе).</li>
<li>Принудительное соблюдение схемы (гарантия того, что данные организованы и структурированы в соответствии с заданной схемой).</li>
</ul>
<p>В 1970–1980-х годах реляционные базы данных использовались для операционных приложений, таких как обработка заказов и управление запасами. Эти приложения называются <strong>OLTP-системами (Online Transaction Processing)</strong>. Они позволяют создавать, читать, обновлять и удалять данные в базе — операции, известные как <strong>CRUD (Create, Read, Update, Delete)</strong>.</p>
<p>CRUD-операции требуют быстрой реакции приложений, чтобы пользователи не испытывали неудобств из-за медленных обновлений данных. Запросы и отчёты в реляционной базе, используемой операционными приложениями, могут сильно загружать ресурсы и конфликтовать с другими CRUD-операциями, замедляя всю систему.</p>
<p>RDW были изобретены, чтобы решить эту проблему. Данные из реляционной базы копируются в хранилище данных, где пользователи могут выполнять запросы и отчёты без нагрузки на операционную систему, как показано на Рисунке 2-1.</p>
<p><strong>Рисунок 2-1. Data warehousing</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-605 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing.png" alt="" width="786" height="467" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing.png 786w, https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing-300x178.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing-768x456.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing-450x267.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_1_data_warehousing-780x463.png 780w" sizes="(max-width: 786px) 100vw, 786px" /></a></p>
<h2>Озеро данных</h2>
<p><strong>Озеро данных (Data Lake)</strong> — более современная концепция, впервые появившаяся около 2010 года. Озеро данных можно представить как усовершенствованную файловую систему, аналогичную файловой системе на вашем ноутбуке. В отличие от реляционного хранилища данных, озеро данных — это только хранилище, без вычислительного механизма.</p>
<p>Однако с озёрами данных могут работать множество вычислительных систем, благодаря чему обработка данных обходится дешевле, чем в RDW. Ещё одно отличие заключается в том, что RDW используют реляционное хранилище, где данные структурированы в строки и столбцы, а озёра данных используют объектное хранилище, где структура не требуется.</p>
<p>Технология хранения озёр данных началась с <strong>HDFS (Hadoop Distributed File System)</strong>, бесплатной технологии с открытым исходным кодом, популярной в начале 2010-х годов и размещаемой почти исключительно локально. <strong>HDFS</strong> — это масштабируемая, отказоустойчивая распределённая система хранения, работающая на обычном оборудовании.</p>
<p>С развитием облачных технологий озёра данных переместились в облако, где используются другие типы хранения. Сегодня большинство озёр данных находится в облаке.</p>
<p>В отличие от RDW, <strong>озёра данных используют <span style="color: #ff6600;">подход schema-on-read</span></strong>, что означает отсутствие необходимости заранее готовить данные для загрузки в озеро. Данные сохраняются в исходном (сыром) формате, как они поступают из исходной системы. Например, экспортированные из реляционной базы данные в формате CSV могут быть загружены в озеро данных без изменений. Для загрузки этих данных в RDW их пришлось бы преобразовать в строки и столбцы таблицы.</p>
<p>При загрузке файла данных в озеро его схема может отсутствовать или находиться в отдельном файле. Поэтому схема определяется при чтении, либо создаётся, либо извлекается из файла, как показано на Рисунке 2-2.</p>
<p>Данные из различных источников — операционных баз данных, датчиков, социальных сетей — могут попадать в озеро данных. Эти файлы могут содержать <strong>структурированные</strong> (например, реляционные базы данных), <strong>полуструктурированные</strong> (CSV, JSON, XML) и <strong>неструктурированные</strong> (электронная почта, документы, PDF) данные, а также <strong>двоичные данные</strong> (изображения, аудио, видео).</p>
<p><strong>Рисунок 2-2. Озеро данных</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-606 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake.png" alt="" width="949" height="721" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake.png 949w, https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake-300x228.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake-768x583.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake-450x342.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_2_data_lake-780x593.png 780w" sizes="(max-width: 949px) 100vw, 949px" /></a></p>
<p>Озёра данных изначально создавались как решение всех проблем реляционных хранилищ данных, таких как высокая стоимость, ограниченная масштабируемость, низкая производительность, сложность подготовки данных и ограниченная поддержка сложных типов данных. Компании, продававшие Hadoop и озёра данных, такие как Cloudera, Hortonworks и MapR, представляли их как волшебное решение, которое автоматически копирует, очищает и делает данные доступными для конечных пользователей. Они утверждали, что озёра данных могут полностью заменить реляционные хранилища данных, предлагая подход «одна технология для всего». Многие компании решили сэкономить, используя бесплатные инструменты с открытым исходным кодом для всех своих нужд.</p>
<p>Однако на практике запросы к озёрам данных оказались далеко не такими простыми: для работы с ними требовались серьёзные навыки. ИТ-отделы говорили конечным пользователям: «Мы скопировали все данные, которые вам нужны, в озеро данных. Просто откройте Jupyter Notebook, используйте Hive и Python, чтобы создавать отчёты с файлами из этих папок». Такой подход провалился, поскольку у большинства пользователей не было необходимых навыков для выполнения этих задач.</p>
<p>Компании на собственном опыте убедились, что такие сложные и трудные в использовании решения оказывались дороже из-за высоких затрат на оборудование и поддержку, задержек в производстве и снижения производительности. Кроме того, озёра данных не обладали такими функциями, как поддержка транзакций, строгое соблюдение схемы и журналирование операций, которые ценились в хранилищах данных. В результате две из трёх ведущих компаний, продававших озёра данных (Hortonworks и MapR), обанкротились.</p>
<p>Тем не менее озёра данных не исчезли. <strong>Их роль трансформировалась: теперь они используются для промежуточного хранения и подготовки данных.</strong> Эта тема подробно рассматривается в главе 5.</p>
<h2>Modern Data Warehouse (Современное хранилище данных)</h2>
<p>Реляционные хранилища данных и озёра данных сами по себе являются простыми архитектурами. Они используют только одну технологию для централизации данных и редко включают дополнительные продукты. Однако, добавляя больше технологий и инструментов для поддержки реляционных хранилищ или озёр данных, можно создать более сложные архитектуры, описанные в этой и следующих главах.</p>
<p>Озёра данных не смогли заменить реляционные хранилища данных, но остались полезными для промежуточного хранения и подготовки данных. Почему бы не объединить преимущества обоих подходов?</p>
<p>Примерно с 2011 года многие компании начали разрабатывать архитектуры, в которых озёра данных существуют рядом с реляционными хранилищами данных. Такой подход сформировал архитектуру, известную как <strong>современное хранилище данных (Modern Data Warehouse, MDW)</strong>, изображённое на Рисунке 2-3.</p>
<p><strong>Рисунок 2-3. Современное хранилище данных (MDW)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-607 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW.png" alt="" width="1412" height="276" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW.png 1412w, https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW-300x59.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW-1024x200.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW-768x150.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW-450x88.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_3_modern_data_warehouse_MDW-780x152.png 780w" sizes="(max-width: 1412px) 100vw, 1412px" /></a></p>
<p><strong>Современное хранилище данных сочетает лучшие стороны обоих подходов:</strong> <span style="color: #ff6600;"><strong>озеро данных</strong></span> используется для промежуточного хранения и подготовки данных, а также для построения моделей машинного обучения. <span style="color: #ff6600;"><strong>Хранилище данных</strong></span> служит для управления, безопасности и соблюдения нормативных требований, а бизнес-пользователи используют его для запросов и отчётов. Подробности о современных архитектурах хранилищ данных можно найти в главе 10.</p>
<h2>Data Fabric (Фабрика данных)</h2>
<p>Архитектура Data Fabric начала появляться около 2016 года. Фабрика данных можно рассматривать как эволюцию архитектуры современного хранилища данных, в которую добавлено больше технологий для работы с дополнительными источниками данных, обеспечения безопасности и упрощения доступа к данным. Также были улучшены процессы загрузки, трансформации, выполнения запросов, поиска и доступа к данным, как показано на Рисунке 2-4. С таким количеством дополнений система превращается в &#171;фабрику&#187; — большую инфраструктуру, способную обрабатывать любые виды данных. Более подробно эта тема раскрывается в Главе 11.</p>
<p><strong>Рисунок 2-4. Data fabric</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-608" src="https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric.png" alt="" width="1414" height="538" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric.png 1414w, https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric-300x114.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric-1024x390.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric-768x292.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric-450x171.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_4_data_fabric-780x297.png 780w" sizes="(max-width: 1414px) 100vw, 1414px" /></a></p>
<h2>Озеро-хранилище данных (Data Lakehouse)</h2>
<p>Термин озеро-хранилище данных представляет собой смесь (портманто) понятий &#171;озеро данных&#187; и &#171;хранилище данных&#187;. Такие архитектуры начали набирать популярность около 2020 года, когда компания Databricks впервые ввела этот термин.</p>
<p>Идея гибридного озера-хранилища заключается в том, чтобы отказаться от реляционного хранилища данных и использовать в архитектуре только одно хранилище — озеро данных. Все типы данных — структурированные, полуструктурированные и неструктурированные — загружаются в озеро данных, и все запросы и отчёты выполняются на его основе.</p>
<p>Вы можете подумать: «Постойте. Ведь изначально озёра данных уже использовали этот подход, и он провалился! Что изменилось?» Ответ, как показано на Рисунке 2-5, заключается <strong>в добавлении транзакционного уровня хранения поверх существующего озера данных.</strong> Этот слой делает озеро данных более похожим на реляционную базу данных. Среди популярных решений с открытым исходным кодом для такого уровня — <strong>Delta Lake</strong>, <strong>Apache Iceberg</strong> и <strong>Apache Hudi</strong>.</p>
<p>Все эти технологии подробно рассматриваются в Главе 12.</p>
<p><strong>Рисунок 2-5. Data LakeHouse</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-613 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse.png" alt="" width="1414" height="538" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse.png 1414w, https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse-300x114.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse-1024x390.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse-768x292.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse-450x171.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_5_data_lakehouse-780x297.png 780w" sizes="(max-width: 1414px) 100vw, 1414px" /></a></p>
<h2>Архитектура &#171;Сетка данных&#187; (Data Mesh)</h2>
<p><strong>Термин архитектура &#171;Сетка данных&#187;</strong> был впервые представлен в блоге Заамак Дехгани (Zhamak Dehghani), основательницы и генерального директора Nextdata, в мае 2019 года. Позже, в декабре 2020 года, Дехгани уточнила концепцию и выделила четыре основные принципы архитектуры Сетки данных. С тех пор эта тема вызывает огромный интерес, обсуждаясь в блогах, на конференциях и в СМИ. Архитектура даже вошла в цикл Gartner Hype Cycle для управления данными. Несмотря на ажиотаж, подход подходит лишь для ограниченного числа сценариев.</p>
<p>Современное хранилище данных, фабрика данных и озеро-хранилище данных используют централизованный подход, предполагающий копирование операционных данных в центральное хранилище, контролируемое ИТ-отделом.</p>
<p><strong>Однако такая централизация вызывает три основные проблемы:</strong></p>
<ul>
<li>Владение данными. Кому принадлежат данные?</li>
<li>Качество данных. Кто отвечает за их чистоту?</li>
<li>Масштабируемость. Как масштабировать организацию и технологии?</li>
</ul>
<p>Цель <strong>Data Mesh</strong> — решить эти проблемы.</p>
<p>В <strong>Data Mesh</strong> остаются распределёнными по различным доменам внутри компании, таким как производство, продажи или поставки (правая часть Рисунка 2-6). Каждый домен имеет собственную мини-команду ИТ, которая отвечает за данные: их очистку, создание аналитических данных и обеспечение доступа. У каждого домена есть своя инфраструктура для вычислений и хранения. Это создаёт децентрализованную архитектуру, где данные, люди и инфраструктура масштабируются вместе с ростом числа доменов.</p>
<p><strong>Рисунок 2-6. Data mesh (архитектура &#171;Сетка данных&#187;)</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-609" src="https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh.png" alt="" width="1401" height="433" srcset="https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh.png 1401w, https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh-300x93.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh-1024x316.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh-768x237.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh-450x139.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/2_6_data_mesh-780x241.png 780w" sizes="(max-width: 1401px) 100vw, 1401px" /></a></p>
<p><strong>Важно понимать, что Data Mesh — это концепция, а не технология.</strong> Нельзя просто купить &#171;сетку данных в коробке&#187;. Её реализация требует значительных организационных и культурных изменений, к которым большинство компаний не готовы. Более того, она подходит только для крупных организаций.</p>
<p>Для её построения нужно определить, какие существующие технологии можно использовать, а какие придётся создавать с нуля. Каждый домен самостоятельно выбирает технологии для своей части сетки, включая такие варианты, как современное хранилище данных, фабрика данных или озеро-хранилище данных.</p>
<p>Все аспекты архитектуры <strong>Data Mesh</strong> рассматриваются в Главах 13 и 14.</p>
<h2>Резюме</h2>
<p>Теперь вы получили общее представление о типах архитектур данных. Следующая глава расскажет о том, как выбрать подходящую архитектуру для своих нужд: этот процесс называется сессией проектирования архитектуры.</p>
<p>Сообщение <a href="https://datatalks.ru/types-of-data-architectures/">Глава 2 &#171;Типы архитектур данных&#187;</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/types-of-data-architectures/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 1 &#171;Big Data&#187; &#8212; Data Architectures</title>
		<link>https://datatalks.ru/big-data-data-architectures/</link>
					<comments>https://datatalks.ru/big-data-data-architectures/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Mon, 06 Jan 2025 07:11:10 +0000</pubDate>
				<category><![CDATA[Big Data]]></category>
		<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[big data]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=517</guid>

					<description><![CDATA[<p>Перевод первой главы Big Data. Big Data &#8212; Большие данные Число компаний, создающих архитектуры данных, значительно увеличилось в 2020-х годах. Этот рост вряд ли замедлится в ближайшее время, главным образом благодаря тому, что объем доступных данных достиг беспрецедентного уровня. Данные поступают из социальных сетей, устройств Интернета вещей (IoT), собственных приложений и программного обеспечения сторонних разработчиков, [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/big-data-data-architectures/">Глава 1 &#171;Big Data&#187; &#8212; Data Architectures</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Перевод первой главы Big Data.</p>
<h1>Big Data &#8212; Большие данные</h1>
<p>Число компаний, создающих архитектуры данных, значительно увеличилось в 2020-х годах. Этот рост вряд ли замедлится в ближайшее время, главным образом благодаря тому, что объем доступных данных достиг беспрецедентного уровня. Данные поступают из социальных сетей, устройств Интернета вещей (IoT), собственных приложений и программного обеспечения сторонних разработчиков, чтобы назвать лишь несколько источников.</p>
<p>Согласно исследованию BCG, проведенному в 2023 году, <strong><em>«объем генерируемых данных примерно удвоился с 2018 по 2021 годы, достигнув около 84 зеттабайт, и ожидается, что темпы роста сохранятся»</em></strong>. Исследователи прогнозируют, что «объем данных будет увеличиваться с совокупным среднегодовым темпом роста (CAGR) в 21% с 2021 по 2024 год, достигнув 149 зеттабайт». Компании понимают, что могут сэкономить миллионы долларов и увеличить доходы, собирая эти данные и используя их для анализа прошлого и настоящего, а также для прогнозирования будущего. Но для этого им необходим способ хранения всех этих данных.</p>
<p>По всему деловому миру идет гонка за скорейшее создание архитектур данных. Эти архитектуры должны быть готовы к обработке любых будущих данных — независимо от их объема, скорости или типа — и при этом обеспечивать их точность. А тем, кто работает с архитектурами данных, необходимо четкое понимание того, как они функционируют и какие существуют варианты. Именно для этого и предназначена эта книга.</p>
<p>Я видел на собственном опыте, к чему может привести недостаток понимания концепций архитектуры данных. Одна известная мне компания построила архитектуру данных стоимостью 100 миллионов долларов за два года, но выяснилось, что она использовала неверные технологии, была слишком сложной в использовании и недостаточно гибкой для обработки определенных типов данных. В результате архитектуру пришлось полностью демонтировать и создавать заново. Не позволяйте этому случиться с вами!</p>
<p>Все сводится к тому, чтобы доставить нужную информацию нужным людям в нужное время и в нужном формате. Для этого вам нужна архитектура данных, которая может принимать, хранить, преобразовывать и моделировать данные (обработка больших данных), чтобы их можно было точно и легко использовать. Такая архитектура должна позволять любому конечному пользователю, даже обладающему минимальными техническими знаниями, анализировать данные и генерировать отчеты и дашборды, не полагаясь на специалистов из IT-отдела с глубокими техническими навыками.</p>
<p>Первая глава начинается с введения в концепцию больших данных и некоторые их фундаментальные аспекты. Затем я рассказываю, как компании используют свои данные, с акцентом на бизнес-аналитику и то, как это использование эволюционирует с развитием архитектуры данных компании.</p>
<h2>Что такое большие данные и как они могут вам помочь?</h2>
<p>Несмотря на то что термин «большие» используется применительно к данным, речь идет не только о размере данных. Речь также обо всех данных, больших или малых, как внутри вашей компании, так и за ее пределами, которые могли бы быть вам полезны. Данные могут быть в любом формате и собираться с любой регулярностью.</p>
<p>Таким образом, лучший способ определить большие данные — это рассматривать их как все данные, независимо от их размера (объем), скорости (скорость поступления) или типа (разнообразие). Помимо этих критериев, есть еще три фактора, которые можно использовать для описания данных: достоверность (veracity), изменчивость (variability) и ценность (value). Вместе они известны как «шесть V» больших данных, как показано на рисунке 1-1.</p>
<p><strong>Рисунок 1-1. Шесть &#171;V&#187; больших данных (источник: The Cloud Data Lake Рукмани Гопалан [O’Reilly, 2023]).</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-566 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake.png" alt="" width="1154" height="722" srcset="https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake.png 1154w, https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake-300x188.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake-1024x641.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake-768x480.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake-450x282.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/six_V_big_data_data_lake-780x488.png 780w" sizes="(max-width: 1154px) 100vw, 1154px" /></a></p>
<p>Рассмотрим каждую из них подробнее.</p>
<h3><strong>Объем (Volume)</strong></h3>
<p><strong>Объем</strong> — это огромное количество данных, которые генерируются и сохраняются. Это может быть от терабайтов до петабайтов данных, поступающих из множества источников, включая социальные сети, транзакции в электронной коммерции, научные эксперименты, данные с датчиков IoT-устройств и многое другое. Например, данные из системы ввода заказов могут составлять несколько терабайтов в день, в то время как IoT-устройства могут генерировать миллионы событий в минуту и создавать сотни терабайтов данных ежедневно.</p>
<h3><strong>Разнообразие (Variety)</strong></h3>
<p><strong>Разнообразие</strong> описывает широкий спектр источников и форматов данных. Данные можно разделить на:</p>
<ul>
<li>структурированные (например, из реляционных баз данных),</li>
<li>полуструктурированные (такие как логи, форматы CSV, XML и JSON),</li>
<li>неструктурированные (например, электронные письма, документы, PDF),</li>
<li>бинарные данные (изображения, аудио, видео).</li>
</ul>
<p>Например, данные из системы ввода заказов будут структурированными, так как они поступают из реляционной базы данных, а данные от IoT-устройства, скорее всего, будут представлены в формате JSON.</p>
<h3><strong>Скорость (Velocity)</strong></h3>
<p><strong>Скорость</strong> касается темпов генерации и обработки данных. Нечастый сбор данных называется пакетной обработкой (batch processing); например, заказы за день собираются и обрабатываются ночью. Данные также могут собираться с высокой частотой или в реальном времени, особенно если они генерируются с высокой скоростью, как данные из социальных сетей, IoT-устройств или мобильных приложений.</p>
<h3><strong>Достоверность (Veracity)</strong></h3>
<p><strong>Достоверность</strong> — это точность и надежность данных. Большие данные поступают из множества разнообразных источников. Ненадежные или неполные источники могут ухудшать качество данных. Например, если данные поступают с IoT-устройства, такого как наружная камера безопасности, которая должна обнаруживать человека, погодные условия могут привести к ложным срабатываниям, что искажает данные. Поэтому данные нужно валидировать при получении.</p>
<h3><strong>Изменчивость (Variability)</strong></h3>
<p><strong>Изменчивость</strong> касается согласованности (или несогласованности) данных в их формате, качестве и значении. Обработка структурированных, полуструктурированных и неструктурированных данных требует разных инструментов и методов. Например, тип, частота и качество данных с датчиков IoT могут значительно варьироваться. Датчики температуры и влажности могут генерировать данные с регулярными интервалами, в то время как датчики движения — только при обнаружении движения.</p>
<h3><strong>Ценность (Value)</strong></h3>
<p><strong>Ценность</strong>, самый важный &#171;V&#187;, относится к полезности и релевантности данных. Компании используют большие данные для получения инсайтов и принятия решений, которые могут привести к бизнес-выгоде, такой как повышение эффективности, снижение затрат или создание новых источников дохода. Например, анализируя данные о клиентах, организации могут лучше понять их поведение, предпочтения и потребности. Это позволяет разрабатывать более целевые маркетинговые кампании, улучшать клиентский опыт и увеличивать продажи.</p>
<h2>Collecting big data (Сбор больших данных)</h2>
<p>Сбор больших данных позволяет компаниям получать инсайты для улучшения бизнес-решений.</p>
<p><strong> Прогнозная аналитика (Predictive Analysis)</strong> — это вид анализа данных, который использует статистические алгоритмы и машинное обучение для анализа исторических данных и прогнозирования будущих событий и трендов. Это позволяет бизнесу действовать проактивно, а не реактивно.</p>
<p>Вы, вероятно, слышали, как многие компании называют данные «новой нефтью», поскольку в современной цифровой экономике они стали столь же ценным ресурсом, каким нефть была в индустриальной эпохе.</p>
<p><strong>Данные подобны нефти по нескольким причинам:</strong></p>
<ul>
<li><strong>Они являются сырьем</strong>, которое нужно извлечь, переработать и обработать, чтобы оно стало полезным. В случае с данными это включает сбор, хранение и анализ для получения инсайтов, которые могут быть использованы для принятия бизнес-решений.</li>
<li><strong>Они чрезвычайно ценны.</strong> Компании, которые собирают и анализируют большие объемы данных, могут использовать их для улучшения своих продуктов и услуг, принятия более качественных решений и получения конкурентного преимущества.</li>
<li><strong>Они могут использоваться по-разному.</strong> Например, данные можно использовать для обучения алгоритмов машинного обучения, которые затем применяются для автоматизации задач, выявления закономерностей и создания прогнозов.</li>
<li><strong>Они являются мощным ресурсом, способным трансформировать общество.</strong> Широкое использование нефти способствовало росту индустрий и появлению новых технологий, в то время как данные привели к развитию искусственного интеллекта, машинного обучения и прогнозной аналитики.</li>
<li><strong>Они являются источником власти и влияния</strong> благодаря всем вышеуказанным факторам.</li>
</ul>
<p>Например, с помощью больших данных можно создавать отчеты и дашборды, которые покажут, где продажи отстают, и принимать меры &#171;задним числом&#187; для их улучшения. Можно также использовать машинное обучение для прогнозирования, где продажи будут падать в будущем, и принимать проактивные меры для предотвращения этого падения. Это называется <strong>бизнес-аналитикой (BI)</strong> — процессом сбора, анализа и использования данных для помощи бизнесу в принятии более информированных решений.</p>
<p>Как показано на Рисунке 1-2, данные можно собирать из новых источников, таких как <strong>IoT-устройства</strong>, <strong>веб-логи</strong> и <strong>социальные сети</strong>, а также из старых источников, таких как приложения <strong>ERP</strong>, <strong>CRM</strong> и <strong>системы управления операциями</strong>. Эти данные могут быть в различных форматах, таких как файлы <strong>CSV</strong>, <strong>JSON</strong> или <strong>Parquet</strong>. Они могут поступать пакетно (например, раз в час) или стримингом в реальном времени с частотой несколько раз в секунду.</p>
<p><strong>Рисунок 1-2. Обработка больших данных (Источник: The Cloud Data Lake Рукмани Гопалан [O’Reilly, 2023])</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-567 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing.png" alt="" width="1143" height="861" srcset="https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing.png 1143w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-300x226.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-1024x771.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-768x579.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-280x210.png 280w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-450x339.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/big_data_processing-780x588.png 780w" sizes="(max-width: 1143px) 100vw, 1143px" /></a></p>
<p>Важно, чтобы компании понимали, где они находятся на своем пути использования данных относительно других. Это называется <strong>зрелостью данных (data maturity)</strong>, и в следующем разделе описаны этапы этого пути, чтобы вы могли определить, на каком уровне находится ваша компания.</p>
<h2>Зрелость данных (Data Maturity)</h2>
<p>Вы, возможно, слышали термин <strong>&#171;цифровая трансформация&#187;</strong>, который описывает процесс внедрения технологий в бизнес-процессы компаний для их фундаментального изменения с целью получения ценности из данных, улучшения операций и предоставления ценности клиентам. Этот процесс предполагает отказ от традиционных, ручных или бумажных процессов в пользу цифровых, с использованием технологий для повышения эффективности, продуктивности и инновационности.</p>
<p>Большая часть этой трансформации связана с использованием данных для улучшения бизнеса, будь то создание профиля 360 для улучшения клиентского опыта или применение машинного обучения для ускорения и повышения точности производственных линий.</p>
<p>Эта <strong>цифровая трансформация делится на четыре стадии</strong>, называемые этапами зрелости данных в организации (см. Рисунок 1-3). Эти этапы описывают уровень развития и зрелости организации в управлении, использовании и извлечении ценности из данных. Модель позволяет оценить способности организации к управлению данными и готовность к применению аналитики, искусственного интеллекта и других инициатив на основе данных. Каждый этап отражает шаг вперед в использовании данных для извлечения бизнес-ценности и принятия решений.</p>
<p><strong>Рисунок 1-3. Этапы зрелости данных в организации</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages.png" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="aligncenter wp-image-568 size-full" src="https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages.png" alt="" width="1224" height="639" srcset="https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages.png 1224w, https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages-300x157.png 300w, https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages-1024x535.png 1024w, https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages-768x401.png 768w, https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages-450x235.png 450w, https://datatalks.ru/wp-content/uploads/2024/12/Enterprise_data_maturity_stages-780x407.png 780w" sizes="(max-width: 1224px) 100vw, 1224px" /></a></p>
<h3><strong>Этап 1: Реактивный (Reactive)</strong></h3>
<p>На первом этапе данные разрознены — скорее всего, они хранятся в Excel-таблицах и/или локальных базах данных на разных файловых системах и пересылаются по электронной почте. Архитекторы данных называют это &#171;spreadmart&#187; (сокращение от &#171;spreadsheet data mart&#187;) — неформализованное, децентрализованное хранилище данных, часто встречающееся в организациях, которые используют электронные таблицы для хранения, управления и анализа данных.</p>
<p>Spreadmart обычно создаются и поддерживаются индивидуально или командой, независимо от централизованных систем управления данными или официальных корпоративных хранилищ данных.</p>
<p><strong>Spreadmarts страдают от:</strong></p>
<ul>
<li>несоответствия данных,</li>
<li>отсутствия управления,</li>
<li>ограниченной масштабируемости,</li>
<li>неэффективности (из-за дублирования усилий).</li>
</ul>
<h3><strong>Этап 2: Информативный (Informative)</strong></h3>
<p><strong>На втором этапе зрелости компании начинают централизовать свои данные,</strong> что упрощает анализ и отчетность. Этапы 1 и 2 ориентированы на историческую отчетность (то есть на анализ трендов и событий из прошлого), поэтому их называют <strong>&#171;зеркалом заднего вида&#187; (rearview mirror)</strong>. На этих этапах вы реагируете на то, что уже произошло.</p>
<p>На втором этапе решения для сбора данных, как правило, не обладают высокой масштабируемостью. Обычно они ограничены в объемах и типах данных, которые могут обрабатывать, и способны загружать данные лишь с низкой частотой (например, раз в сутки).</p>
<p>Большинство компаний находятся именно на этом этапе, особенно если их инфраструктура остается on-premises (локальной).</p>
<h3><strong>Этап 3: Прогнозирующий (Predictive)</strong></h3>
<p>На третьем этапе компании переходят в облако и создают системы, способные обрабатывать большие объемы данных, различные типы данных и данные, поступающие чаще (например, каждый час или в режиме стриминга).</p>
<p><strong>На этом этапе также улучшается процесс принятия решений за счет внедрения машинного обучения (advanced analytics), что позволяет принимать решения в реальном времени.</strong></p>
<p>Например, пользователь заходит в интернет-магазин, и система рекомендует дополнительные книги на странице оформления заказа, основываясь на его предыдущих покупках.</p>
<h3><strong>Этап 4: Трансформирующий (Transformative)</strong></h3>
<p><strong>На последнем этапе компания создает решение, способное работать с любыми данными — независимо от их объема, скорости поступления или типа.</strong> Подключение новых данных становится легким и занимает минимальное время, так как архитектура системы спроектирована для обработки любых данных, а инфраструктура обладает достаточной мощностью для их поддержки.</p>
<p>На этом этапе конечные пользователи, не обладающие техническими навыками, могут легко создавать отчеты и дашборды с помощью инструментов по своему выбору.</p>
<p>Этапы 3 и 4 являются ключевыми в контексте этой книги. В частности, когда конечные пользователи самостоятельно занимаются созданием отчетов, это называется самообслуживанием в бизнес-аналитике (self-service BI), что является темой следующего раздела.</p>
<h2>Самообслуживание в бизнес-аналитике (Self-Service Business Intelligence)</h2>
<p>Ранее, если конечному пользователю в организации требовался отчет или дашборд, он должен был:</p>
<ol>
<li>Собрать все свои требования (какие источники данных необходимы и как должен выглядеть отчет или дашборд).</li>
<li>Заполнить форму запроса в IT.</li>
<li>Ждать.</li>
</ol>
<p>IT-отдел затем выполнял следующие шаги:</p>
<ol>
<li>Извлекал данные.</li>
<li>Загружал их в хранилище данных.</li>
<li>Создавал модель данных.</li>
<li>Наконец, генерировал отчет или дашборд.</li>
</ol>
<p>После этого конечный пользователь просматривал результат, либо утверждал его, либо запрашивал изменения. Этот процесс часто приводил к длинной очереди запросов, превращая IT в узкое место (bottleneck). Ожидание могло занимать дни, недели или даже месяцы, прежде чем пользователь получал реальную ценность из данных.</p>
<p><strong>Сегодня этот подход называется &#171;традиционной BI&#187; (traditional BI), так как на смену ему пришло самообслуживание BI.</strong></p>
<p>Цель любой архитектуры данных заключается в том, чтобы сделать процесс создания запросов, отчетов и дашбордов максимально быстрым и простым для любого пользователя, независимо от его технической подготовки. Пользователи не должны вовлекать IT для выполнения этих задач — они должны справляться с этим самостоятельно.</p>
<h3><strong>Как этого достичь?</strong></h3>
<p>IT-отдел должен предварительно провести анализ потребностей пользователей, выяснив, какие данные им необходимы.</p>
<p>На основании этих потребностей строится архитектура данных, ориентированная на конечных пользователей.</p>
<p>Такой подход требует больших усилий на этапе разработки, но окупается за счет значительного сокращения времени на создание отчетов. Этот процесс устраняет очереди запросов и необходимость постоянного взаимодействия с IT, сотрудники которого зачастую плохо понимают данные.</p>
<p>Вместо этого конечные пользователи, которые лучше всего знают данные, получают к ним прямой доступ, подготавливают их, строят модели данных, создают отчеты и проверяют их на корректность. Такой рабочий процесс гораздо продуктивнее.</p>
<p>Создание решения, которое легко воспринимается конечными пользователями, приводит к реализации концепции самообслуживания в BI. Создание отчета должно быть настолько простым, чтобы пользователи могли буквально перетаскивать поля в рабочей области. Им не нужно разбираться в том, как объединить данные из разных таблиц или беспокоиться о производительности отчета.</p>
<p>Когда вы разрабатываете решение для работы с данными, всегда задавайте себе вопрос:<br />
&#171;Насколько просто будет людям создавать собственные отчеты?&#187;</p>
<h2>Резюме</h2>
<p>В этой главе вы узнали, что такое большие данные и как они могут помочь вам и вашей организации принимать более взвешенные бизнес-решения, особенно в сочетании с машинным обучением. Вы изучили шесть характеристик больших данных (6 Vs) и поняли, что такое зрелость данных (data maturity) и как определить её этапы. Наконец, вы познакомились с различиями между традиционной бизнес-аналитикой (<strong>traditional BI</strong>) и самообслуживанием в BI (<strong>self-service BI</strong>), где цель заключается в том, чтобы каждый мог быстро и легко использовать данные для создания отчетов и выявления инсайтов.</p>
<p><strong>Теперь позвольте рассказать, чего ожидать в следующих главах.</strong></p>
<p><strong>Глава 2</strong> будет посвящена тому, что такое архитектура данных. Вы получите обзор изменений типов архитектур данных за последние годы.</p>
<p><strong>Глава 3</strong> покажет, как проводить сессии проектирования архитектуры, чтобы определить, какая архитектура данных лучше всего подойдет для ваших целей.</p>
<p><span style="color: #ff6600;"><strong>Часть II: Общие концепции архитектуры данных</strong></span></p>
<p><strong>Глава 4</strong> расскажет, что такое хранилище данных (data warehouse), зачем его использовать и что им не является. Мы рассмотрим подход сверху вниз (top-down approach), обсудим, действительно ли реляционное хранилище данных устарело, и изучим способы его наполнения.</p>
<p><strong>Глава 5</strong> описывает, что такое data lake, зачем он нужен, а также подход снизу вверх (bottom-up approach). В ней также будет разбор дизайна data lake и случаев, когда стоит использовать несколько data lakes.</p>
<p><strong>Глава 6</strong> охватывает ключевые концепции архитектуры данных, связанные с хранилищами, включая витрины данных (data marts), операционные хранилища (operational data stores), управление мастер-данными (master data management) и виртуализацию данных (data virtualization).</p>
<p><strong>Глава 7</strong> фокусируется на архитектурных концепциях, связанных с проектированием, включая OLTP и OLAP, операционные и аналитические данные, SMP и MPP, архитектуры Lambda и Kappa, а также полиглотную персистентность (polyglot persistence).</p>
<p><strong>Глава 8</strong> посвящена моделированию данных, включая реляционное и многомерное моделирование, дебаты между подходами Кимбалла и Инмона, общую модель данных (common data model) и хранилища данных (data vaults).</p>
<p><strong>Глава 9</strong> будет о загрузке данных (data ingestion), где мы обсудим ELT и ETL, обратный ELT (reverse ETL), пакетную обработку против обработки в реальном времени, а также управление данными (data governance).</p>
<p><span style="color: #ff6600;"><strong>Часть III: Конкретные архитектуры данных</strong></span></p>
<p><strong>Глава 10</strong> описывает современное хранилище данных (modern data warehouse) и пять этапов его построения.</p>
<p><strong>Глава 11</strong> охватывает архитектуру <strong>data fabric</strong> и её сценарии использования.</p>
<p><strong>Глава 12</strong> объясняет архитектуру <strong>data lakehouse</strong> и компромиссы при отказе от реляционного хранилища данных.</p>
<p><strong>Глава 13 и 14 посвящены архитектуре data mesh.</strong></p>
<p><strong>Глава 13</strong> сосредоточена на децентрализованном подходе data mesh, <strong>четырёх принципах data mesh</strong>, а также описывает, что такое домены данных и продукты данных.</p>
<p><strong>Глава 14</strong> рассматривает проблемы и вызовы при создании <strong>data mesh</strong>, а также опровергает распространённые мифы. Эта глава поможет вам оценить готовность к внедрению data mesh и представить, каким будет его будущее.</p>
<p><strong>Глава 15</strong> исследует причины успехов и провалов проектов, а также описывает организацию команды, необходимую для построения архитектуры данных.</p>
<p><strong>Глава 16</strong> завершает обсуждением open-source решений, преимуществ облака, крупных облачных провайдеров, использования мультиоблака и программных фреймворков.</p>
<p>Теперь я готов перевернуть ваш мир данных. Вы готовы?</p>
<p>Сообщение <a href="https://datatalks.ru/big-data-data-architectures/">Глава 1 &#171;Big Data&#187; &#8212; Data Architectures</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/big-data-data-architectures/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
