因此,根据 Mehrdad's answer to a related question,我明白“正确的”数据库表列不存储列表。相反,您应该创建另一个有效保存所述列表元素的表,然后直接或通过联结表链接到它。但是,我要创建的列表类型将由独特的项目组成(与链接问题的 fruit 示例不同)。此外,我的列表中的项目是明确排序的——这意味着如果我将元素存储在另一个表中,我每次访问它们时都必须对它们进行排序。最后,列表基本上是原子的,因为任何时候我想访问列表,我都想访问整个列表,而不仅仅是其中的一部分——所以不得不发出数据库查询来收集碎片似乎很愚蠢名单。
AKX 的解决方案(上面链接)是将列表序列化并将其存储在二进制列中。但这似乎也很不方便,因为这意味着我必须担心序列化和反序列化。
有没有更好的解决方案?如果没有更好的解决方案,那为什么呢?看来这个问题应该时不时出现。
......只是更多的信息让你知道我来自哪里。当我刚开始了解 SQL 和一般数据库时,我就开始使用 LINQ to SQL,所以现在我有点被宠坏了,因为我希望处理我的编程对象模型而不必考虑对象是如何处理的被查询或存储在数据库中。
谢谢大家!
约翰
更新:所以在我得到的第一批答案中,我看到“你可以走 CSV/XML 路线……但不要!”。所以现在我正在寻找原因的解释。给我一些好的参考。
另外,为了让您更好地了解我在做什么:在我的数据库中,我有一个函数表,其中包含 (x,y) 对的列表。 (该表还将包含其他对我们的讨论无关紧要的信息。)我永远不需要查看 (x,y) 对列表的一部分。相反,我会将它们全部绘制在屏幕上。我将允许用户拖动节点以偶尔更改值或向绘图添加更多值。
不,没有“更好”的方式将一系列项目存储在单个列中。关系数据库专门设计用于为每个行/列组合存储一个值。为了存储多个值,您必须将列表序列化为单个值进行存储,然后在检索时对其进行反序列化。没有其他方法可以做你正在谈论的事情(因为你正在谈论的是一个坏主意,一般来说,永远不要这样做)。
我知道您认为创建另一个表来存储该列表很愚蠢,但这正是关系数据库所做的。您正在打一场艰苦的战斗,并且无缘无故地违反了关系数据库设计的最基本原则之一。既然您说您只是在学习 SQL,我强烈建议您避免这种想法,并坚持使用更有经验的 SQL 开发人员向您推荐的做法。
您违反的原则称为第一范式,这是数据库规范化的第一步。
冒着过度简化事情的风险,数据库规范化是根据数据是来定义数据库的过程,这样您就可以针对它编写合理、一致的查询并能够轻松地对其进行维护。规范化旨在限制数据中的逻辑不一致和损坏,并且它有很多级别。 database normalization 上的 Wikipedia 文章实际上非常好。
基本上,规范化的第一条规则(或形式)规定您的表必须表示关系。这意味着:
您必须能够将一行与任何其他行区分开来(换句话说,您的表必须具有可以用作主键的内容。这也意味着不应重复任何行。
数据的任何顺序都必须由数据定义,而不是由行的物理顺序定义(SQL 基于集合的概念,这意味着您应该依赖的唯一顺序是您在查询中明确定义的顺序)
每个行/列交叉点必须包含一个且只有一个值
最后一点显然是这里的重点。 SQL 旨在为您存储集合,而不是为您提供一个“桶”让您自己存储集合。是的,有可能做到。不,世界不会终结。但是,您已经通过立即开始使用 ORM,从而削弱了自己对 SQL 和与之相伴的最佳实践的理解。 LINQ to SQL 非常棒,就像图形计算器一样。然而,同样地,它们不应该被用来代替了解它们所采用的过程实际上是如何工作的。
您的列表现在可能完全是“原子的”,并且对于这个项目可能不会改变。但是,您会养成在其他项目中做类似事情的习惯,并且您最终(可能很快)会遇到一个场景,您现在正在适应您的快速-n-easy list-in-a-column在完全不合适的情况下进行处理。为您尝试存储的内容创建正确的表并没有太多额外的工作,并且当其他 SQL 开发人员看到您的数据库设计时,他们不会嘲笑您。此外,LINQ to SQL 将查看您的关系并自动为您提供适当的面向对象接口到您的列表。你为什么要放弃 ORM 为你提供的便利,以便你可以执行非标准和不明智的数据库黑客攻击?
我见过很多人这样做(这可能不是最好的方法,如果我错了,请纠正我):
下面给出了我在示例中使用的表(该表包括您给特定女朋友的昵称。每个女朋友都有一个唯一的 id):
nicknames(id,seq_no,names)
假设,您想在一个 id 下存储许多昵称。这就是我们包含 seq_no
字段的原因。
现在,将这些值填充到您的表中:
(1,1,'sweetheart'), (1,2,'pumpkin'), (2,1,'cutie'), (2,2,'cherry pie')
如果你想找到你给你女朋友 id 1 的所有名字,那么你可以使用:
select names from nicknames where id = 1;
简单的回答:当且仅当您确定该列表将始终用作列表时,然后在您的最后将列表与一个不会在文本,并存储它。然后当你检索它时,你可以用'\0'分割。当然还有其他方法可以解决这些问题,但这些取决于您的特定数据库供应商。
例如,您可以将 JSON 存储在 Postgres 数据库中。如果您的列表是文本,并且您只想要该列表而不需要进一步的麻烦,那么这是一个合理的折衷方案。
其他人大胆地提出了序列化的建议,但我并不认为序列化是一个好主意:关于数据库的巧妙之处在于,用不同语言编写的多个程序可以相互通信。如果 Lisp 程序想要加载它,那么使用 Java 格式序列化的程序就不会做得那么好。
如果您想要一种做这种事情的好方法,通常有可用的数组或类似类型。 Postgres for instance, offers array as a type, and lets you store an array of text, if that's what you want,对于使用 JSON 的 MySql 和 MS SQL 也有类似的技巧,并且 IBM's DB2 也提供了一个数组类型(在他们自己的 helpful 文档中)。如果不需要的话,这不会那么普遍。
走这条路,你确实失去了将列表视为一堆按顺序排列的东西的概念。至少名义上,数据库将字段视为单个值。但如果这就是你想要的,那么你应该去做。这是你必须为自己做出的价值判断。
除了其他人所说的之外,我建议您比现在更长时间地分析您的方法。目前的情况是物品是独一无二的。目前的情况是,重新使用这些项目需要一个新列表。几乎要求该列表当前很短。尽管我没有具体的领域,但认为这些要求可能会改变并不是一件容易的事。如果你序列化你的列表,你就会陷入一种僵化的状态,而这在更规范化的设计中是不必要的。顺便说一句,这并不一定意味着完整的多:多关系。您可以只有一个子表,其中包含一个指向父项的外键和一个用于项目的字符列。
如果您仍想走这条序列化列表的道路,您可以考虑将列表存储在 XML 中。某些数据库(例如 SQL Server)甚至具有 XML 数据类型。我建议 XML 的唯一原因是几乎按照定义,这个列表需要很短。如果列表很长,那么通常将其序列化是一种糟糕的方法。如果您采用 CSV 路线,则需要考虑包含分隔符的值,这意味着您必须使用带引号的标识符。假设列表很短,那么无论您使用 CSV 还是 XML,都可能没有太大区别。
如果需要对列表进行查询,则将其存储在表中。
如果您总是想要该列表,则可以将其作为分隔列表存储在列中。即使在这种情况下,除非您有非常具体的理由不这样做,否则将其存储在查找表中。
许多 SQL 数据库允许表包含子表作为组件。通常的方法是允许其中一列的域是一个表。这是除了使用诸如 CSV 之类的约定以 DBMS 未知的方式对子结构进行编码之外。
当 Ed Codd 在 1969-1970 年开发关系模型时,他专门定义了一个不允许这种表格嵌套的范式。范式后来被称为第一范式。然后他继续表明,对于每个数据库,都有一个第一范式的数据库表达相同的信息。
为什么要为此烦恼?嗯,第一范式的数据库允许对所有数据进行键控访问。如果您提供表名、该表的键值和列名,则数据库将最多包含一个包含一项数据的单元格。
如果您允许单元格包含列表或表格或任何其他集合,则现在您无法提供对子项的键控访问,除非完全重新设计键的概念。
对所有数据的键控访问是关系模型的基础。没有这个概念,模型就不是关系型的。至于为什么关系模型是一个好主意,以及这个好主意的局限性是什么,你必须看看 50 年积累的关系模型经验。
我只是将它存储为 CSV,如果它是简单的值,那么它应该是你所需要的(XML 非常冗长,并且序列化到/从它可能会过大,但这也是一个选项)。
这是关于如何使用 LINQ 提取 CSV 的 good answer。
答案中只有一个选项没有提到。您可以反规范化您的数据库设计。所以你需要两张桌子。一个表包含正确的列表,每行一个项目,另一个表在一列中包含整个列表(例如,逗号分隔)。
这是“传统”数据库设计:
List(ListID, ListName)
Item(ItemID,ItemName)
List_Item(ListID, ItemID, SortOrder)
这是非规范化表:
Lists(ListID, ListContent)
这里的想法 - 您使用触发器或应用程序代码维护 Lists 表。每次修改 List_Item 内容时,列表中的相应行都会自动更新。如果您主要阅读列表,它可以工作得很好。优点 - 您可以在一份声明中阅读列表。缺点 - 更新需要更多时间和精力。
因为很多答案,我非常不愿意选择我最终决定走的路。虽然他们增加了对什么是 SQL 及其原理的理解,但我决定成为一名亡命之徒。我也不愿发表我的发现,因为对于某些人来说,向违反规则的人发泄挫败感比理解普遍真理很少。
我已经对其进行了广泛的测试,在我的具体情况下,它比使用数组类型(由 PostgreSQL 慷慨提供)或查询另一个表更有效。
这是我的答案:通过利用列表中每个项目的固定长度,我已经成功地将列表实现到 PostgreSQL 中的单个字段中。假设每个项目都是 ARGB 十六进制值的颜色,这意味着 8 个字符。因此,您可以通过乘以每个项目的长度来创建最多 10 个项目的数组:
ALTER product ADD color varchar(80)
如果您的列表项长度不同,您始终可以使用 \0 填充填充
注意:显然这不一定是十六进制数的最佳方法,因为整数列表会消耗更少的存储空间,但这只是为了通过使用分配给每个项目的固定长度来说明数组的这种想法。
原因: 1/ 非常方便:在子字符串 i*n, (i +1)*n 处检索项目 i。 2/ 没有跨表查询的开销。 3/ 在服务器端更高效、更节省成本。该列表就像客户端必须拆分的迷你 blob。
虽然我尊重遵守规则的人,但许多解释都是非常理论化的,并且常常无法承认,在某些特定情况下,尤其是在以低延迟解决方案实现成本最优时,一些小的调整是非常受欢迎的。
“上帝保佑它违反了一些神圣的SQL原则”:在背诵规则之前采取更加开放和务实的态度总是要走的路。否则,在被天网抹杀之前,您可能会像一个坦率的狂热者一样背诵 Three Laws of Robotics
我不假装这个解决方案是一个突破,也不假装它在可读性和数据库灵活性方面是理想的,但它肯定可以在延迟方面为您提供优势。
我所做的是,如果需要存储的列表很小,那么我只需将其转换为字符串,然后在需要时将其拆分。 python中的示例-
for y in b:
if text1 == "":
text1 = y
else:
text1 = text1 + f"~{y}"
然后当我需要它时,我只是从数据库中调用它,然后 -
out = query.split('~')
print(out)
这将返回一个列表,并将一个字符串存储在数据库中。但是,如果您要在列表中存储大量数据,那么创建一个表是最好的选择。
如果您真的想将其存储在列中并使其可查询,那么现在很多数据库都支持 XML。如果不查询,您可以将它们存储为逗号分隔值,并在需要分隔它们时使用函数解析它们。我同意其他所有人的观点,但如果您希望使用关系数据库,那么规范化的很大一部分就是这样的数据分离。我并不是说所有数据都适合关系数据库。如果你的很多数据不适合模型,你总是可以查看其他类型的数据库。
我认为在某些情况下,您可以在数据库中创建一个 FAKE 项目“列表”,例如,商品有几张图片来显示其详细信息,您可以将所有图片的 ID 用逗号分隔并将字符串存储到DB,那么您只需要在需要时解析字符串。我现在在一个网站上工作,我打算用这种方式。
您可以将其存储为看起来像列表的文本,并创建一个可以将其数据作为实际列表返回的函数。例子:
数据库:
_____________________
| 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 编写,但它应该很容易翻译成大多数其他编程语言)。 TEXT 表示从 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
想象一下你祖母的一盒食谱,全都写在索引卡上。这些食谱中的每一个都是一份成分列表,这些成分本身就是成对的物品和数量。如果您创建一个配方数据库,您将不需要为配方名称创建一个表和另一个表,其中每种成分都是单独的记录。这听起来就像我们在这里所说的。如果我误读了任何内容,我深表歉意。
来自微软的 T-SQL Fundamentals:
属性的原子性是主观的,就像集合的定义是主观的一样。例如,Employees 关系中的员工姓名应该用一个属性(全名)、两个(名字和姓氏)还是三个(名字、中间名和姓氏)来表示?答案取决于应用程序。如果应用程序需要单独处理员工姓名的各个部分(例如出于搜索目的),将它们分开是有意义的;否则,它不会。
因此,如果您需要通过 SQL 操作坐标列表,则需要将列表的元素拆分为单独的记录。但是您是否只是想存储一个列表并检索它以供其他软件使用,然后将列表存储为单个值更有意义。