Чи коли-небудь добре використовувати списки у реляційній базі даних?


94

Я намагався створити базу даних, щоб відповідати концепції проекту, і наткнувся на те, що здається гарячою дискусією. Я прочитав декілька статей і кілька відповідей на переповнення стека, що стверджує, що ніколи (або майже ніколи) не в порядку зберігати список ідентифікаторів або подібне у полі - усі дані мають бути реляційними тощо.

Однак проблема, з якою я стикаюся, полягає в тому, що я намагаюся зробити завдання, що відповідає за завдання. Люди створюватимуть завдання, призначатимуть їх кільком людям, і це збережеться в базі даних.

Звичайно, якщо я збережу ці завдання окремо в "Особі", мені доведеться мати десятки фіктивних стовпців "TaskID" і мікро-керувати ними, оскільки, скажімо, однією людиною може бути від 0 до 100 завдань.

Потім знову, якщо я збережу завдання в таблиці «Завдання», мені доведеться мати десятки підставних стовпців «PersonID» і мікро-керувати ними - та сама проблема, як і раніше.

Для такої проблеми, чи добре зберігати список ідентифікаторів, що приймають ту чи іншу форму, чи я просто не думаю про інший спосіб, якого можна досягти, не порушуючи принципів?


22
Я розумію, що це позначено "реляційною базою даних", тому я просто залишу це як коментар не як відповідь, але в інших типах баз даних має сенс зберігати списки. Кассандра приходить до тями, оскільки не має приєднань.
Людина капітана

12
Гарна робота в дослідженні, а потім прохання тут! Справді, «рекомендація» ніколи не порушувати 1-ю нормальну форму справді вдала для вас, тому що ви дійсно повинні придумати інший, реляційний підхід, а саме відношення «багато-багато-багато», для якого існує стандартна модель у реляційні бази даних, які слід використовувати.
JimmyB

6
"Чи це колись добре" так .... що далі, відповідь - так. Поки у вас є поважна причина. Завжди є випадок використання, який змушує вас порушувати кращі практики, оскільки це має сенс. (У вашому випадку, правда, ви точно не повинні)
xyious

3
Наразі я використовую масив ( не обмежений рядок - а VARCHAR ARRAY) для зберігання списку тегів. Це, мабуть, не так, як вони будуть зберігатися пізніше внизу рядка, але списки можуть бути надзвичайно корисними на етапах складання прототипів, коли вам більше нічого не вказувати і не хочете складати всю схему бази даних, перш ніж ви зможете робити все інше.
Нік Хартлі

3
@Ben " (хоча вони не підлягають індексації) " - у Postgres кілька запитів до стовпців JSON (і, ймовірно, XML, хоча я не перевіряв) підлягають індексації.
Нік Хартлі

Відповіді:


249

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

Що б ви не робили, а не додавали інформацію про завдання до особи чи таблиці завдань, чи додаєте ви нову таблицю з цією інформацією про призначення, з відповідними відносинами.

Наприклад, у вас є такі таблиці:

Кількість осіб:

+ −−−− + + −−−−−−−−−−−− +
| ID | Назва |
+ ==== + =========== +
| 1 | Альфред |
| 2 | Єбедія |
| 3 | Яків |
| 4 | Єзекіїль |
+ −−−− + + −−−−−−−−−−−− +

Завдання:

+ −−− -2 +
| ID | Назва |
+ ==== + ===================== +
| 1 | Годуйте курчат |
| 2 | Плуг |
| 3 | Доїти корів |
| 4 | Підняти сарай |
+ −−− -2 +

Потім ви створили б третю таблицю із завданнями. Ця таблиця буде моделювати взаємозв'язок між людьми та завданнями:

+ −−− -2 +
| ID | ОсобаId | ЗавданняId |
+ ==== + =========== + ========= +
| 1 | 1 | 3 |
| 2 | 3 | 2 |
| 3 | 2 | 1 |
| 4 | 1 | 4 |
+ −−− -2 +

Тоді у нас виникне обмеження зовнішнього ключа, таким чином, що база даних буде виконувати обов'язки, що PersonId та TaskIds повинні бути дійсними ідентифікаторами для цих іноземних позицій. Для першого ряду ми можемо побачити PersonId is 1, так Альфред призначається TaskId 3, доївши корів .

Що ви повинні бачити тут, це те, що ви можете мати якнайменше або стільки завдань на завдання чи на людину, скільки ви хочете. У цьому прикладі Єзекіїлю не призначено жодних завдань, а Альфреду призначено 2. Якщо у вас є одне завдання зі 100 людьми, виконуватимете SELECT PersonId from Assignments WHERE TaskId=<whatever>;100 рядків, призначаючи безліч різних осіб. Ви можете WHEREв PersonId знайти всі завдання, призначені цій людині.

