Тема 01

Тема 1. Аналитическое хранилище и база приложения

Тема 1. Аналитическое хранилище и база приложения

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

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

Файл как способ хранения

Начнём с простейшего. Учебный набор курса содержит файл почасовых показаний приборов учёта: 345 846 строк, около 18 МБ в формате CSV. Такой файл читается одной командой, открывается в текстовом редакторе, передаётся по электронной почте, не требует ни установки программ, ни согласования с кем-либо. Для набора подобного размера файлового подхода достаточно, и тем полезнее установить, где проходят его границы.

Содержимое такого файла — строки текста, разделённые запятыми: первая строка перечисляет имена столбцов, каждая последующая описывает одно измерение — какой прибор, в какой час, какое значение передал.

Фрагмент файла показаний и те же строки в виде таблицы
Фрагмент файла показаний: те же данные как текст и как таблица

Четыре ограничения

Первое ограничение — отсутствие схемы. Формат CSV не хранит типов: столбец ts представляет собой текст, и станет ли он отметкой времени, зависит от того, укажет ли читающая программа соответствующий параметр. Столбец value может содержать пустую строку, число с запятой вместо точки или слово NULL — файл примет любое из этих значений. Правил, ограничивающих содержимое, тоже нет: ничто не препятствует записи показания несуществующего прибора или даты из будущего. Проверки приходится повторять в каждой программе, читающей файл, и достаточно одной программы, где о них забыли, чтобы в данные попали некорректные значения.

Второе ограничение — стоимость чтения. Для вычисления среднего по одному столбцу программа обязана прочитать файл целиком: разобрать все 345 846 строк, разделить каждую по запятым, отбросить ненужные поля. Восемнадцать мегабайт читаются за доли секунды, и различие в скорости незаметно. Однако зависимость линейна: на файле в двадцать гигабайт тот же запрос потребует минут и потребует их снова при следующем обращении к тем же данным.

Третье ограничение определяется устройством инструментов обработки. Библиотеки, работающие с табличными данными в памяти, размещают таблицу целиком в оперативной памяти, причём с запасом на промежуточные копии при преобразованиях; практическая оценка потребности — от трёх до десяти объёмов исходного файла. При шестнадцати гигабайтах оперативной памяти это даёт предел порядка двух-трёх гигабайт исходных данных, после которого процесс либо вытесняет память на диск, либо аварийно завершается. Границу можно отодвинуть чтением по частям, но тогда каждое агрегирование приходится программировать как накопление промежуточных итогов.

Четвёртое ограничение — согласованность при коллективной работе. Файл не допускает одновременного изменения из нескольких мест: копия у аналитика, копия у инженера и копия в почтовой рассылке расходятся в течение недели, и вопрос о том, какая версия верна, не имеет технического ответа. Сюда же примыкает утрата истории: файл обычно перезаписывается, поэтому предыдущее состояние данных не восстанавливается, а отчёт, подготовленный месяцем ранее, невоспроизводим.

Почему организационные меры не помогают

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

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

База данных

Таблица, схема и ключи

Основная форма организации данных — таблица, фрагмент которой приведён в начале темы. Строка таблицы описывает один объект или одно событие, столбец — одно свойство, одинаковое по смыслу для всех строк. Тип столбца фиксирован: целое число, дробное число, строка, отметка времени. В учебном наборе таблица приборов учёта содержит по строке на прибор со столбцами «модель», «ресурс», «дата установки»; таблица показаний — по строке на одно измерение со столбцами «прибор», «время», «значение».

Перечень таблиц, их столбцов, типов и ограничений называется схемой. Схема объявляется до загрузки данных и действует как контракт: значение, не соответствующее объявленному типу, в таблицу не попадает. Именно этого не хватало файлу.

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

СУБД, запрос и транзакция

База данных — организованная по общей схеме совокупность данных. Система управления базами данных (англ. database management system, DBMS) — программа, которая этими данными управляет: размещает их на диске, выполняет запросы, следит за соблюдением схемы, обслуживает одновременные обращения нескольких пользователей. В обиходной речи базой данных называют и сами данные, и управляющую ими программу; далее эти понятия различаются, поскольку одну и ту же схему с данными можно перенести из одной СУБД в другую.

