【问题标题】:Is storing a delimited list in a database column really that bad?在数据库列中存储分隔列表真的那么糟糕吗?
【发布时间】:2025-12-01 05:45:01
【问题描述】:

想象一个带有一组复选框的 Web 表单(可以选择其中的任何一个或全部)。我选择将它们保存在以逗号分隔的值列表中,这些值存储在数据库表的一列中。

现在,我知道正确的解决方案是创建第二个表并正确规范化数据库。实施简单的解决方案更快,我希望快速获得该应用程序的概念验证,而不必花费太多时间。

我认为在我的情况下节省时间和更简单的代码是值得的,这是一个合理的设计选择,还是我应该从一开始就对其进行规范化?

更多上下文,这是一个小型内部应用程序,它实质上替换了存储在共享文件夹中的 Excel 文件。我也在问,因为我正在考虑清理程序并使其更易于维护。里面有些东西我不太满意,其中之一就是这个问题的主题。

【问题讨论】:

  • 在这种情况下,为什么要打扰数据库?,保存在文件中就可以了。
  • 同意@thavan。为什么还要保存数据以进行概念验证?完成证明后,请正确添加数据库。做轻量级的概念证明你很好,只是不要做你以后必须取消的东西。
  • 在 Postgres 中,数组列应该优先于逗号分隔的列表。这至少确保了正确的数据类型,在区分分隔符和实际数据方面没有问题,并且可以有效地对其进行索引。

标签: database database-design database-normalization


【解决方案1】:

逗号分隔的列表除了违反First Normal Form因为单列存储重复组值之外,还有很多其他更实际的问题:

  • 无法确保每个值都是正确的数据类型:没有办法防止1,2,3,banana,5
  • 无法使用外键约束将值链接到查找表;无法强制执行参照完整性。
  • 无法强制唯一性:无法阻止 1,2,3,3,3,5
  • 无法在不获取整个列表的情况下从列表中删除值。
  • 存储的列表长度不能超过字符串列的长度。
  • 很难在列表中搜索所有具有给定值的实体;您必须使用低效的表扫描。可能不得不求助于正则表达式,例如在 MySQL 中:
    idlist REGEXP '[[:<:]]2[[:>:]]' 或在 MySQL 8.0 中:idlist REGEXP '\\b2\\b'
  • 很难计算列表中的元素,或进行其他聚合查询。
  • 很难将这些值加入到它们引用的查找表中。
  • 很难按排序顺序获取列表。
  • 很难选择保证不会出现在值中的分隔符

要解决这些问题,您必须编写大量应用程序代码,重新发明 RDBMS 已经提供的更有效的功能

逗号分隔的列表是错误的,我把它作为我书中的第一章:SQL Antipatterns: Avoiding the Pitfalls of Database Programming

有时您需要使用非规范化,但作为@OMG Ponies mentions,这些是例外情况。任何非关系“优化”都会以牺牲数据的其他用途为代价来使一种类型的查询受益,因此请确保您知道哪些查询需要特别处理,以使其值得去规范化。

【讨论】:

  • 一个数组(任何数据类型)可以修复异常,只需检查 PostgreSQL:postgresql.org/docs/current/static/arrays.html(@Bill:好书,任何开发人员或 dba 都必须阅读)
  • 有关 PostgreSQL 特定的讨论,请参阅 dba.stackexchange.com/q/55871/7788 。逗号分隔同样糟糕,但如果仔细应用并考虑后果,数组字段在某些情况下可能是可接受的性能优化。
  • @CraigRinger,是的,这是一种非规范化。如果谨慎使用,非规范化对于您尝试优化的某个查询可能是正确的做法,但必须在充分了解它会损害其他查询的情况下完成。如果这些其他查询对您的应用程序不重要,那么痛苦就会减少。
  • 我知道它不推荐,但扮演魔鬼的倡导者:如果有一个处理唯一性和数据类型的 ui(否则会出错或行为不端),大部分这些都可以删除,ui 丢弃并创建它无论如何,有一个驱动表,其中的值来自使它们唯一,可以使用像'%P%'这样的字段,值是P,R,S,T,计数无关紧要,排序无关紧要.根据 ui,值可以拆分 [] 例如在最不常见的情况下从驱动程序表中检查列表中的复选框,而无需转到另一个表来获取它们。
  • @PrabhuNandanKumar,我会将 174 rows 存储在第二个表中,该表引用您的第一个表。不要存储具有相似数据的 174 列。
【解决方案2】:

“一个原因是懒惰”。

这敲响了警钟。您应该这样做的唯一原因是您知道如何以“正确的方式”执行此操作,但您得出的结论是有一个切实的理由不这样做。

话虽如此:如果您选择以这种方式存储的数据是您永远不需要查询的数据,那么可能会以您选择的方式存储它。

(有些用户会质疑我在上一段中的说法,说“你永远不知道将来会添加什么要求”。这些用户要么被误导,要么陈述了宗教信仰。有时努力工作是有利的你面前的要求。)

【讨论】:

  • 我总是听到有人说“我的设计比你的更灵活”,当我面对诸如不设置外键约束或将列表存储在单个字段中等问题时。对我来说,灵活性(在这种情况下)== 没有纪律== 懒惰。
【解决方案3】:

关于 SO 的问题有很多:

  • 如何从逗号分隔列表中获取特定值的计数
  • 如何从逗号分隔列表中获取仅具有相同 2/3/etc 特定值的记录

逗号分隔列表的另一个问题是确保值一致 - 存储文本意味着可能出现拼写错误...

这些都是非规范化数据的症状,并强调了为什么您应该始终为规范化数据建模。非规范化可以是一种查询优化,在实际需要时应用