Якщо ви хочете повернути запити, що замінюють ідентифікатори іменами та завданнями, тоді ви дізнаєтесь, як об’єднати таблиці.


86
Ключове слово, яке ви хочете шукати, щоб дізнатися більше, це " стосунки " багато до багатьох "
BlueRaja - Danny Pflughoeft

34
Щоб детальніше зупинитися на коментарі Thierrys: Ви можете подумати, що вам не потрібно нормалізуватися, тому що мені потрібен лише X, і зберігати список ідентифікаторів дуже просто , але для будь-якої системи, яка згодом може розширитися, ви пошкодуєте, що не нормалізували її раніше. Завжди нормалізувати ; питання лише в якій нормальній формі
Ян Догген

8
Погодився з @Jan - проти мого кращого переконання я дозволив моїй команді короткий час зробити дизайн, зберігаючи JSON замість чогось, що "не потрібно буде продовжувати". Це тривало як півроку FML. Тоді у нашого випускника була неприємна боротьба за міграцію JSON до схеми, з якої ми повинні були почати. Я справді повинен був знати краще.
Гонки легкості по орбіті

13
@Deduplicator: це лише подання стовпця первинного ключа з цільним цілим числом автоматичного збільшення. Досить типовий матеріал.
whatsisname

8
@whatsisname Я б погодився з вами у таблиці "Особи або завдання". На мостовій таблиці, де єдиною метою є представлення зв'язку між багатьма іншими таблицями, у яких вже є сурогатні ключі? Я б не додав його без поважних причин. Це просто накладні витрати, оскільки його ніколи не використовуватимуть у запитах чи стосунках.
jpmc26

35

Ви задаєте тут два запитання.

По-перше, ви запитуєте, чи нормально зберігати списки, серіалізовані у стовпці. Так, його штраф. Якщо ваш проект вимагає цього. Прикладом можуть бути інгредієнти продуктів для сторінки каталогу, де ви не бажаєте намагатися відстежувати кожен інгредієнт окремо.

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


9
Приклад інгредієнта, на який ви посилаєтесь, є правильним на поверхні; але в цьому випадку це був би простий текст. Це не перелік у програмовому розумінні (якщо ви не маєте на увазі, що рядок - це список символів, яких ви, очевидно, немає). ОП, описуючи їх дані як "перелік ідентифікаторів" (або навіть просто "список [..]"), означає, що вони в якийсь момент обробляють ці дані як окремі об'єкти.
Flater

10
@Flater: Але це список. Потрібно мати можливість переформатувати його у вигляді (різного роду) списку HTML, списку відміток, списку JSON тощо для того, щоб забезпечити належне відображення елементів на веб-сторінці, простому текстовому документі, мобільному пристрої додаток ... і ви не можете реально зробити це звичайним текстом.
Кевін

12
@Kevin Якщо це ваша мета, то це набагато легше і легше досягти, зберігаючи інгредієнти в таблиці! Не кажучи вже про те, якби згодом люди хотіли б… о, я не знаю, скажімо, бажав би рекомендованих замінників чи чогось нерозумного, як шукати всі рецепти без арахісу, ні клейковини, ні тваринних білків…
Ден Брон

10
@DanBron: YAGNI. Зараз ми використовуємо лише список, оскільки це полегшує логіку інтерфейсу користувача. Якщо нам потрібна або потрібна поведінка у вигляді списку на рівні бізнес-логіки, то її слід нормалізувати в окрему таблицю. Столи та з'єднання не обов'язково дорогі, але вони не безкоштовні, і вони задають питання про порядок елементів ("Чи ми дбаємо про порядок інгредієнтів?") Та подальшій нормалізації ("Чи збираєтесь ви перетворити" 3 яйця " в ("яйця", 3)? Що з "Сіллю на смак", це ("сіль", NULL)? ").
Кевін

7
@Kevin: YAGNI тут абсолютно неправильний. Ви самі доводили необхідність можливості перетворення списку різними способами (HTML, розмітка, JSON) і тим самим стверджуєте, що вам потрібні окремі елементи списку . Якщо додатки для зберігання даних та програми "обробка списку" не є двома програмами, які розробляються незалежно (і зауважте, що окремі рівні додатків! = Окремі програми), структура бази даних завжди повинна створюватися для зберігання даних у форматі, який залишає їх легко доступними - уникаючи додаткової логіки розбору / перетворення.
Пісня

22

Те, що ви описуєте, відоме як "багато для багатьох", у вашому випадку між Personі Task. Зазвичай він реалізується за допомогою третьої таблиці, яку іноді називають таблицею "посилання" або "перехресне посилання". Наприклад:

create table person (
    person_id integer primary key,
    ...
);

create table task (
    task_id integer primary key,
    ...
);

create table person_task_xref (
    person_id integer not null,
    task_id integer not null,
    primary key (person_id, task_id),
    foreign key (person_id) references person (person_id),
    foreign key (task_id) references task (task_id)
);

2
Ви також можете додати індекс task_idспочатку, якщо ви можете робити запити відфільтровані за завданням.
jpmc26

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

13

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

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

Оскільки «список», за визначенням, складається з менших елементів (елементів), це не так, і вам слід нормалізувати дані.

... якщо я збережу ці завдання окремо в "Person", мені доведеться мати десятки "фіктивних" стовпців "...

Ні. У таблиці перетину (слабка сутність) між особою та завданням буде кілька рядків . Бази даних справді добре працюють з великою кількістю рядків; вони насправді досить сміття при роботі з великою кількістю [повторних] стовпців.

Хороший чіткий приклад, який дається whatsisname.


4
При створенні систем реального життя "ніколи не кажи ніколи" - дуже хороше правило, яким потрібно жити.
l0b0

1
У багатьох випадках вартість елемента на підтримання або отримання списку в нормалізованому вигляді може значно перевищувати витрати на збереження елементів як крапку, оскільки кожен елемент списку повинен містити ідентифікацію основного елемента, з яким він знаходиться асоціюється та його місцезнаходження в списку на додаток до фактичних даних. Навіть у випадках, коли код може отримати користь від можливості оновлення деяких елементів списку без оновлення всього списку, може бути дешевше зберігати все як крапку та переписувати все, коли потрібно щось переписувати.
supercat

4

Це може бути законним у певних попередньо розрахованих полях.

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

Наприклад, у користувальницькому інтерфейсі ви хочете показати цей список за допомогою перегляду сітки, де кожен рядок може відкрити повну інформацію (із повними списками) після подвійного клацання:

REGISTERED USER LIST
+------------------+----------------------------------------------------+
|Name              |Top 3 most visited tags                             |
+==================+====================================================+
|Peter             |Design, Fitness, Gifts                              |
+------------------+----------------------------------------------------+
|Lucy              |Fashion, Gifts, Lifestyle                           |
+------------------+----------------------------------------------------+

Ви будете тримати другий стовпець оновленим за допомогою тригера, коли клієнт відвідує нову статтю або заплановане завдання.

Ви можете зробити таке поле доступним навіть для пошуку (як звичайний текст).

У таких випадках ведення списків є законним. Вам просто потрібно розглянути випадок можливого перевищення максимальної довжини поля.


Крім того, якщо ви використовуєте Microsoft Access, пропоновані багатозначні поля є ще одним особливим випадком використання. Вони обробляють ваші списки в полі автоматично.

Але ви завжди можете повернутися до стандартної нормованої форми, показаної в інших відповідях.


Резюме: Нормальні форми бази даних - це теоретична модель, необхідна для розуміння важливих аспектів моделювання даних. Але звичайно, нормалізація не враховує ефективність чи інші витрати на отримання даних. Це не виходить за межі цієї теоретичної моделі. Але зберігання списків або інших попередньо розрахованих (і контрольованих) дублікатів часто вимагає практичної реалізації.

Зважаючи на вищезазначене, на практиці ми б вважали за краще запит, що спирається на ідеальну нормальну форму та працює 20 секунд, або еквівалентний запит, покладаючись на попередньо обчислені значення, що займає 0,08 с? Ніхто не любить, щоб їх програмний продукт звинувачувався у повільності.


1
Це може бути законним навіть без попередніх розрахунків. Я робив це кілька разів, коли дані зберігаються належним чином, але з міркувань продуктивності корисно ввести кілька кешованих результатів у основні записи.
Лорен Печтел

@LorenPechtel - Так, дякую, в моєму використанні попередньо обчисленого терміна я також включаю випадки кешованих значень, які зберігаються там, де це потрібно. У системах зі складними залежностями вони є способом нормальної роботи. І якщо вони запрограмовані з адекватним ноу-хау, ці значення надійні та завжди синхронізовані. Я просто не хотів додавати випадок кешування у відповідь, щоб відповідь була простою та безпечною. Це все-таки заборонили. :)
miroxlav

@LorenPechtel Власне, це все-таки буде поганою причиною ... дані кешу повинні зберігатися в проміжному сховищі кешу, і хоча кеш все ще дійсний, цей запит ніколи не повинен потрапляти в основну БД.
Тезра

