Как сохранить список в столбце таблицы базы данных

117

Итак, согласно ответу Мехрдада на связанный вопрос , я понял, что «правильный» столбец таблицы базы данных не хранит список. Скорее, вы должны создать другую таблицу, которая эффективно содержит элементы указанного списка, а затем связать с ней напрямую или через таблицу соединений. Однако тип списка, который я хочу создать, будет состоять из уникальных элементов (в отличие от плода связанного вопросапример). Кроме того, элементы в моем списке явно отсортированы - это означает, что если бы я сохранил элементы в другой таблице, мне пришлось бы сортировать их каждый раз, когда я к ним обращался. Наконец, список в основном является атомарным в том смысле, что каждый раз, когда я хочу получить доступ к списку, я захочу получить доступ ко всему списку, а не только к его части, поэтому кажется глупым создавать запрос к базе данных, чтобы собрать вместе части список.

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

Есть ли лучшее решение? Если нет никакого лучшего решения, то почему? Кажется, что эта проблема должна время от времени возникать.

... еще немного информации, чтобы вы знали, откуда я. Как только я только начал разбираться в SQL и базах данных в целом, меня включили в LINQ to SQL, и теперь я немного избалован, потому что рассчитываю работать со своей объектной моделью программирования, не задумываясь о том, как объекты запрашиваются или хранятся в базе данных.

Спасибо всем!

Джон

ОБНОВЛЕНИЕ: Итак, в первом шквале ответов, которые я получаю, я вижу «вы можете пойти по маршруту CSV / XML ... но НЕ НУЖНО!». Итак, теперь я ищу объяснения, почему. Укажите мне несколько хороших ссылок.

Кроме того, чтобы вы лучше понимали, чем я занимаюсь: в моей базе данных есть таблица функций, в которой будет список пар (x, y). (В таблице также будет другая информация, не имеющая значения для нашего обсуждения.) Мне никогда не понадобится видеть часть списка пар (x, y). Скорее, я возьму их все и нанесу на экран. Я разрешаю пользователю перетаскивать узлы, чтобы время от времени изменять значения или добавлять дополнительные значения к графику.

JnBrymn
источник

Ответы:

183

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

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

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

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