Запрос — сформулированное на специальном языке требование выдать или изменить данные. Для табличных баз таким языком служит SQL (Structured Query Language, SQL).

По способу развёртывания СУБД делятся на клиент-серверные и встраиваемые. Клиент-серверная работает как отдельная постоянно запущенная программа, к которой подключаются по сети; так устроены PostgreSQL и MongoDB. Встраиваемая подключается к программе пользователя как библиотека и не требует ни отдельного процесса, ни администрирования; так устроены SQLite и DuckDB. Различие касается эксплуатации, а не языка запросов и не модели данных.

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

Что база данных добавляет к файлу

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

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

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

Снимается ограничение по памяти: СУБД читает данные блоками, использует диск при сортировке, выполняет соединения порциями. Объём обрабатываемых данных ограничен дисковым пространством, а рост времени выполнения при увеличении объёма остаётся плавным. Наконец, одновременная работа нескольких пользователей обеспечивается транзакциями, а не договорённостями между людьми. Сохранения предыдущих состояний данных база приложения при этом не гарантирует: обновление перезаписывает строку. Это требование аналитики, и к нему мы вернёмся при рассмотрении хранилища данных.

Что теряется в файловом подходе и что добавляет база данных
Файл и база данных: что приходится программировать самостоятельно, а что берёт на себя система

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

Две роли базы данных

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

Операционная нагрузка

Операционная обработка транзакций (англ. online transaction processing, OLTP) обслуживает работу прикладной системы. Обратимся к порталу приёма обращений: житель отправляет заявку, диспетчер открывает её карточку, исполнитель меняет статус на «в работе», затем на «закрыта». Каждое действие затрагивает одну-две строки и должно выполняться за десятки миллисекунд, поскольку пользователь ожидает отклика интерфейса.

Для этого профиля характерны множество коротких транзакций, точечный доступ по первичному ключу, значительная доля операций записи и десятки одновременно работающих пользователей. Требования к целостности жёсткие: заявка не может потеряться или продублироваться, статус не может измениться самопроизвольно. Под такой профиль оптимизированы классические реляционные СУБД: нормализованная схема 1 исключает противоречия при обновлении, индексы обеспечивают доступ по ключу, транзакции гарантируют атомарность. Ключевые метрики здесь — задержка отклика на одну операцию и число транзакций в секунду.

Аналитическая нагрузка

Аналитическая обработка (англ. online analytical processing, OLAP) отвечает на вопросы о совокупности данных 2. Сколько тепла израсходовано по районам за отопительный сезон. У каких объектов потребление растёт быстрее среднего. Как связаны аномалии в показаниях и последующие аварии. Ни один из этих вопросов не решается чтением одной строки: каждый требует просмотра сотен тысяч записей и возвращает таблицу в несколько строк.

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

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

Сопоставление профилей

Различия удобно свести воедино.

Сопоставление операционного и аналитического профилей нагрузки
Два профиля нагрузки: что читает запрос и чем измеряется результат

Единица работы: транзакция, затрагивающая одну строку, — против запроса, сканирующего таблицу. Форма схемы: нормализованная, минимально избыточная — против удобной для чтения, сознательно избыточной; к этому мы вернёмся в теме 4. Свежесть данных: секунды — против часов или суток, что для аналитики обычно приемлемо. Метрика: задержка отклика — против пропускной способности, то есть объёма данных, обработанного за единицу времени.

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