1
@Tezra Ні, я кажу, що інколи частину даних із вторинної таблиці потрібно досить часто, щоб мати сенс помістити копію в основний запис. (Приклад, який я зробив - таблиця службовців включає останній час у та останній вихідний час. Вони використовуються лише для цілей відображення; будь-який фактичний розрахунок надходить із таблиці із записами годинник / час виходу.)
Лорен Печтел

0

Дано дві таблиці; ми будемо називати їх Person and Task, кожен з яких має свій ідентифікатор (PersonID, TaskID) ... Основна ідея полягає у створенні третьої таблиці, щоб зв’язати їх разом. Ми будемо називати цю таблицю PersonToTask. Як мінімум, він повинен мати власний ідентифікатор, а також два інших. Отже, якщо мова йде про призначення когось завдання; вам більше не потрібно буде ОНОВЛЮВАТИ таблицю Person, просто потрібно ВСТАВИТИ новий рядок у PersonToTaskTable. А технічне обслуговування стає простішим - необхідність видалити завдання просто стає DELETE на основі TaskID, не оновлюючи таблицю Person та пов'язаний з цим аналіз

CREATE TABLE dbo.PersonToTask (
    pttID INT IDENTITY(1,1) NOT NULL,
    PersonID INT NULL,
    TaskID   INT NULL
)

CREATE PROCEDURE dbo.Task_Assigned (@PersonID INT, @TaskID INT)
AS
BEGIN
    INSERT PersonToTask (PersonID, TaskID)
    VALUES (@PersonID, @TaskID)
END

CREATE PROCEDURE dbo.Task_Deleted (@TaskID INT)
AS
BEGIN
    DELETE PersonToTask  WHERE TaskID = @TaskID
    DELETE Task          WHERE TaskID = @TaskID
END

Як щодо простого звіту або кого все призначено для завдання?

CREATE PROCEDURE dbo.Task_CurrentAssigned (@TaskID INT)
AS
BEGIN
    SELECT PersonName
    FROM   dbo.Person
    WHERE  PersonID IN (SELECT PersonID FROM dbo.PersonToTask WHERE TaskID = @TaskID)
END

Ви, звичайно, могли б зробити набагато більше; a TimeReport можна зробити, якщо ви додали поля DateTime для TaskAssigned та TaskCompleted. Все залежить від вас


0

Це може спрацювати, якщо ви скажете, що у вас є легкі для читання людиною Первинні ключі та ви хочете перелік № # завдання, не маючи справу з вертикальним характером структури таблиці. тобто набагато простіше читати першу таблицю.

------------------------  
Employee Name | Task 
Jack          |  1,2,5
Jill          |  4,6,7
------------------------

------------------------  
Employee Name | Task 
Jack          |  1
Jack          |  2
Jack          |  5
Jill          |  4
Jill          |  6
Jill          |  7
------------------------

Тоді виникає питання: чи слід зберігати або формувати список завдань на вимогу, що значною мірою залежатиме від таких вимог, як: як часто потрібен список, наскільки точно існує кількість рядків даних, як будуть використовуватися дані тощо. .. після чого слід проаналізувати компроміси з урахуванням досвіду користувача та задоволення вимог.

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

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


Як ви кажете, все залежить від способу отримання даних. Якщо ви / тільки / коли-небудь запитуєте цю таблицю за ім'ям користувача, то поле "список" є цілком адекватним. Однак як можна запитати таку таблицю, щоб дізнатися, хто працює над Завданням # 1234567, і як і раніше підтримувати її виконавцем? Практично про всі види рядкової функції "find-X-where-in-the-field" буде викликати такий запит до / Table Scan /, сповільнюючи роботу сканування. З нормально нормалізованими, правильно індексованими даними цього просто не відбувається.
Філл В.

0

Ви берете інший стіл, перевертаючи його на 90 градусів і вбираючи його в інший стіл.

Це як би мати таблицю замовлень, де у вас є itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. Окрім того, що неприємно впоратися з програмою, ви можете гарантувати, що завтра хтось захоче замовити 38 речей.

Я зробив би це так, якби "список" насправді не є списком, тобто там, де він стоїть у цілому, і кожна окрема позиція не посилається на якусь чітку та незалежну сутність. У такому випадку просто заповнити все у певному типі даних, який є досить великим.

Отже, замовлення - це список, Білла Матеріалів - це перелік (або список списків, який був би ще більшим кошмаром для впровадження «набік»). Але примітка / коментар і вірш - ні.


0

Якщо це "не нормально", то досить погано, що кожен сайт Wordpress коли-небудь має список у wp_usermeta з wp_capability в одному ряду, список відхилених_wp_pointers в одному ряду та інші ...

Насправді у таких випадках це може бути краще для швидкості, оскільки ви майже завжди хочете цього списку . Але Wordpress, як відомо, не є ідеальним прикладом найкращих практик.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.