По сути, первое правило (или форма) нормализации гласит, что ваша таблица должна представлять отношение. Это означает, что:

  • Вы должны иметь возможность отличать одну строку от любой другой (другими словами, в вашей таблице должно быть что-то, что может служить первичным ключом. Это также означает, что ни одна строка не должна дублироваться.
  • Любой порядок данных должен определяться данными, а не физическим порядком строк (SQL основан на идее набора, что означает, что единственный порядок, на который вы должны полагаться, это тот, который вы явно определяете в своем запросе)
  • Каждое пересечение строки / столбца должно содержать одно и только одно значение

Последний пункт, очевидно, является здесь наиболее важным. SQL предназначен для хранения ваших наборов для вас, а не для того, чтобы предоставить вам «ведро» для хранения набора самостоятельно. Да, это возможно. Нет, миру не конец. Однако вы уже искалечили себя в понимании SQL и передовых практик, которые с ним связаны, сразу же перейдя к использованию ORM. LINQ to SQL - это фантастика, как и графические калькуляторы. Однако в том же духе их не следует использовать как замену знанию того, как на самом деле работают используемые ими процессы.

Теперь ваш список может быть полностью «атомарным», и это не может измениться для этого проекта. Но вы, тем не менее, приобретете привычку делать аналогичные вещи в других проектах, и в конечном итоге (вероятно, быстро) столкнетесь со сценарием, в котором теперь вы вставляете свой быстрый и легкий список в столбце. подходить там, где это совершенно неуместно. Для создания правильной таблицы для того, что вы пытаетесь сохранить, не требуется много дополнительной работы, и другие разработчики SQL не будут насмехаться над вами, когда они увидят ваш проект базы данных. Кроме того, LINQ to SQL увидит ваше отношение и автоматически предоставит вам правильный объектно-ориентированный интерфейс для вашего списка . Почему вы отказываетесь от удобства, предлагаемого ORM, чтобы вы могли выполнять нестандартные и необдуманные взломы базы данных?

Адам Робинсон
источник
17
Итак, вы твердо уверены, что хранить список в столбце - плохая идея, но не можете указать почему. Так как я только начинаю работать с SQL, немного «почему» было бы действительно очень полезно. Например, вы говорите, что я «веду тяжелую битву и нарушаю один из самых основных принципов проектирования реляционных баз данных без уважительной причины» ... так в чем же принцип? Почему причины, которые я назвал «бесполезными»? (в частности, отсортированный и атомарный характер моих списков)
JnBrymn
6
По сути, все сводится к многолетнему опыту, воплощенному в лучших практиках. Рассматриваемый основной принцип известен как 1-я нормальная форма .
Тоби
1
Спасибо, Адам. Очень информативно. Хороший ответ на ваш последний вопрос.
JnBrymn
8
«[…] И другие разработчики SQL не будут насмехаться над вами, когда они увидят ваш проект базы данных». Есть очень веские причины уважать Первую нормальную форму (и в вашем ответе они упоминаются), но давление со стороны сверстников / «вот как здесь все делается» не входит в их число.
Lynn
5
Мы уже каждый день храним пачки списков в столбцах базы данных. Их называют «чар» и «варчар». Конечно, в Postgres они также называются текстовыми. На самом деле 1NF говорит о том, что вам никогда не следует разделять информацию в каком-либо поле на более мелкие поля, и если вы это сделаете, вы напортачите. Таким образом, вы не сохраняете имя, вы сохраняете личное имя, отчество и фамилии (в зависимости от локализации) и соединяете их вместе. В противном случае мы бы вообще не хранили строки текста. С другой стороны, все, что ему нужно, - это веревка. И есть способы сделать это.
Haakon Løtveit
15

Вы можете просто забыть о SQL и перейти к подходу «NoSQL». В качестве возможных решений приходят на ум RavenDB , MongoDB и CouchDB . При подходе NoSQL вы не используете реляционную модель ... вы даже не ограничены схемами.

Jaltiere
источник
11

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

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

nicknames(id,seq_no,names)

Предположим, вы хотите сохранить под одним идентификатором много никнеймов. Вот почему мы включили seq_noполе.

Теперь внесите эти значения в свою таблицу:

(1,1,'sweetheart'), (1,2,'pumpkin'), (2,1,'cutie'), (2,2,'cherry pie')

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

select names from nicknames where id = 1;
Х. Паувелин
источник
5

Простой ответ: если и только если вы уверены, что список всегда будет использоваться как список, тогда объедините список вместе с вашей стороны с помощью символа (например, '\ 0'), который не будет использоваться в текст когда-либо и сохраните это. Затем, когда вы его получите, вы можете разделить его на '\ 0'. Конечно, есть и другие способы решения этой проблемы, но они зависят от конкретного поставщика базы данных.

Например, вы можете хранить JSON в базе данных Postgres. Если ваш список является текстовым, и вам просто нужен список без лишних хлопот, это разумный компромисс.

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

Если вам нужен хороший способ делать такие вещи, обычно доступны массивы или аналогичные типы. Postgres, например, предлагает массив как тип и позволяет хранить массив текста, если это то, что вы хотите , и есть аналогичные приемы для MySql и MS SQL с использованием JSON, а IBM DB2 также предлагает тип массива (в своем собственная полезная документация). Это было бы не так часто, если бы в этом не было необходимости.

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

Хокон Лётвейт
источник
3

В дополнение к тому, что говорили все остальные, я предлагаю вам проанализировать свой подход в более долгосрочной перспективе, чем сейчас. В настоящее время вещи уникальны. В настоящее время для пересортировки элементов потребуется новый список. Практически требуется, чтобы список на данный момент был коротким. Несмотря на то, что у меня нет специфики предметной области, нетрудно предположить, что эти требования могут измениться. Если вы сериализуете свой список, вы теряете гибкость, которая не требуется в более нормализованном дизайне. Кстати, это не обязательно означает полноценные отношения «Многие: многие». У вас может быть только одна дочерняя таблица с внешним ключом для родительского элемента и символьный столбец для элемента.

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

Томас
источник
+1 для прогнозирования будущих изменений - всегда проектируйте свою модель данных так, чтобы она была расширяемой.
coolgeek
2

Я бы просто сохранил его как CSV, если это простые значения, тогда это должно быть все, что вам нужно (XML очень подробный, и его сериализация в / из него, вероятно, была бы излишней, но это тоже был бы вариант).

Вот хороший ответ, как вытащить CSV с помощью LINQ.

Дэвид Нил
источник
Я думал об этом. Это все еще означает, что мне придется сериализовать и десериализовать ... но я подозреваю, что это выполнимо. Я бы хотел, чтобы был какой-то оправданный способ делать то, что я хочу, но подозреваю, что это не так.
JnBrymn
capnproto.org - это способ избежать сериализации и десериализации, также быстро (по сравнению с csv или xml) в случае, если capnproto не поддерживается на выбранном вами языке msgpack.org/index.html
VoronoiPotato
2

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

Если вам всегда нужен список, вы можете сохранить его как список с разделителями в столбце. Даже в этом случае, если у вас нет ОЧЕНЬ конкретных причин не делать этого, сохраните его в таблице поиска.

домашний тост
источник
1

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

Вот и «традиционный» дизайн БД:

List(ListID, ListName) 
Item(ItemID,ItemName) 
List_Item(ListID, ItemID, SortOrder)

Вот ненормализованная таблица:

Lists(ListID, ListContent)

Идея здесь - вы ведете таблицу списков с помощью триггеров или кода приложения. Каждый раз, когда вы изменяете содержимое List_Item, соответствующие строки в списках обновляются автоматически. Если вы в основном читаете списки, это может сработать. Плюсы - можно читать списки в одном утверждении. Минусы - обновления требуют больше времени и усилий.

Алсин
источник
0

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

Дэвид Дэниел
источник
0

Я думаю, что в некоторых случаях вы можете создать ЛОЖНЫЙ «список» элементов в базе данных, например, у товара есть несколько изображений, чтобы показать его детали, вы можете объединить все идентификаторы изображений, разделенные запятой, и сохранить строку в БД, тогда вам просто нужно проанализировать строку, когда она вам понадобится. Сейчас я работаю над веб-сайтом и планирую использовать его.

Nen
источник
0

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

Я тщательно протестировал его, и в моем конкретном случае он был намного эффективнее, чем использование типа массива (щедро предлагаемого PostgreSQL) или запрос другой таблицы.

Вот мой ответ: я успешно реализовал список в одном поле в PostgreSQL, используя фиксированную длину каждого элемента списка. Допустим, каждый элемент имеет цвет в шестнадцатеричном формате ARGB, это означает 8 символов. Таким образом, вы можете создать свой массив из 10 элементов, умножив его на длину каждого элемента:

ALTER product ADD color varchar(80)

Если длина элементов вашего списка различается, вы всегда можете заполнить отступ с помощью \ 0

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

Причина: 1 / Очень удобно: получить элемент i по подстроке i * n, (i +1) * n. 2 / Отсутствие накладных расходов на запросы к перекрестным таблицам. 3 / Более эффективный и экономичный на стороне сервера. Список похож на мини-blob, который клиенту придется разделить.

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

«Не дай Бог, что это нарушает какой-то священный священный принцип SQL»: всегда лучше принять более непредубежденный и прагматичный подход перед тем, как декламировать правила. В противном случае вы могли бы закончить как откровенный фанатик, декламирующий Три закона робототехники, прежде чем вас уничтожит Скайнет.

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

Антонин ГАВРЕЛЬ
источник
Но это очень специфический случай: фиксированное количество элементов фиксированной длины. Даже в этом случае простой поиск типа «все продукты, имеющие цвет не ниже x» сложнее, чем в стандартном SQL.
Герт Арнольд
Как я уже неоднократно заявлял, я не использую его для цвета, поле, в котором я его использую, не должно индексироваться или использоваться в качестве условия, и все же это критическое значение,
Антонин ГАВРЕЛЬ
Я знаю, я пытаюсь указать, что это очень специфично. Если возникает какое-либо небольшое дополнительное требование, оно быстро становится более неудобным, чем стандартные решения. Подавляющему большинству людей, у которых возникает соблазн хранить списки в одном поле db, вероятно, лучше этого не делать.
Герт Арнольд
0

Многие базы данных SQL позволяют таблице содержать подтаблицу в качестве компонента. Обычный метод - разрешить домену одного из столбцов быть таблицей. Это в дополнение к использованию некоторого соглашения, такого как CSV, для кодирования подструктуры способами, неизвестными СУБД.

Когда Эд Кодд разрабатывал реляционную модель в 1969-1970 годах, он специально определил нормальную форму , которая запрещала бы такое вложение таблиц. Нормальная форма позже была названа Первой нормальной формой. Затем он показал, что для каждой базы данных существует база данных в первой нормальной форме, которая выражает одну и ту же информацию.

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

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

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

Уолтер Митти
источник
-1

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

база данных:

 _____________________
|  word  | letters    |
|   me   | '[m, e]'   |
|  you   |'[y, o, u]' |  note that the letters column is of type 'TEXT'
|  for   |'[f, o, r]' |
|___in___|_'[i, n]'___|

И функция компилятора списка (написана на python, но должна легко переводиться на большинство других языков программирования). ТЕКСТ представляет текст, загруженный из таблицы sql. возвращает список строк из строки, содержащей список. если вы хотите, чтобы он возвращал целые числа вместо строк, сделайте режим равным 'int'. То же самое с 'string', 'bool' или 'float'.

def string_to_list(string, mode):
    items = []
    item = ""
    itemExpected = True
    for char in string[1:]:
        if itemExpected and char not in [']', ',', '[']:
            item += char
        elif char in [',', '[', ']']:
            itemExpected = True
            items.append(item)
            item = ""
    newItems = []
    if mode == "int":
        for i in items:
            newItems.append(int(i))

    elif mode == "float":
        for i in items:
            newItems.append(float(i))

    elif mode == "boolean":
        for i in items:
            if i in ["true", "True"]:
                newItems.append(True)
            elif i in ["false", "False"]:
                newItems.append(False)
            else:
                newItems.append(None)
    elif mode == "string":
        return items
    else:
        raise Exception("the 'mode'/second parameter of string_to_list() must be one of: 'int', 'string', 'bool', or 'float'")
    return newItems

Также здесь есть функция преобразования списка в строку на случай, если она вам понадобится.

def list_to_string(lst):
    string = "["
    for i in lst:
        string += str(i) + ","
    if string[-1] == ',':
        string = string[:-1] + "]"
    else:
        string += "]"
    return string
человек человек
источник