Внедрение гибких отношений в РСУБД - каковы на самом деле компромиссы?
У меня есть куча продуктов с кучей различных возможных атрибутов для каждого продукта. Например, продукт А имеет название, размер, цвет, форму. Продукт B имеет название, калории, сахар и т. Д. Один из способов решить эту проблему:
1) Создать таблицы
Products (id, name)
Attributes (id, name)
Product_Attributes (product_id, attribute_id, value as string)
Это обеспечивает максимальную гибкость, но я слышал, что многие люди рекомендуют против этого, хотя я не уверен, почему. Я имею в виду, что если бы эти таблицы назывались Teams, Players, Team_Players, мы бы все согласились, что это правильный реляционный дизайн.
Каждый, кто объясняет мне, почему это плохо, делает это в контексте полностью гибкого реляционного дизайна, в котором вы никогда не создаете реальные таблицы за несколькими базовыми начальными таблицами (например, object, attribute, object_attribute)- что, я думаю, мы все могут согласиться, это плохо. Но это гораздо более ограниченная и ограниченная версия (только продукты, а не каждый объект в системе), поэтому я не думаю, что было бы справедливо объединять эти две архитектуры вместе.
С какими проблемами вы столкнулись (опыт или теоретические), которые делают этот дизайн таким плохим?
2) Еще один способ решить эту проблему - создать таблицу Product с несколькими столбцами, такими как Size, Color, Shape, Weight, Sugar и т. Д., А затем добавить несколько дополнительных столбцов в конце, чтобы дать нам некоторую гибкость. Это создаст в целом разреженные строки, заполненные в основном значениями NULL. Людям нравится этот подход, но у меня вопрос: сколько столбцов может быть у вас до того, как этот подход потеряет свою производительность? Если у вас есть 200 столбцов, я думаю, что это уже не умный ход, а как насчет 100 столбцов? 50 колонок? 25 столбцов?
3) Последний подход, о котором я знаю, - хранить все атрибуты в виде большого двоичного объекта (возможно, JSON) в одном столбце таблицы "Продукты". Мне нравится такой подход, но он не чувствуется правильным. Запросы сложны. И если вы хотите иметь возможность легко изменить имя атрибута позже, вам нужно либо проанализировать каждую запись по отдельности, либо сделать так, чтобы они вводились в ваш BLOB-объект по некоторому идентификатору. Если вы идете по пути id, вам понадобятся другие атрибуты таблицы, и все будет выглядеть как подход № 1 сверху, за исключением того, что вы не сможете присоединить attribute_id со своим BLOB-объектом, поэтому я надеюсь, что вы не хотите ничего запрашивать по имени атрибута.
Что мне нравится в этом подходе, так это то, что вы можете запрашивать один продукт, а в своем коде вы можете легко получить доступ ко всем его свойствам - быстро. И если вы удалите продукт, вам не придется очищать другие таблицы - легко оставаться последовательным.
4) Я прочитал кое-что о возможности индексирования строго типизированных форматов xml в некоторых СУБД, но, честно говоря, я не очень много знаю об этом подходе.
Я застрял. Я чувствую, что подход № 1 - лучший выбор, но все, что я читаю, говорит о том, что воняет. Как правильно подумать об этой проблеме, чтобы решить, какой метод лучше всего подходит для данной ситуации? Мы приветствуем больше идей, чем я перечислил!
4 ответа
Вы, вероятно, сможете найти много интересного по этой теме, выполнив поиск в Google по "значению атрибута сущности antipattern".
Одна из проблем этого подхода заключается в том, что вы смешиваете метаданные с реальными данными. Теперь ваш "атрибут" должен сообщить базе данных, что именно содержится в столбце "значение". Это может очень затруднить обработку этих данных во внешнем интерфейсе, в программном обеспечении для отчетов и т. Д.
Во-вторых, вам будет очень трудно обеспечить соблюдение целостности данных в базе данных. Когда ваш продукт имеет атрибут "вес", что мешает кому-то поставить значение "22 дюйма"? Или не числовое значение полностью. Вы можете сказать: "Ну, мое заявление справится с этим". Затем вам нужно менять свое приложение каждый раз, когда вы хотите добавить новый атрибут, потому что приложение должно знать, как его обрабатывать. Если вы собираетесь пройти через всю эту работу, просто добавьте новый столбец.
В-третьих, как вы обеспечиваете, чтобы данный продукт имел все необходимые атрибуты? В строке вы можете сделать столбец NOT NULL, и тогда они потребуются для передачи этой строки в базу данных. Вы не можете применить это в модели EAV.
В-четвертых, такая модель обычно приводит к путанице. Люди не уверены, какие "атрибуты" поддерживаются, или они дублируют атрибут, или они забывают обрабатывать атрибут при создании отчета. Например, если у меня есть атрибут "Вес (кг)" и другой атрибут для "Вес (фунты)", и кто-то спрашивает меня: "Какой самый тяжелый продукт в вашей базе данных?" Я лучше помню, что мне нужно проверить оба атрибута.
В-пятых, эта модель обычно также приводит к лени. Эй, нет никакой причины проводить какой-либо анализ продуктов, с которыми может справиться наша система, потому что, что бы ни случилось, мы просто добавим некоторые атрибуты. По моему опыту, компаниям гораздо лучше проводить анализ, необходимый для создания хорошего дизайна базы данных, а не использовать такой антипаттерн. Вы узнаете кое-что о базе данных, приложении и, вероятно, о бизнесе.
В-шестых, может потребоваться МНОГО объединений, чтобы получить одну строку данных для данного продукта. Вы можете вернуть атрибуты в виде отдельных строк, но теперь вам нужно придумать настраиваемые списки для перечисления этих продуктов и т. Д. Аналогично, написание поисковых запросов для этой модели может быть очень трудным, и в обеих этих ситуациях вы, вероятно, есть проблемы с производительностью.
Это лишь некоторые из проблем, с которыми я сталкивался на протяжении многих лет. Я уверен, что есть и другие.
Какое правильное решение для вашей системы во многом зависит от специфики вашего бизнеса и приложения. Вместо разреженной строки вы можете рассмотреть возможность использования таблиц подтипов, если ваши продукты попадают в несколько категорий, которые имеют общие атрибуты.
Существует много проблем с гибкими моделями данных, но первая, которая может вас укусить, заключается в том, что запросы очень громоздки очень быстро. Например, если вы хотите получить атрибут Size для каждого продукта, запрос будет относительно простым.
SELECT p.name product_name,
pa.value product_size
FROM product p
left outer join product_attribute pa on (p.product_id = pa.product_id)
left outer join attribute a on (pa.attribute_id = a.attribute_id and
a.name = 'size')
Если вы хотите получить размер и некоторые другие атрибуты, такие как цвет, все становится сложнее
SELECT p.name product_name,
pa_size.value product_size
pa_color.value product_color
FROM product p
left outer join product_attribute pa_size on (p.product_id = pa_size.product_id)
left outer join product_attribute pa_color on (p.product_id = pa_size.product_id)
left outer join attribute a_size on (pa_size.attribute_id = a.attribute_id and
a_size.name = 'size')
left outer join attribute a_color on (pa_color.attribute_id = a.attribute_id and
a_color.name = 'color')
Очень быстро, когда вы начинаете хотеть получить 10 атрибутов или писать сложные поиски (покажите мне продукты, где цвет синий, а размер средний), запросы начинают становиться очень сложными как для разработчиков, чтобы писать и поддерживать, так и для оптимизатора базы данных. создать план запроса для. Если вы объединяете 30 таблиц вместе, оптимизатору придется сократить дерево планов, которые он считает очень, очень быстро, чтобы иметь возможность генерировать план запроса в разумные сроки. Это приводит к тому, что оптимизатор отбрасывает многообещающие пути слишком рано и генерирует пути, не являющиеся оптимальными, для многих ваших запросов.
Это, в свою очередь, означает, что вы очень быстро попадаете в точку, в которой новые разработки оказываются узкими местами, поскольку разработчики не могут правильно выполнить свои запросы или разработчики не могут заставить свои запросы возвращаться достаточно быстро. Независимо от того, сколько времени вы сэкономили заранее, не собрав требования, чтобы быстро определить, какие действительные атрибуты быстро используются, на 47-й итерации "Почему я не могу получить нужные данные из этой гнилостной модели данных?"
Помимо этой стоимости для разработчиков, вы в конечном итоге создаете много затрат для организации в целом.
- Ни один инструмент запросов не будет хорошо обрабатывать такую модель данных. Таким образом, все пользователи, которые в настоящее время могут запустить свой любимый инструмент запросов и запустить некоторые отчеты из вашей базы данных, теперь застряли в ожидании, когда разработчики напишут свои отчеты и сделают для них выдержки.
- Качество данных становится очень трудно обеспечить. Становится очень трудно проверять условия, которые включают в себя несколько атрибутов (например, если размер продукта средний, то вес должен быть от 1 до 10 фунтов, если указан рост продукта, то также требуется ширина), чтобы люди не делали их чеки. Они не пишут отчеты, чтобы определить, где такие правила нарушены. Таким образом, данные заканчивают тем, что представляют собой небольшой набор данных, которые последующие процессы решают, что они не могут использовать, потому что они недостаточно полны.
- Вы переносите слишком много начальных обсуждений требований в будущее, когда понимание основных сущностей, вероятно, приведет к гораздо лучшему дизайну в целом. Если вы не можете согласиться с набором атрибутов, которые должна поддерживать первая версия продукта, вы не понимаете, что должна делать эта версия. Даже если вы успешно закодировали очень универсальное приложение, это означает, что после его создания потребуется много времени для настройки (потому что кто-то должен будет выяснить, какие атрибуты он поддерживает в этот момент). И тогда вы обнаружите, что при настройке приложения вы пропустили множество требований, которые стали понятны только после определения атрибутов - вы не можете знать, что ширина требуется, если указана высота, если вы не знаете, они собираются хранить высоту или ширину в первую очередь.
В худшем случае ответом на эту проблему во время настройки будет немедленное определение необходимости предоставления гибкого способа задания бизнес-правил и рабочих процессов, чтобы люди, настраивающие приложение, могли быстро кодировать свои бизнес-правила при добавлении новых атрибутов. и чтобы они могли контролировать поток приложения, группируя атрибуты или пропуская определенные страницы (то есть, есть страница, на которой требуются марка и модель, если тип продукта - автомобиль, пропустите эту страницу, если сейчас). Но для того, чтобы сделать это, вы в конечном итоге создадите целую среду разработки. И вы собираетесь продвигать работу по написанию кода приложения для тех, кто настраивает продукт. Если вам не удастся действительно хорошо создать среду разработки, и если люди, конфигурирующие продукт, не являются действительно разработчиками, это не закончится хорошо.
Я имею в виду, что если бы эти таблицы назывались Teams, Players, Team_Players, мы бы все согласились, что это правильный реляционный дизайн.
Нет, не будем. Вот почему
Вы начали с этого.
Products (id, name)
Attributes (id, name)
Product_Attributes (product_id, attribute_id, value as string)
Давайте отбросим идентификационные номера, чтобы мы могли видеть, что на самом деле происходит. (Более длинные имена столбцов для ясности.)
Products (product_name)
Attributes (attribute_name)
Product_Attributes (product_name, attribute_name, value as string)
И переводить это для команд и игроков.,,
Teams (team_name)
Players (player_name)
Team_Players (team_name, player_name, value as string)
Так что для образца данных мы могли бы иметь
Team Player Value
--
St. Louis Cardinals Boggs, Mitchell ?
St. Louis Cardinals Carpenter, Chris ?
St. Louis Cardinals Franklin, Ryan ?
St. Louis Cardinals Garcia, Jaime ?
Что на земле принадлежит вместо знаков вопроса? Допустим, мы хотим записать количество сыгранных игр. Теперь пример данных выглядит следующим образом.
Team Player Value
--
St. Louis Cardinals Boggs, Mitchell 23
St. Louis Cardinals Carpenter, Chris 15
St. Louis Cardinals Franklin, Ryan 19
St. Louis Cardinals Garcia, Jaime 14
Хотите хранить средний уровень тоже? Ты не можешь Мало того, что вы не можете хранить среднее значение ватина вместе с играми, вы не можете определить, просмотрев базу данных, сыграл ли Митч Боггс в 23 играх, имел 23 удара, забил 23 пробежки, имел 23 "на летучих мышах", имел 23 сингла, или вычеркнул 23 раза.
Причина, по которой этот подход настолько плох, заключается в том, что вы не знаете, как часто вам приходится присоединяться к таблице, чтобы получить все атрибуты. Плюс 20-кратное объединение с одной и той же таблицей приводит к созданию массивного блока производительности. Я предполагаю, что Продукты будут в основе вашей системы и, таким образом, будут критически важным местом для производительности.
Теперь вы говорите, что атрибуты продукта будут сильно отличаться. Я не согласен. Там будет много атрибутов, которые являются общими для большого количества ваших товаров, таких как цена, единицы, размер, цвет, размеры, вес. Они должны быть в таблице продуктов как общие свойства. Это также те, которые пользователь, скорее всего, будет искать при выборе товара.
Другие свойства полезны в качестве описания, но не для большинства других (они не будут найдены или введены в детали заказа). Поместите их в поле описания или заметок.
Наконец, у вас осталось несколько атрибутов, которые могут отличаться. Но насколько они отличаются? Являются ли они общими для продукта типа partiuclar (у книг есть эти атрибуты, у камер есть они), тогда связанная таблица для этого типа продукта могла бы работать хорошо.
Как только вы выполнили свою работу и выяснили все это, добавьте гибкость таблицы EAV, если она вам все еще нужна. Вышеприведенные шаги должны охватывать 98+% реальных требований.
(Также довольно сложно разработать таблицу деталей заказа, если вы не знаете полей атрибутов, которые нужно записать для заказа - вы не можете полагаться на таблицу продуктов для этого)
(О, и я полностью согласен с тем, что говорит @Tom H.)