Вернёмся к вопросу, с которого начался раздел. Выполнять аналитические запросы в базе работающего приложения нежелательно по двум причинам. С технической стороны продолжительный запрос-сканирование конкурирует за память, диск и процессор с короткими транзакциями, и время отклика прикладной системы возрастает. Со стороны структуры данных операционная схема, нормализованная ради корректности записи, неудобна для чтения: расчёт потребления по районам требует соединения четырёх-пяти таблиц, что усложняет формулировку запроса и увеличивает время его выполнения. Противоречие не устраняется настройками: два профиля предъявляют к устройству данных несовместимые требования 3. Существуют, впрочем, и промежуточные решения — гибридные системы (англ. hybrid transactional/analytical processing, HTAP), рассчитанные на обе нагрузки, и выполнение аналитики на реплике операционной базы, разводящее конкуренцию за ресурсы, но не различие в структуре.

Практический вывод состоит в разделении: аналитическую нагрузку выносят на отдельную базу данных, наполняемую копиями данных из операционных систем. Такую базу называют хранилищем данных.

Хранилище данных

Термин и его границы

Слово «хранилище» употребляется в двух значениях, и различать их следует сразу. В широком значении хранилище — любое организованное место, откуда данные читают: каталог файлов, объектное хранилище, база данных. В узком, терминологическом значении хранилище данных (англ. data warehouse) — база данных особого назначения, предназначенная для анализа накопленных данных, а не для обслуживания работы приложения.

Существенно, что это не альтернатива базе данных и не отдельный класс программ: хранилище данных — та же база данных, отличающаяся тем, какие данные в ней собраны, как они туда попадают и какие вопросы к ним задают. Одна и та же СУБД способна обслуживать и приложение, и аналитику; различие лежит в организации данных и характере нагрузки, а не в названии продукта. Далее «база данных» употребляется как общий термин, «хранилище данных» — как обозначение аналитической базы, «озеро данных» — как обозначение файлового хранения в объектной системе.

Четыре свойства

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

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

Интегрированность — наиболее трудоёмкое из четырёх свойств. Данные поступают из разных систем, и каждая ведёт учёт по-своему: в одной объект обозначен инвентарным номером, в другой — адресом, в третьей — условным именем «ЦТП-007». Тепло в одном источнике измеряется в гигакалориях, в другом — в киловатт-часах; отметки времени приходят то в местном времени, то в UTC, то без указания часового пояса. Интеграция означает приведение перечисленного к единым справочникам, единицам и правилам, причём однократно при загрузке, а не в каждом отчёте. В учебном наборе такая работа уже выполнена: показания, инциденты и обращения связаны общими ключами объектов и районов, а погода — датой.

Неизменяемость означает, что загруженные данные не редактируются пользователями; они изменяются только очередной загрузкой, и предыдущее состояние при этом сохраняется. Отсюда следует четвёртое свойство — привязка ко времени: строка отвечает не на вопрос «как обстоит дело сейчас», а на вопрос «как обстояло дело в такой-то момент». Это делает возможным отчёт по состоянию на прошлый квартал и, как будет показано в теме 9, корректную подготовку данных для обучения моделей.

Слои хранения

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

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

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

Витрины (англ. data mart) собираются под конкретного потребителя: отчёт, информационную панель, обучающую выборку. Витрина заранее агрегирована и денормализована, запрос к ней прост и выполняется быстро; платой служат избыточность и необходимость пересчёта при изменении детального слоя.

Слои — логическое разделение, а не обязательно разные системы. В небольшом контуре все три слоя размещаются в одной базе данных как три группы таблиц, различающиеся правилами наполнения; в крупном сырой слой может лежать в объектном хранилище, детальный — в аналитической СУБД, а витрины — в отдельной базе, обслуживающей отчётность. Существенно не число систем, а то, что данные каждого слоя получены известным преобразованием данных предыдущего.

Слои хранения: источники, сырой слой, детальный слой, витрины, потребители
Слои хранения и их назначение

Учебный набор курса устроен таким же образом. Файл показаний с пропусками, повторными отправками и выбросами относится к сырому слою. Таблица показаний с разобранными типами, снятыми дублями и отброшенными ошибочными значениями — к детальному; правила её построения разбираются в теме 8. Витрина суточных показателей собрана из сырого слоя по явно заданным правилам, поэтому её значения воспроизводимы: витрину всегда можно пересобрать заново, а значит, ошибку в правиле преобразования можно исправить без потери данных.

