Динамические столбцы в таблицах базы данных и EAV

Я пытаюсь решить, куда идти, если у меня есть приложение, которое должно иметь возможность изменять схему БД на основе пользовательского ввода.

Например, если у меня есть объект «автомобиль», который содержит свойства автомобиля, такие как год, модель, количество дверей и т. д., как мне сохранить его в БД таким образом, чтобы пользователь мог добавлять новые свойства?

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

Могу ли я вместо этого генерировать таблицы динамически? Я вижу, что Sqlite поддерживает ADD COLUMN, но насколько это быстро, когда таблица достигает большого количества записей? И похоже, что нет никакого способа удалить столбец. Мне нужно создать новую таблицу без столбца, который я хочу удалить, и скопировать данные из старой таблицы. Это, безусловно, медленно на больших таблицах :(


person Alex    schedule 08.05.2015    source источник
comment
Является ли SQLite строгим требованием? Или вы бы даже оценили что-то еще?   -  person MatteoSp    schedule 11.05.2015
comment
На этот вопрос нет однозначного ответа, просто многое зависит от него. Знайте, что должен делать ваш код, оцените варианты (некоторые из них хороши, это то, что вы могли бы сделать, ответы ниже) и предвидите большую боль при написании кода, чем вам хотелось бы.   -  person Philip Kelley    schedule 12.05.2015


Ответы (6)


Я предполагаю, что SQLite (или другая реляционная СУБД) является обязательным требованием.

EAV

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

Допустим, вы разрабатываете модель данных с тремя таблицами: entities, attributes и _entities_attributes_:

CREATE TABLE entities
(entity_id INTEGER PRIMARY KEY, name TEXT);

CREATE TABLE attributes 
(attribute_id INTEGER PRIMARY KEY, name TEXT, type TEXT);

CREATE TABLE entity_attributes 
(entity_id INTEGER, attribute_id INTEGER, value TEXT, 
PRIMARY KEY(entity_id, attribute_id));

В этой модели таблица entities будет содержать ваши автомобили, а таблица attributes — атрибуты, которые вы можете связать с вашими автомобилями (марка, модель, цвет,... ) и его тип (текст, число, дата,...), а _entity_attributes_ будет содержать значения атрибутов для данного объекта (например, "красный").

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

INSERT довольно просты. Вам нужно только вставить новый объект, кучу атрибутов и их отношений. Например, чтобы вставить новую сущность с 3 атрибутами, вам потребуется выполнить 7 вставок (одну для сущности, еще три для атрибутов и еще три для отношений.

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

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

Но когда вы хотите выполнить SELECT, дело становится неприятным (вам нужно писать действительно сложные запросы), и производительность ужасно падает.

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

SELECT brand, model FROM cars;

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

Кроме того, подумайте об определении ваших атрибутов. Все ваши атрибуты хранятся как TEXT, и это может быть проблемой. Что, если кто-то ошибется и сохранит «красное» в качестве цены?

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

Как вы сказали, основная проблема разработчика заключается в том, что запросы действительно сложно писать, трудно тестировать и трудно поддерживать (сколько клиент должен заплатить, чтобы купить все красные Pontiac Firebirds 1980 года, которые у вас есть?) , и будет работать очень плохо при увеличении объема данных.

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

Кроме того, чтобы использовать авторитетный аргумент, я скажу, что Том Кайт решительно выступает против общих моделей данных: http://tkyte.blogspot.com.es/2009/01/this-should-be-fun-to-watch.html https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:10678084117056

Динамические столбцы в таблицах базы данных

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

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

Эта модель, как вы говорите, также имеет другую проблему при работе с SQLite, поскольку нет прямого способа удалить столбцы, и вам нужно будет сделать это, как указано на http://www.sqlite.org/faq.html#q11

BEGIN TRANSACTION;
CREATE TEMPORARY TABLE t1_backup(a,b);
INSERT INTO t1_backup SELECT a,b FROM t1;
DROP TABLE t1;
CREATE TABLE t1(a,b);
INSERT INTO t1 SELECT a,b FROM t1_backup;
DROP TABLE t1_backup;
COMMIT;

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

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

Другой вариант (псевдообщая модель)

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

Допустим, вы создаете такую ​​таблицу:

CREATE TABLE entities
(entity_id INTEGER PRIMARY KEY,
 name TEXT,
 type TEXT,
 attribute1 TEXT,
 attribute1 TEXT,
 ...
 attributeN TEXT
 );

В этой таблице вы можете хранить любые объекты (машины, дома, собаки), потому что у вас есть поле type и вы можете хранить столько атрибутов для каждый объект по своему усмотрению (N в данном случае).

Если вам нужно знать, что означает attribute37, когда type имеет значение «красный», вам нужно будет добавить еще одну таблицу, которая связывает типы и атрибуты с описанием атрибутов. .

А что, если вы обнаружите, что одной из ваших сущностей нужно больше атрибутов? Затем просто добавьте новые столбцы в таблицу entities (attributeN+1, ...).

В этом случае атрибуты всегда хранятся в виде ТЕКСТА (как в EAV) с его недостатками.

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

Надеюсь, поможет.


Дополнение из комментариев:

При использовании псевдоуниверсальной модели ваша таблица entities будет содержать множество столбцов. Из документации (https://www.sqlite.org/limits.html) по умолчанию значение для SQLITE_MAX_COLUMN равно 2000. Я работал с таблицами SQLite с более чем 100 столбцами с высокой производительностью, поэтому 40 столбцов не должны иметь большого значения для SQLite.

Как вы сказали, большинство ваших столбцов будут пустыми для большинства ваших записей, и вам нужно будет проиндексировать все ваши столбцы для повышения производительности, но вы можете использовать частичные индексы (https://www.sqlite.org/partialindex.html). Таким образом, ваши индексы будут небольшими даже при большом количестве строк, а селективность каждого индекса будет высокой.

Если вы реализуете EAV только с двумя таблицами, количество соединений между таблицами будет меньше, чем в моем примере, но запросы все равно будет сложно писать и поддерживать, и вам нужно будет сделать несколько (внешних) соединений для извлечения данных. , что снизит производительность даже при большом индексе, когда вы храните много данных. Например, представьте, что вы хотите получить марку, модель и цвет ваших автомобилей. Ваш SELECT будет выглядеть так:

SELECT e.name, a1.value brand, a2.value model, a3.value color
FROM entities e
LEFT JOIN entity_attributes a1 ON (e.entity_id = a1.entity_id and a1.attribute_id = 'brand')
LEFT JOIN entity_attributes a2 ON (e.entity_id = a2.entity_id and a2.attribute_id = 'model')
LEFT JOIN entity_attributes a3 ON (e.entity_id = a3.entity_id and a3.attribute_id = 'color');

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

SELECT name, attribute1 brand, attribute7 model, attribute35 color
FROM entities;

Кроме того, примите во внимание потенциальный размер вашей таблицы _entity_attributes_. Если у вас потенциально может быть 40 атрибутов для каждой сущности, скажем, у вас есть 20 ненулевых атрибутов для каждого из них. Если у вас есть 10 000 сущностей, ваша таблица _entity_attributes_ будет содержать 200 000 строк, и вы будете запрашивать ее, используя один огромный индекс. С псевдоуниверсальной моделью у вас будет 10 000 строк и один небольшой индекс для каждого столбца.

person antonio    schedule 12.05.2015
comment
Спасибо за подробное объяснение! Если я выберу псевдоуниверсальную модель, не станет ли БД огромной? Я имею в виду, что у меня будет около 40 столбцов, многие из них будут пустыми для большинства записей, и все они должны быть проиндексированы, чтобы пользователи могли фильтровать автомобили по атрибутам. С EAV мне нужно только вставить существующие атрибуты. Кстати, моя версия EAV представляет собой единую таблицу с именем, значением и внешним ключом, которая указывает на таблицу car. - person Alex; 13.05.2015
comment
@Алекс Добро пожаловать! Я отредактировал свой ответ, чтобы попытаться решить ваши проблемы. - person antonio; 13.05.2015

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

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

Например, если ваши пользователи могут настроить произвольные объекты данных (например, «автомобиль» в вашем примере), а затем хотят найти автомобили с объемом двигателя более 2000 куб. часть таблицы "старушки", я не знаю элегантного способа сделать это в SQL.

Однако вы можете добиться чего-то подобного, используя XML, XPath и т. д.

Если в вашем приложении есть набор объектов данных с известными атрибутами, но пользователи могут расширять эти атрибуты (обычное требование для таких продуктов, как средства отслеживания ошибок), «добавить столбец» — хорошее решение. Однако вам может потребоваться изобрести собственный язык запросов, чтобы пользователи могли запрашивать эти столбцы. Например, решение для отслеживания ошибок Atlassian Jira использует JQL, язык, похожий на SQL, для запросов ошибок.

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

person Neville Kuyt    schedule 10.05.2015
comment
Скажем, у меня есть ORM, который может относительно легко генерировать запросы, и, таким образом, единственная проблема заключается в производительности. Насколько велика проблема, если у меня есть миллионы строк в «денормализованной» EAV-таблице? - person Tarlen; 28.10.2016
comment
Это зависит от сложности вашего ORM (я такого зверя, кстати, не знаю - если есть ссылка, уверен, будет интересно). Производительность этих запросов будет сильно зависеть от того, насколько эффективно эти запросы используют индексы. Это почти наверняка означает разные типы данных для столбца значений (строка 10 не совпадает с целым числом 10), и любой запрос, который обходит индекс, вероятно, будет ужасно медленным (например, введите 'little_old_lady') . - person Neville Kuyt; 28.10.2016

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

person jjcosare    schedule 11.05.2015

Другой вариант, о котором я не упоминал выше, — это использование денормализованных таблиц для расширенных атрибутов. Это комбинация псевдоуниверсальной модели и динамических столбцов в таблицах базы данных. Вместо добавления столбцов в существующие таблицы вы добавляете столбцы или группы столбцов в новые таблицы с индексами FK к исходной таблице. Конечно, вам понадобится хорошее соглашение об именах (car, car_attributes_door, car_attributes_littleOldLadies).

  • Your selection problem becomes that of applying a LEFT OUTER JOIN to include the extended attributes that you want to include.
    • Slower than normalized, but not as slow as EAV.
  • Adding new extended attributes becomes a problem of adding a new table.
    • Harder than EAV, easier/faster than modifying table schema.
  • Deleting attributes becomes a problem of dropping whole tables.
    • Easier/faster than modifying table schema.
  • These new attributes can be strongly typed.
    • As good as modifying table schema, faster than EAV or generic columns.

Самое большое преимущество этого подхода, которое я вижу, заключается в том, что удаление неиспользуемых атрибутов довольно просто по сравнению с любым другим с помощью одной команды DROP TABLE. У вас также есть возможность позже нормализовать часто используемые атрибуты в более крупные группы или в основную таблицу, используя один процесс ALTER TABLE, а не один для каждого нового столбца, который вы добавляли по мере их добавления, что помогает с медленными запросами LEFT OUTER JOIN.

Самым большим недостатком является то, что вы загромождаете свой список таблиц, что, по общему признанию, часто не является тривиальной проблемой. Это, и я не уверен, насколько лучше LEFT OUTER JOIN работают, чем соединения таблиц EAV. Это определенно ближе к производительности соединения EAV, чем производительность нормализованной таблицы.

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

person JVal90    schedule 11.03.2020

Я бы попробовал ЭАВ.

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

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

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

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

ent_id, ent_name, ... ent_id, attr_name, attr_type, attr_value...

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

person Tim3880    schedule 14.05.2015

У меня есть ответ низкого качества, но, возможно, он получен из тегов HTML, например: <tag width="10px" height="10px" ... />

Таким грязным способом у вас будет только один столбец как varchar(max) для всех свойств, скажем, столбец Props, и вы будете хранить в нем данные следующим образом:

Props
------------------------------------------------------------
Model:Model of car1|Year:2010|# of doors:4
Model:Model of car2|NewProp1:NewValue1|NewProp2:NewValue2

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

Для большей достоверности специальных символов, таких как ':' и '|', я предлагаю '@:@' и '@|@' или что-то более редкое для разделительной части.


Аналогичным образом вы можете использовать поле text или binary и хранить данные XML в столбце.

person shA.t    schedule 13.05.2015