Можно ли использовать списки в реляционной базе данных?

94

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

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

Конечно, если я сохраню эти задачи по отдельности в «Персоне», мне понадобятся десятки фиктивных столбцов «TaskID» и микро-управление ими, поскольку, скажем, одному человеку может быть назначено от 0 до 100 задач.

Опять же, если я сохраню задачи в таблице «Задачи», мне понадобятся десятки фиктивных столбцов «PersonID» и микро-управление ими - та же проблема, что и раньше.

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

linus72982
источник
22
Я понимаю , что помечено «реляционная база данных» , так что я просто оставить его в качестве комментария не ответ, но и в других типах баз данных он делает , имеет смысл хранить списки. Кассандра приходит на ум, так как у нее нет соединений.
Капитан Мэн
12
Хорошая работа в исследовании, а затем спрашивать здесь! Действительно, «рекомендация» никогда не нарушать 1-ую нормальную форму действительно хорошо для вас, потому что вы действительно должны придумать другой, реляционный подход, а именно отношение «многие ко многим», для которого есть стандартный шаблон в реляционные базы данных, которые следует использовать.
JimmyB
6
"Это когда-нибудь хорошо" да .... что бы ни следовало, ответ - да. Пока у тебя есть веская причина. Всегда есть сценарий использования, который заставляет вас нарушать лучшие практики, потому что это имеет смысл сделать. (В вашем случае, однако, вы определенно не должны)
xyious
3
В настоящее время я использую массив (а не строку с разделителями - а VARCHAR ARRAY) для хранения списка тегов. Вероятно, это не то, как они в конечном итоге будут храниться в дальнейшем, но списки могут быть чрезвычайно полезны на этапах создания прототипов, когда вам не на что больше указывать и вы не хотите создавать всю схему базы данных, прежде чем сможете делай что-нибудь еще
Ник Хартли
3
@Ben « (хотя они не будут индексироваться) » - в Postgres, несколько запросов к колонкам JSON (и , вероятно XML, хотя я не проверил) являются индексируемая.
Ник Хартли

Ответы:

249

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

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

Например, у вас есть следующие таблицы:

Количество человек:

+ ---- + ----------- +
| ID | Имя |
+ ==== + =========== +
| 1 | Альфред |
| 2 | Джебедия |
| 3 | Джейкоб |
| 4 | Иезекииль |
+ ---- + ----------- +

Задачи:

+ ---- + -------------------- +
| ID | Имя |
+ ==== + ==================== +
| 1 | Накормить Цыплят |
| 2 | Плуг |
| 3 | Доения коров |
| 4 | Поднять сарай |
+ ---- + -------------------- +

Затем вы создадите третью таблицу с заданиями. Эта таблица будет моделировать отношения между людьми и задачами:

+ ---- + ----------- + --------- +
| ID | PersonId | TaskId |
+ ==== + =========== ========= + +
| 1 | 1 | 3 |
| 2 | 3 | 2 |
| 3 | 2 | 1 |
| 4 | 1 | 4 |
+ ---- + ----------- + --------- +

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

То, что вы должны увидеть здесь, это то, что у вас может быть как можно меньше заданий или заданий на одного человека или на одного человека. В этом примере Иезекиилю не назначены никакие задачи, а Альфреду назначено 2. Если у вас есть одно задание с 100 людьми, выполнение SELECT PersonId from Assignments WHERE TaskId=<whatever>;приведет к 100 строкам с различными назначенными людьми. Вы можете WHEREв PersonId найти все задачи, назначенные этому человеку.

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

