Динамични колони в таблици на база данни срещу EAV

Опитвам се да реша кой път да поема, ако имам приложение, което трябва да може да променя db схемата въз основа на въведеното от потребителя.

Например, ако имам обект "автомобил", който съдържа свойства на автомобил, като година, модел, брой врати и т.н., как да го съхраня в DB по такъв начин, че потребителят да може да добавя нови свойства?

Четох за 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_ ще съдържат стойностите на атрибутите за даден обект (например "червено").

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

INSERTs са доста ясни. Трябва само да вмъкнете нов обект, куп атрибути и техните релации. Например, за да вмъкнете нов обект с 3 атрибута, ще трябва да изпълните 7 вмъквания (едно за обекта, още три за атрибутите и още три за връзките.

Когато искате да извършите UPDATE, ще трябва да знаете кой е обектът, който искате да актуализирате, и да актуализирате съединяването на желания атрибут с връзката между обекта и неговите атрибути.

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

Но когато искате да изпълните SELECT, нещата стават гадни (трябва да пишете наистина трудни заявки) и производителността пада ужасно.

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

SELECT brand, model FROM cars;

Ако проектирате общ модел на данни, както в примера, SELECT за запитване до всички ваши съхранени автомобили ще бъде наистина трудно за писане и ще включва свързване на 3 таблици. Заявката ще се представи много зле.

Също така помислете за определението на вашите атрибути. Всички ваши атрибути се съхраняват като TEXT и това може да е проблем. Ами ако някой сгреши и запише "червено" като цена?

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

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

Единственото предимство на използването на 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

Динамични колони в таблици на база данни

От друга страна, можете, както казвате, да генерирате таблиците динамично, добавяйки (и премахвайки) колони, когато е необходимо. В този случай можете например да създадете таблица кола с основните атрибути, които знаете, че ще използвате, и след това да добавяте колони динамично, когато имате нужда от тях (например броя на ауспусите).

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

Този модел, както казвате, също има друг проблем при работа с 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;

Както и да е, наистина не мисля, че ще трябва да изтривате колони (или поне това ще бъде много рядък сценарий). Може би някой добавя броя на вратите като колона и съхранява кола с това свойство. Ще трябва да се уверите, че някоя от вашите коли има това свойство, за да предотвратите загуба на данни, преди да изтриете колоната. Но това, разбира се, зависи от вашия конкретен сценарий.

Друг недостатък на това решение е, че ще ви е необходима таблица за всеки обект, който искате да съхранявате (една маса за съхраняване на коли, друга за съхраняване на къщи и т.н.).

Друга опция (псевдогенеричен модел)

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

Да приемем, че създавате таблица като тази:

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

В тази таблица можете да съхранявате всеки субект (коли, къщи, кучета), защото имате поле за тип и можете да съхранявате колкото се може повече атрибути за всеки субект, както искате (N в този случай).

Ако трябва да знаете какво означава attribute37, когато type е "червен", ще трябва да добавите друга таблица, която свързва типовете и атрибутите с описанието на атрибутите .

И какво ще стане, ако установите, че един от вашите обекти се нуждае от повече атрибути? След това просто добавете нови колони към таблицата entities (атрибутN+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
Благодаря за подробното обяснение! Ако отида с псевдогенеричния модел, няма ли db да стане огромен? Искам да кажа, че бих имал около 40 колони, много от тях ще бъдат празни за повечето записи и всички те трябва да бъдат индексирани, за да могат потребителите да филтрират автомобили по атрибути. С EAV трябва само да вмъкна атрибути, които съществуват. Btw моята версия на EAV е единична таблица с име, стойност и външен ключ, който сочи към таблицата на колата - person Alex; 13.05.2015
comment
@Alex Добре дошъл! Редактирах отговора си, за да се опитам да отговоря на вашите опасения - person antonio; 13.05.2015

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

Ако трябва да изпълнявате заявки, които трябва да правят сложни сравнения или обединения на данни, чиято схема не знаете предварително, SQL и релационният модел рядко са подходящи.

Например, ако вашите потребители могат да настроят произволни обекти с данни (като „кола“ във вашия пример) и след това искат да намерят автомобили, чийто обем на двигателя е по-голям от 2000 кубика, с поне 3 врати, произведени след 2010 г., чийто настоящ собственик е част от таблицата "малки стари дами", не знам за елегантен начин да се направи това в 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.

Добавянето на колони въз основа на въвеждане от потребителя не ми звучи добре и можете бързо да изчерпите капацитета си. Заявките върху много плоска маса също могат да бъдат проблем. Искате ли да създадете стотици индекси?

Вместо да пиша всяко нещо в една таблица, бих съхранил възможно най-много общи свойства (цена, име, цвят, ...) в основната таблица и тези по-рядко срещани свойства в таблица с "допълнителни" атрибути. Винаги можете да ги балансирате по-късно с малко усилия.

EAV може да работи добре за малък до среден набор от данни. Тъй като искате да използвате SQLlite, предполагам, че това не е проблем.

Може също да искате да избегнете „прекомерното“ нормализиране на вашите данни. С евтиното хранилище, което имаме в момента, можете да използвате една таблица за съхраняване на всички „Допълнителни“ атрибути, вместо две:

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

Хората против EAV ще кажат, че неговата производителност е лоша в голяма база данни. Сигурно е, че няма да има производителност, както и нормализирана структура, но вие също не искате да променяте структурата на 3TB маса.

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