Классы хранилищ

До сих пор речь шла о таблицах — данных, у которых заранее известен набор столбцов и их типы. Значительная часть данных устроена иначе: ответ веб-сервиса содержит вложенные структуры и переменный набор полей, поток измерений с датчика важен не отдельными записями, а поведением во времени, текстовое описание неудобно искать по точному совпадению слов. Под каждый такой случай сложился свой класс хранилищ.

Классы хранилищ: реляционные, колоночные, документные, временных рядов, ключ-значение, векторные
Классы хранилищ и характер вопросов, под которые они приспособлены

Реляционные аналитические хранилища работают с таблицами и SQL, но, в отличие от операционных СУБД, хранят данные по столбцам, а не по строкам; этому посвящена тема 3. Документные хранилища принимают записи с нефиксированным набором полей и вложенной структурой (тема 5). Хранилища временных рядов рассчитаны на поток измерений с метками времени (тема 6). Хранилища «ключ — значение» возвращают значение по ключу за микросекунды и служат слоем быстрого доступа рядом с основным. Векторные хранилища ищут не совпадение, а близость — по числовому представлению смысла (тема 7). Озеро данных (англ. data lake) хранит файлы произвольных форматов в объектном хранилище, а надстройка Lakehouse добавляет к ним табличную семантику.

Использование под разные задачи разных хранилищ получило название полиглотного хранения (англ. polyglot persistence) 5. Критерий выбора обычно формулируют через структуру данных и характер запросов; не менее весома третья составляющая — стоимость сопровождения. Каждая дополнительная система в контуре требует резервного копирования, наблюдения за состоянием, обновлений и специалистов, способных её обслуживать. Поэтому обоснованным решением нередко оказывается сохранение имеющейся СУБД до тех пор, пока её возможностей достаточно для поставленных задач.

Ориентиры курса

Изложение опирается на один инструмент — DuckDB 6. Это встраиваемый аналитический движок: он не требует сервера, устанавливается одной командой, хранит базу в единственном файле и читает форматы CSV и Parquet напрямую, без предварительной загрузки. По внутреннему устройству это колоночная система, спроектированная под аналитический профиль нагрузки; по способу применения — библиотека, подключаемая в обычной программе на Python. Там, где встраиваемого движка принципиально недостаточно, используется полноценный сервер: документная модель в теме 5 рассматривается на MongoDB.

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

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

Дальнейшие темы следуют перечисленным классам хранилищ. Тема 2 разбирает аналитический SQL — язык, на котором задаются вопросы к табличным хранилищам, и с него мы начнём, поскольку сформулировать вопрос точно требуется прежде, чем выбирать, где хранить ответ.

Итоги темы

Ограничения файла — отсутствие схемы, полное чтение, предел оперативной памяти, расхождение копий — снимаются программой, а не порядком работы. СУБД переносит контроль схемы в одну точку и делает доступ к данным декларативным.

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

Способы хранения в курсе сравниваются по стоимости вопроса: времени выполнения, объёму прочитанных данных и трудоёмкости формулировки.