как зовут
источник
86
Ключевое слово, которое вы хотите найти, чтобы узнать больше, это «отношения многие ко многим »
BlueRaja - Дэнни Пфлугхофт
34
Немного поясним комментарий Тьерриса: Вы можете подумать, что вам не нужно нормализовать, потому что мне нужен только X, и очень просто сохранить список идентификаторов , но для любой системы, которая может быть расширена позже, вы пожалеете, что не нормализовали ее. ранее. Всегда нормализуйся ; вопрос только в том, какая нормальная форма
Ян Догген,
8
Согласился с @Jan - вопреки моему здравому мнению, я позволил моей команде некоторое время назад использовать ярлык дизайна, вместо этого сохраняя JSON для чего-то, что «не нужно расширять». Это продолжалось как шесть месяцев FML. Затем у нашего модератора была неприятная борьба за перенос JSON на схему, с которой мы должны были начать. Я действительно должен был знать лучше.
Гонки
13
@Deduplicator: это просто представление целочисленного первичного ключа столбца с автоматическим приращением. Довольно типичные вещи.
whatsisname
8
@whatsisname В таблице «Персоны» или «Задачи» я бы с вами согласился. На промежуточной таблице, где единственной целью является представление отношения «многие ко многим» между двумя другими таблицами, которые уже имеют суррогатные ключи? Я бы не стал добавлять один без уважительной причины. Это просто накладные расходы, поскольку они никогда не будут использоваться в запросах или отношениях.
jpmc26
35

Вы задаете два вопроса здесь.

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

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

GrandmasterB
источник
9
Пример ингредиента, на который вы ссылаетесь, является правильным на поверхности; но это было бы открытым текстом в этом случае. Это не список в смысле программирования (если только вы не имеете в виду, что строка - это список символов, который вы явно не делаете). ОП, описывающая свои данные как «список идентификаторов» (или даже просто «список [..]»), подразумевает, что они в какой-то момент обрабатывают эти данные как отдельные объекты.
Flater
10
@Flater: Но это список. Вы должны иметь возможность переформатировать его как (по-разному) список HTML, список уценок, список JSON и т. Д., Чтобы обеспечить правильное отображение элементов на (по-разному) веб-странице, текстовом документе, мобильном устройстве. приложение ... и вы не можете сделать это с обычным текстом.
Кевин
12
@Kevin Если это ваша цель, то гораздо проще и проще достичь, храня ингредиенты в столе! Не говоря уже о том, будут ли люди позже ... ох, я не знаю, скажем, пожелания рекомендуемых заменителей или что-то глупое, похожее на поиск всех рецептов без арахиса, глютена или животных белков ...
Дэн Брон
10
@DanBron: ЯГНИ. Сейчас мы используем только список, потому что он упрощает логику пользовательского интерфейса. Если нам нужно или потребуется поведение, подобное списку, на уровне бизнес-логики, то оно должно быть нормализовано в отдельную таблицу. Таблицы и объединения не обязательно дороги, но они не бесплатны, и они вызывают вопросы о порядке элементов («Заботимся ли мы о порядке ингредиентов?») И дальнейшей нормализации («Собираетесь ли вы превратить 3 яйца»? в («яйца», 3)? А как насчет «соли по вкусу» («соль», NULL)? »).
Кевин
7
@Kevin: ЯГНИ совершенно не прав. Вы сами утверждали о необходимости возможности трансформировать список разными способами (HTML, markdown, 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", мне понадобятся десятки фиктивных столбцов "TaskID" ...

Нет. У вас будет несколько строк в Таблице пересечений (она же Слабая сущность) между человеком и задачей. Базы данных действительно хороши для работы с большим количеством строк; они на самом деле довольно глупы, работая с множеством [повторяющихся] столбцов.

Хороший четкий пример, приведенный по whatsisname.

Фил В.
источник
4
При создании реальных систем «никогда не говори никогда» это очень хорошее правило, по которому нужно жить.
10
1
Во многих случаях стоимость каждого элемента для поддержания или извлечения списка в нормализованной форме может значительно превышать стоимость хранения элементов в виде большого двоичного объекта, поскольку каждый элемент списка должен содержать идентичность главного элемента, с которым он связано и его местоположение в списке в дополнение к фактическим данным. Даже в тех случаях, когда код может выиграть от возможности обновления некоторых элементов списка без обновления всего списка, может быть дешевле хранить все как большой двоичный объект и переписывать все, когда нужно что-то переписать.
суперкат
4

Это может быть законно в определенных предварительно рассчитанных полях.

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

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

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

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

Вы можете сделать такое поле доступным даже для поиска (как обычный текст).

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


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

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


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

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

miroxlav
источник
1
Это может быть законно даже без заранее рассчитанных вещей. Я делал это пару раз, когда данные правильно хранятся, но по соображениям производительности полезно поместить несколько кэшированных результатов в основные записи.
Лорен Печтел
@LorenPechtel - Да, спасибо, в моем использовании предварительно вычисленного термина я также включаю случаи кэшированных значений, хранящихся там, где это необходимо В системах со сложными зависимостями они являются способом поддержания нормальной производительности. И если запрограммированы с адекватным ноу-хау, эти значения надежны и всегда синхронизированы. Я просто не хотел добавлять кеширование в ответ, чтобы ответ был простым и безопасным. В любом случае, это было отвергнуто. :)
miroxlav
@LorenPechtel На самом деле это все-таки плохая причина ... данные кеша должны храниться в промежуточном хранилище кеша, и хотя кеш все еще действителен, этот запрос никогда не должен попадать в основную БД.
Тезра
1
@ Tezra Нет, я говорю, что иногда часть данных из вторичной таблицы требуется достаточно часто, чтобы иметь смысл поместить копию в основную запись. (Пример, который я сделал - таблица сотрудников включает в себя последний и последний тайм-ауты. Они используются только для целей отображения, любой фактический расчет происходит из таблицы с записями о входе / выходе из времени.)
Лорен Печтел
0