【讨论】:

    【解决方案4】:

    一般来说,只要满足您项目的要求,任何东西都可以防御。这并不意味着人们会同意或想要捍卫您的决定……

    一般来说,以这种方式存储数据不是最理想的(例如,更难进行有效的查询),如果您修改表单中的项目,可能会导致维护问题。或许您可以找到一个中间立场并使用一个表示一组位标志的整数来代替?

    【讨论】:

      【解决方案5】:

      是的,它那么糟糕。我的观点是,如果您不喜欢使用关系数据库,那么请寻找更适合您的替代方案,那里有很多有趣的“NOSQL”项目,它们具有一些非常高级的功能。

      【讨论】:

        【解决方案6】:

        我需要一个多值列,它可以实现为 xml 字段

        必要时可以转换成逗号分隔

        querying an XML list in sql server using Xquery.

        作为一个 xml 字段,可以解决一些问题。

        使用 CSV: 无法确保每个值都是正确的数据类型:没有办法防止 1,2,3,banana,5

        使用 XML: 标签中的值可以强制为正确的类型


        使用 CSV: 不能使用外键约束将值链接到查找表;没有办法强制引用完整性。

        使用 XML: 仍然是个问题


        使用 CSV: 无法强制唯一性:无法阻止 1,2,3,3,3,5

        使用 XML: 仍然是个问题


        使用 CSV:如果不获取整个列表,则无法从列表中删除值。

        使用 XML:可以删除单个项目


        使用 CSV: 难以搜索列表中具有给定值的所有实体;您必须使用低效的表扫描。

        使用 XML: xml 字段可以被索引


        使用 CSV:很难计算列表中的元素,或进行其他聚合查询。**

        使用 XML: 不是特别难


        使用 CSV:很难将值连接到它们引用的查找表中。**

        使用 XML: 不是特别难


        使用 CSV:很难按排序顺序获取列表。

        使用 XML: 不是特别难


        使用 CSV: 将整数存储为字符串占用的空间大约是存储二进制整数的两倍。

        使用 XML: 存储比 csv 更糟糕


        使用 CSV: 加上很多逗号字符。

        使用 XML: 使用标签代替逗号


        简而言之,使用 XML 解决了分隔列表的一些问题,并且可以根据需要转换为分隔列表

        【讨论】:

          【解决方案7】:

          是的,我会说这真的很糟糕。这是一个合理的选择,但这并不意味着它是正确的或好的。

          它打破了第一范式。

          第二个批评是,将原始输入结果直接放入数据库,根本没有任何验证或绑定,这会使您容易受到 SQL 注入攻击。

          你所说的懒惰和缺乏 SQL 知识是新手们的组成部分。我建议您花时间正确地做这件事,并将其视为学习的机会。

          或者保持现状,吸取 SQL 注入攻击的惨痛教训。

          【讨论】:

          • 我在这个问题中没有看到任何表明他容易受到 SQL 注入攻击的内容。 SQL注入和数据库规范化是正交的话题,你对注入的题外话与问题无关。
          • 输入被转义,任何有权访问此应用程序的人都已经有了更简单的方法来造成严重破坏。我使用 Drupal db_query 来访问数据库,单独提供参数。
          • @Hammerite,即使这种特殊的懒惰和不愿学习不会导致 SQL 注入,其他相同态度的例子也会。
          • @Hammerite,也没有什么可以排除这种可能性。我认为值得提出以防 OP 的无知也扩展到 SQL 注入。我同意规范化和 SQL 注入可以是正交的,但是在没有其他信息的情况下,我觉得应该提到它。这几乎无关紧要。
          • @Paul:也许同样的态度会导致他在过马路前没有向两边看时被公共汽车撞到,但你没有警告过他。编辑:我以为你是这个答案的发布者,我的错误。
          【解决方案8】:

          嗯,我已经在 SQL Server 的 NTEXT 列中使用键/值对制表符分隔列表超过 4 年了,现在它可以工作了。您确实失去了进行查询的灵活性,但另一方面,如果您有一个持久化/去持久化键值对的库,那么这不是一个坏主意。

          【讨论】:

          • 不,这是一个可怕的想法。您已经设法摆脱了它,但是几分钟的开发时间成本已经使您的代码查询性能、灵活性和可维护性变得糟糕。
          • 保罗,我同意。但正如我所说,我将 if 用于特定目的,即用于具有多种表单的数据输入操作。现在我正在修改设计,因为我已经学习了 NHibernate,但当时我需要灵活性来在 ASP.NET 中设计表单,并使用文本框 ID 作为键/值对中的键。
          • 告诉维护应用程序 4 年的人有关维护问题有点冒昧。软件开发中很少有“可怕”的想法 - 大多数只是适用性非常有限的想法。警告人们注意限制是合理的,但惩罚那些做过并经历过它的人让我觉得这是一种比你更神圣的态度,我可以不用。
          【解决方案9】:

          我可能会采取中间立场:将 CSV 中的每个字段都放在数据库中的单独列中,但不必太担心规范化(至少目前如此)。在某些时候,规范化可能会变得有趣,但是由于所有数据都被塞到一个列中,您几乎没有从使用数据库中获得任何好处。您需要将数据分成逻辑字段/列/您想要调用的任何内容,然后才能进行有意义的操作。

          【讨论】:

            【解决方案10】:

            如果您有固定数量的布尔字段,您可以为每个字段使用INT(1) NOT NULL(或BIT NOT NULL,如果存在)或CHAR (0)(可为空)。你也可以使用SET(我忘记了确切的语法)。

            【讨论】:

            • INT(1) 占用 4 个字节; (1) 毫无意义。
            • INT(1) 占用多少字节取决于产品,以及 INT(1) 的含义。可以是一个数字、一个字节、一个单词还是其他什么?