Литература

  1. Codd E. F. A Relational Model of Data for Large Shared Data Banks. — Communications of the ACM, 1970, С. 377–387, DOI: 10.1145/362384.362685.
  2. Codd E. F., Codd S. B., Salley C. T. Providing OLAP to User-Analysts: An IT Mandate. — Arbor Software Corporation, 1993.
  3. Chaudhuri S., Dayal U. An Overview of Data Warehousing and OLAP Technology. — ACM SIGMOD Record, 1997, С. 65–74, DOI: 10.1145/248603.248616.
  4. Inmon W. H. Building the Data Warehouse. — Wiley, 2005.
  5. Sadalage P. J., Fowler M. NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence. — Addison-Wesley, 2013.
  6. Raasveldt M., Mühleisen H. DuckDB: An Embeddable Analytical Database. — Proceedings of the 2019 International Conference on Management of Data (SIGMOD '19), 2019, С. 1981–1984, DOI: 10.1145/3299869.3320212.
Окружение лабораторной работыzip

Лабораторная работа 1. Один вопрос, три способа хранения

  • Объём: 4 академических часа
  • Раздел курса: тема 1 «Аналитическое хранилище и база приложения»

Введение

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

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

Цель работы

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

После выполнения работы студент сможет:

  • выполнить агрегирующий запрос к CSV-файлу средствами pandas и получить корректный численный ответ;
  • выполнить тот же запрос на SQL во встраиваемом движке DuckDB, обращаясь к файлу и к таблице;
  • получить тот же ответ из готовой витрины и сверить его с ответом, посчитанным из сырых данных;
  • измерить время выполнения каждого способа и объём прочитанных данных;
  • обосновать, какой способ уместен при каком объёме данных и числе обращений.

Теоретический минимум

Профили нагрузки, слои хранения и понятие стоимости вопроса — см. учебное пособие, тема 1, разделы «Две роли базы данных», «Слои хранения» и «Ориентиры курса».

Запрос из ноутбука. DuckDB подключается к программе как библиотека и не требует сервера. Запрос выполняется функцией duckdb.sql, метод .df() возвращает результат в виде DataFrame:

import duckdb
duckdb.sql("SELECT 42 AS answer").df()

Чтение файлов без загрузки. Путь к файлу подставляется вместо имени таблицы, в одинарных кавычках:

duckdb.sql("SELECT count(*) FROM 'data/raw/readings.csv'")
duckdb.sql("SELECT * FROM 'data/mart/consumption_daily.parquet' LIMIT 5")

Типы столбцов DuckDB определяет сам; посмотреть их можно запросом DESCRIBE SELECT * FROM '…'.

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

con = duckdb.connect("lr01.duckdb")
con.sql("CREATE OR REPLACE TABLE readings AS SELECT * FROM 'data/raw/readings.csv'")
con.sql("SELECT count(*) FROM readings").df()

Запрос с соединением и группировкой. Конструкции SQL разбираются в теме 2; для работы достаточно образца, который считает число показаний по видам ресурсов:

SELECT m.resource, count(*) AS readings
FROM 'data/raw/readings.csv' r
JOIN 'data/raw/meters.csv' m ON m.meter_id = r.meter_id
WHERE r.status <> 'error'
GROUP BY m.resource
ORDER BY readings DESC

FROM называет таблицу и даёт ей имя r, JOIN … ON присоединяет сведения о приборе по совпадению идентификатора, WHERE отбирает строки до подсчёта, GROUP BY задаёт группировку, count(*) считает строки в группе, ORDER BY упорядочивает результат; несколько справочников присоединяются несколькими JOIN подряд.

Операции заданий в SQL и pandas. Остальные операции, которые потребуются в частях 1–4:

ОперацияSQL в DuckDBpandas
отбор периодаWHERE ts >= '2025-01-01' AND ts < '2025-02-01'df.loc[(df["ts"] >= "2025-01-01") & (df["ts"] < "2025-02-01")]
соединение со справочникомJOIN … ON m.meter_id = r.meter_iddf.merge(meters, on="meter_id")
число строк по группамGROUP BY district_id и count(*)df.groupby("district_id").size()
среднее по группамavg(value)df.groupby("month")["value"].mean()
месяц из отметки времениdate_trunc('month', ts)df["ts"].dt.to_period("M")
минимум и максимумmin(ts), max(ts)df["ts"].min(), df["ts"].max()
число различных значенийcount(DISTINCT meter_id)df["meter_id"].nunique()
доля строк по условиюavg(CASE WHEN status <> 'ok' THEN 1 ELSE 0 END)(df["status"] != "ok").mean()

Многошаговый запрос. Конструкция WITH имя AS (запрос) даёт промежуточному результату имя, к которому следующий запрос обращается как к таблице.

Снятие повторных отправок. Правило «оставить последнюю запись по received_at» выражается конструкцией DISTINCT ON, которая возвращает по одной строке на каждое сочетание перечисленных столбцов — первую в заданном порядке сортировки:

WITH dedup AS (
  SELECT DISTINCT ON (meter_id, ts) *
  FROM 'data/raw/readings.csv'
  WHERE status <> 'error'
  ORDER BY meter_id, ts, received_at DESC
)
SELECT count(*) FROM dedup

Средствами pandas то же правило записывается сортировкой по ['meter_id', 'ts', 'received_at'] и последующим drop_duplicates(subset=['meter_id', 'ts'], keep='last').

Витрина. Столбец date витрины хранится как текст вида 2025-01-31; месяц выделяется выражением substr(date, 1, 7).

Замер времени. Функция timeit_once(fn) из заготовки выполняет функцию один раз и возвращает пару «результат, время в секундах»; для устойчивого замера служит магическая команда %%timeit. В отчёт вносится время первого (холодного) и повторного запуска.

Объём прочитанных данных. Для CSV это размер файла: он читается целиком, сколько бы столбцов ни требовалось. Для Parquet объём считается по нужным столбцам функцией parquet_columns_size из заготовки (почему — в теме 3). Для таблицы файловой базы объём не измеряется, в сводной таблице ставится прочерк.

Данные. Архив окружения содержит каталог data/ с файлами для заданий: raw/readings.csv, raw/meters.csv, raw/objects.csv, raw/districts.csv и витрину mart/consumption_daily.parquet; полный состав набора — в data/README.md.

Перечень оснащения

  • Python 3.12 или новее, JupyterLab.
  • Пакеты pandas, duckdb, pyarrow; устанавливаются первой ячейкой заготовки. Работа проверена на Python 3.12, duckdb 1.5.5, pandas 3.0.5, pyarrow 25.0.1.
  • Архив окружения работы — lr-env: заготовка-ноутбук lr01_template.ipynb с готовыми ячейками загрузки и вспомогательными функциями и каталог data/ с учебным набором. Ноутбук открывается из распакованного каталога lr-env/.
  • Свободное дисковое пространство — 300 МБ (данные, файловая база, промежуточные файлы).
  • Интернет требуется только на этапе установки пакетов.

Порядок выполнения работы

Работа выполняется в заготовке-ноутбуке: ячейки с пометкой # ЗАДАНИЕ дописываются, остальные не изменяются.

Часть 1. Среда и знакомство с данными

Задание

Установить пакеты первой ячейкой заготовки, выполнить ячейки загрузки и убедиться, что каталог data/ найден. Средствами DuckDB определить число строк в readings.csv, период наблюдений (минимальную и максимальную отметку времени), число различных приборов и типы столбцов. Отдельно подсчитать число строк со статусом, отличным от ok, и их долю от общего числа.

Результат

Заполненная таблица характеристик набора и вывод DESCRIBE в отчёте.

Часть 2. Вопрос к файлу средствами pandas

Аналитический вопрос работы состоит из двух частей:

  1. сколько показаний получено по каждому району за январь 2025 года;
  2. как менялась средняя температура теплоносителя по месяцам за весь период наблюдений.

Обе величины считаются по одному правилу отбора строк, и это правило — часть определения ответа. Строки со статусом error в расчёт не берутся: это заведомо неверные значения, помеченные системой сбора. Повторно присланные записи учитываются один раз: если у пары «прибор — отметка времени» несколько строк, берётся последняя по received_at. Откуда в данных берутся повторы, разбирается в теме 8; здесь достаточно применить правило.

Задание

Не используя SQL, ответить на оба вопроса средствами pandas: прочитать readings.csv, снять повторы, отбросить ошибочные строки, соединить показания со справочниками приборов, объектов и районов. Для первого вопроса отобрать январь и посчитать число показаний по районам; для второго — оставить показания с ресурсом «температура» и усреднить значение по месяцам. Замерить время выполнения всей цепочки и зафиксировать число строк кода.

Результат

Таблица «район — число показаний за январь», таблица «месяц — средняя температура», время выполнения, число строк кода.

Часть 3. Тот же вопрос на SQL

Задание

Ответить на те же вопросы запросом DuckDB, опираясь на образец и таблицу операций из теоретического минимума, — сначала обращаясь к CSV-файлам напрямую, затем загрузив показания и справочники в таблицы файловой базы и повторив запрос к таблицам. Численный результат обязан совпасть с результатом части 2; при расхождении найти и описать его причину. Замерить время в обоих вариантах (первый и повторный запуск) и зафиксировать число строк кода.

Результат

Текст запроса, совпадающая с частью 2 таблица ответа, четыре замера времени (файл — холодный и повторный, таблица — холодный и повторный).

Часть 4. Тот же вопрос к витрине

Задание

Получить оба ответа из готовой витрины consumption_daily.parquet. Витрина хранит суточные итоги: число показаний и сумму значений по каждой паре «объект — ресурс», поэтому среднюю температуру следует считать как отношение суммы значений к числу показаний, а не как среднее от суточных средних. Сверить результат с частями 2 и 3: он обязан совпасть до второго знака, потому что витрина собрана из тех же строк по тому же правилу отбора. Функцией parquet_columns_size определить объём столбцов, фактически потребовавшихся для ответа, и сравнить его с размером readings.csv.

Результат

Ответ из витрины, подтверждение совпадения с предыдущими частями, сравнение объёмов прочитанных данных.

Часть 5. Сопоставление и выводы

Задание

Свести замеры в одну таблицу: три способа хранения в четырёх вариантах замера (pandas по CSV, SQL по файлам, SQL по таблицам, витрина) — время холодного запуска, время повторного запуска, объём прочитанных данных, число строк кода. Письменно ответить на пять вопросов.

  1. Какой способ выигрывает по времени и меняется ли ответ при повторном запуске?
  2. Во сколько раз различается объём прочитанных данных у самого расточительного и самого экономного способа?
  3. Витрина отвечает быстрее всех — почему из этого не следует, что все вопросы нужно задавать витрине? Какой вопрос к учебному набору эта витрина не решает?
  4. На сколько и в какую сторону изменится число показаний по районам, если не снимать повторно присланные записи? Почему правило отбора строк приходится фиксировать до того, как назван результат?
  5. При каком объёме данных и какой частоте обращений уместен каждый из трёх способов? Ответ обосновывается собственными замерами, а не общими соображениями.

Результат

Сводная таблица замеров и письменные ответы на пять вопросов.

Форма отчёта

Отчёт сдаётся одним ноутбуком lr01_<фамилия>.ipynb с выполненными ячейками и сохранённым выводом. В ноутбуке должны присутствовать:

  • заполненная таблица характеристик набора (часть 1);
  • три реализации ответа на оба вопроса с видимым результатом каждой (части 2–4);
  • сводная таблица замеров в виде DataFrame со столбцами «способ», «холодный, с», «повторный, с», «прочитано, МБ», «строк кода»;
  • письменные ответы на пять вопросов части 5 в ячейках Markdown, а не в комментариях к коду.

Файл lr01.duckdb в отчёт не включается: он воспроизводится запуском ноутбука.

Контрольные вопросы

  1. Чем декларативная формулировка запроса отличается от программирования порядка обработки и что из этого следует при росте объёма данных?
  2. Почему аналитические запросы не принято выполнять на базе данных работающего приложения?
  3. Что означает утверждение «CSV не хранит типов» и как отсутствие типов проявляется при чтении показаний?
  4. Почему повторный запуск запроса к CSV выполняется быстрее первого и можно ли опираться на время повторного запуска при сравнении способов хранения?
  5. Витрина в этой работе отвечает быстрее сырых данных. За счёт чего получен выигрыш и чем за него заплатили?
  6. Почему среднюю температуру по витрине нельзя считать как среднее от суточных средних?
  7. Данные какого слоя используются в частях 2 и 3, а какого — в части 4? Что произойдёт с витриной, если в сыром слое обнаружится ошибка загрузки?
  8. В каком случае развёртывание хранилища не оправдано и достаточно файлов?