Даны две таблицы; мы назовем их Person и Task, каждый со своим идентификатором (PersonID, TaskID) ... основная идея - создать третью таблицу, чтобы связать их вместе. Мы назовем эту таблицу PersonToTask. Как минимум, он должен иметь свой собственный идентификатор, а также два других. Так что, когда дело доходит до назначения кого-то для задачи; Вам больше не нужно ОБНОВЛЯТЬ таблицу Person, вам просто нужно ВСТАВИТЬ новую строку в PersonToTaskTable. И обслуживание становится проще - необходимость удаления задачи становится просто УДАЛИТЬ на основе 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

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

Mad Myche
источник
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 строк, и выполнения запроса, который сгенерирует 2 строки. Если это занимает много времени, а пользователю не нужен самый актуальный список (* ожидающий менее 1 изменения в день), то он может быть сохранен.

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

Двойной E CPU
источник
Как вы говорите, все зависит от того, как данные должны быть получены. Если вы / только / когда-либо запрашивали эту таблицу по имени пользователя, тогда поле «список» вполне подходит. Тем не менее, как вы можете запросить такую ​​таблицу, чтобы узнать, кто работает над задачей № 1234567, и при этом сохранить ее производительность? Почти каждая разновидность строковой функции find-X -where-in-the-field будет вызывать такой запрос к / Table Scan /, замедляя процесс сканирования. С правильно нормализованными, правильно проиндексированными данными этого просто не произойдет.
Фил В.
0

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

Это похоже на таблицу заказов, в которой есть itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. Помимо того, что вам неудобно обращаться с программным обеспечением, вы можете гарантировать, что завтра кто-то захочет заказать 38 вещей.

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

Таким образом, заказ - это список, а ведомость материалов - это список (или список списков, что было бы еще большим кошмаром для реализации «вбок»). Но примечание / комментарий и стихотворение - нет.

Bloke Down The Pub
источник
0

Если это «не нормально», то очень плохо, что каждый сайт Wordpress когда-либо имеет список в wp_usermeta с wp_capabilities в одной строке, список dismissed_wp_pointers в одной строке и другие ...

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

NoBugs
источник