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

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

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

Под таблицами я подразумеваю структуры, организованные по строкам и столбцам в базах данных SQL, а под коллекциями я подразумеваю их аналоги в базах данных No-SQL.

Для лучшего понимания я разделил все принципы на четыре группы:

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

Принципы, связанные с процессом

Понять проблему

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

Из-за этого, может быть, есть решение, которое не требует загрузки нового SQL-сервера? Возможно, вашему решению нужна база данных No-SQL или служба хранения объектов?

В общем, неразумно ограничиваться одной идеей!

Определить контроль доступа

Вы должны спросить себя, какие части вашей системы будут иметь доступ к новой базе данных. Прямо и косвенно. Будете ли вы придерживаться старомодного правила «один бэкэнд на базу данных» или будете использовать другую стратегию? Должны ли вы разрешать людям (разработчикам или менеджерам по продуктам) прямой доступ к базе данных?

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

Проактивный подход заключается в обеспечении контроля доступа с помощью инструментов, предоставляемых ядром базы данных. В случае с базой данных SQL вы можете захотеть использовать пользователей базы данных с разными привилегиями — например, с разными привилегиями CRUD для определенных таблиц. Использование определенного пользователя для некоторых операций обеспечивает слой разделения между запросами и данными. Например, для выполнения запросов к базе данных для системного пользователя я мог бы использовать пользователя базы данных с привилегиями, предоставленными этому конечному пользователю.

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

Напишите схему на бумаге

Я думаю, что написание схемы сначала на бумаге позволяет ее разработчику раньше увидеть явные недостатки. С помощью карандаша легче исправлять ошибки, чем в существующей базе данных SQL. Или, что еще хуже, если бы мы использовали ORM для генерации схемы SQL для себя.

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

Просмотрите дизайн

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

Принципы, связанные с базой данных

Не использовать внешние ключи

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

Некоторые разработчики избавляются от этого препятствия, используя внешние ключи, допускающие значение NULL. Прочитав эту статью, вы поймете, почему я стараюсь вообще не использовать поля, допускающие значение NULL.

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

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

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

Не используйте каскады

Базы данных SQL позволяют пользователям определять каскадное поведение для внешних ключей, чтобы контролировать, что происходит, когда кто-то обновляет или удаляет строку в родительской таблице. Теперь я объяснил, почему я не буду использовать внешний ключ в распределенной системе, но отказ от каскадов остается другой темой.

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

Понимание часовых поясов

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

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

Использовать временные метки строк

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

Знай актера

Кто такой актер? Под актером я подразумеваю человека или службу, которые изменяют данные в системе. Пользователь может изменять данные с помощью предоставленного пользовательского интерфейса. Служба может изменять данные при периодическом вызове планировщиком.

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

Используйте нумерованные перечисления

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

Истина, вероятно, лежит между ними, поскольку я считаю, что использование целочисленных типов требует точного планирования, экономит место (включая пространство индекса) и позволяет проводить более быстрые сравнения. Кроме того, в случае злонамеренного получения данных базы данных он скрывает части бизнес-логики от злоумышленника. При этом я считаю разумным использовать символьные поля на этапе Proof-of-Concept продукта.

Ограничьте длину имени столбца

Я считаю, что имена столбцов должны быть как можно короче. Мне легче писать и читать лаконичные SQL-запросы. Если вы постоянно отправляете много запросов (возможно, вы также захотите использовать для этого внутренние хранимые процедуры), вы можете получить дополнительный трафик, сохраняя свои имена длинными.

В базах данных NoSQL, которые помещают имя столбца в каждый записываемый объект, вы можете потратить много места на диске, если будете придерживаться длинных строк!

Ограничьте длину значения столбца

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

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

Для действительных чисел обычно доступны три типа:

  • тип одинарной точности с плавающей запятой, использующий основание 2,
  • тип двойной точности с плавающей запятой (с использованием той же базы, что и раньше),
  • фиксированная точность с использованием базы 10.

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

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

Узнайте о типах денег

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

Основная проблема заключается в том, что наши денежные единицы делятся на 10, а не на 2. Таким образом, вы не можете точно представить их, используя большинство реализаций арифметики с плавающей запятой, например, используя стандарт, известный как IEEE-754.

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

Принципы, связанные с запросами

Рассмотрим чтение и запись

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

Сколько операций записи в секунду он должен поддерживать? Сколько читает? Можете ли вы легко разделить его? Поддерживает ли база данных шардинг?

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

Обеспечьте запись без чтения

Если вы строите распределенную систему, вы можете захотеть, чтобы ваши записи не зависели от ваших чтений. Это означает, что вы можете обновлять строки таблицы в своей базе данных, не зная их предыдущих значений. Это один из способов отказа от традиционных транзакций в пользу атомарных обновлений.

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

Если вся ваша база данных работает по этому принципу, вы сможете перестроить базу данных, начав с нуля и воспроизведя все команды и сообщения, поступившие в систему, в их порядке.

Понимание транзакций и уровней изоляции

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

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

Планируйте свои запросы

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

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

Вы можете проверить, означают ли создание и обновление одно и то же для приложения, и считать их одной операцией, обычно называемой в отрасли «upsert». Вы можете извлечь выгоду, заменив все мысли о записи на просто «upserts», по крайней мере, для некоторых таблиц в вашей базе данных.

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

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

Планируйте свои индексы

Когда вы закончите планирование своих запросов, вы должны спланировать свои индексы.

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

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

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

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

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

Принципы, связанные с сущностью

Используйте единый источник правды

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

Сделайте данные независимыми

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

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

При добавлении столбцов в базы данных SQL вы всегда должны следить за тем, чтобы:

  • первая миграция добавляет столбец со значением по умолчанию (например, нулевое значение или 0),
  • вторая миграция заполняет столбец правильными значениями,
  • третья миграция делает столбец необнуляемым, если это необходимо.

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

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

Используйте один первичный ключ

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

Я против использования целочисленных последовательностей для первичных ключей.

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

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

Наконец, мы не можем установить значение первичного ключа в коде приложения.

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

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

Использование денормализации

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

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

Вставка, обновление и вставка

Для людей, которые только что услышали слово «upsert» (я дал ему определение раньше, но вы, возможно, забыли!) — это сочетание «обновить» и «вставить». Это означает, что обновление — это операция, которая обновляет определенную запись в базе данных, если она уже существует, или создает новую в противном случае.

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

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

Ставьте только необходимые нулевые значения

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

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

Воздержитесь от значений по умолчанию

Я редко полагаюсь на значения по умолчанию, будь то базы данных или код. Вы рассеиваете свою бизнес-логику и создаете неявное поведение, используя значения по умолчанию в определении столбца. Я написал «неявный», потому что если вы не укажете значение при записи в определенное поле, база данных предоставит его вам. Судя по моему опыту, отслеживать имплицитное поведение непросто, и я избегаю этого, пока могу.

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

Сохраняйте независимость сущностей

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

Сохраняйте восстанавливаемые объекты

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

Краткое содержание

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

Как всегда, вы можете оставить комментарий в разделе комментариев!