SQLite 为开发人员提供了自由,让他们可以以任何所需的格式存储内容,而不管列的声明数据类型是什么。有些人觉得这个功能很麻烦。一些开发人员惊讶地发现,可以将文本插入到标记为 INTEGER 的列中。
本文主张支持 SQLite 中的灵活类型规则。
有关 SQLite 灵活类型系统的详细信息,请参阅单独的 SQLite 中的数据类型 文档。以下是简要总结
列定义上的数据类型名称是可选的。列定义可以只包含列名,不包含其他内容。
当提供数据类型名称时,它们可以是任何文本。SQLite 尝试根据列定义中的数据类型名称推断列的首选数据类型,但该首选数据类型是建议性的,而不是强制性的。首选数据类型称为“列亲和性”。
尝试将传入的数据转换为列的首选数据类型。(所有 SQL 数据库引擎都这样做,而不仅仅是 SQLite。)如果此转换成功,一切正常。但如果失败,SQLite 不会引发错误,而是使用其原始数据类型存储内容。
以上可能会导致严格类型支持者认为不方便的情况
列数据类型 | 该列允许的类型 |
---|---|
INTEGER | INTEGER、REAL、TEXT、BLOB |
REAL | REAL、TEXT、BLOB |
TEXT | TEXT、BLOB |
BLOB | INTEGER、REAL、TEXT、BLOB |
请注意,INTEGER 或 REAL 值永远不会存储在 TEXT 列中,因为 INTEGER 或 REAL 值始终可以转换为其等效的 TEXT 表示形式。类似地,INTEGER 永远不会存储在 REAL 列中,因为它始终会被转换为 REAL。但 TEXT 并不总是像 INTEGER 或 REAL 值,因此无法始终转换。BLOB 无法转换为任何内容,其他任何内容也无法转换为 BLOB。
一些读者在第一次遇到 SQLite 中的灵活类型系统时,会问自己“这怎么可能有用?” 这里尝试回答这个问题
许多应用程序,特别是那些使用 SQLite 作为 应用程序文件格式 的应用程序,需要一个地方来存储各种属性,例如缩略图图像(作为 BLOB 值)、简短的文本片段(例如用户的姓名),以及数字、日期和 JSON 值。创建一个单表来处理此存储非常方便
CREATE TABLE attribute(name TEXT PRIMARY KEY, value) WITHOUT ROWID;
如果没有灵活类型系统,这样的表将需要更复杂,为每种可能的数据类型设置单独的列。 “value” 列的灵活类型使表在概念上更简单、更节省空间,并且更易于访问和更新。
在 Fossil 版本控制系统 中,每个存储库都有一个 CONFIG 表,用于存储各种设置以及所有可能的数据类型。Fossil 的用户特定配置文件(~/.fossil 文件)是一个单独的 SQLite 数据库,其中包含一个单一的属性表,用于保存所有存储库中的用户特定状态。
一些应用程序使用 SQLite 数据库作为纯键值存储。数据库模式包含一个类似于以下内容的单表
CREATE TABLE storage(name TEXT PRIMARY KEY, value ANYTHING);
SQLite 中内置的 json_tree 和 json_each 表值函数都具有一个 “value” 列,该列可以根据相应 JSON 字段的类型保存 INTEGER、REAL 或 TEXT 类型的值。例如
SELECT typeof(value) FROM json_each('{"a":1,"b":2.5,"c":"hello"}');
上面的查询返回三行一列,值分别为“integer”、“real”和“text”。
分析师有时会遇到 CSV 文件,其中某些列包含整数、实数和文本数据的混合。例如,从 Excel 电子表格导出获得的 CSV 文件通常具有此特征。将此类“脏数据”导入 SQL 数据库时,使用灵活类型的列进行导入很方便。
当然,脏数据不仅限于来自 Excel 的 CSV 文件。在许多数据源中,单个字段可能包含多种类型。例如,数据列可能有时包含自 1970 年以来的秒数,或者在其他情况下包含文本日期字符串。需要清理这些不一致的表示形式,但同时能够在清理过程中将所有不同的表示形式存储在中间数据库的同一列中也很方便。
SQLite 最初是作为 TCL 扩展而开发的,后来扩展到了更广泛的领域。TCL 是一种动态语言,因为程序员不需要了解数据类型。在幕后,TCL 仔细跟踪每个值的类型,但对于 TCL 程序的开发人员和用户而言,所有内容都看起来像字符串。灵活类型系统非常适合与 TCL 等动态编程语言一起使用,因为使用动态编程语言,您无法始终提前预测变量将保存何种数据类型。因此,当您需要将该变量的值存储到数据库中时,拥有支持灵活类型系统的数据库会使存储变得更加容易。
每个 SQL 数据库引擎似乎都有自己独特的一组支持的数据类型名称
事实上,SQLite 会接受所有这些名称作为有效类型名称,并允许您将任何类型的内容存储到列中,这增加了为在其他 SQL 数据库引擎上运行而编写的脚本在 SQLite 中也能工作的可能性。
因为 SQLite 数据库文件是磁盘上的单个文件,所以一些应用程序使用 SQLite 作为 应用程序文件格式。这意味着应用程序的单个实例在其生命周期内可能会与数百或数千个单独的数据库进行通信,每个数据库都在一个单独的文件中。当此类应用程序经过多年的发展时,基础数据库中的一些列的含义会发生细微的改变。或者,可能需要重用现有列来服务于两个或多个目的。如果列具有灵活的数据类型,则这样做要容易得多。
以下关于灵活类型系统的感知缺点是从 Hacker News、Reddit 和类似论坛上无数的帖子中收集和整理出来的,在这些论坛中,开发人员会讨论这些类型的事情。如果您能想到其他关于为什么灵活类型系统是个坏主意的理由,请与 SQLite 开发人员联系或在 SQLite 论坛 上发帖,以便将您的想法添加到列表中。
许多灵活类型系统的怀疑论者只是表达震惊和怀疑,而没有提供任何理由来解释为什么他们认为灵活类型系统是个坏主意。如果没有支持性论据,则必须假设他们不喜欢灵活类型系统的原因是它与他们习惯的不同。
据推测,许多对 SQLite 的灵活类型系统感到震惊的开发人员之所以这样认为,是因为他们以前从未遇到过类似的东西。所有之前接触的数据库,尤其是 SQL 数据库,都涉及严格类型系统,并且读者对 SQL 的心理模型将严格类型系统视为一项基本功能。灵活类型系统颠覆了他们的世界观。
是的,灵活类型系统是一种关于 SQL 数据库中数据的新思维方式。但新并不一定不好。有时,我认为尤其是在灵活类型系统的情况下,创新会导致改进。
在许多程序员中,已经形成了一种教条,即防止应用程序错误的最佳方法是严格类型强制。但我没有发现任何支持这一点的证据。
可以肯定的是,严格类型强制确实有助于防止某些类型的错误,例如在 C 和 C++ 等低级语言中,这些语言提供的模型接近于机器硬件。但对于抽象级别更高的语言,情况似乎并非如此,在这些语言中,所有数据都以某种“Value”超类的形式传递,该超类是针对各种低级数据类型的子类。当所有内容都是 Value 对象时,特定数据类型就不再重要了。
此技术说明由 SQLite 的原始作者撰写。我已经编写了 27 年的 TCL 程序。TCL 完全没有类型强制。TCL 中的“Value”类(称为 Tcl_Obj)可以保存许多不同的数据类型,但它以字符串的形式向程序和应用程序用户呈现内容。多年来,我的这些 TCL 程序中出现了很多错误。但我记不起有任何一个错误可能是由严格类型系统捕获的。我还编写了大量的 C 代码,时间跨度为 35 年,其中最不重要的是 SQLite 本身。我发现 C 中的类型系统在查找和防止问题方面非常有帮助。对于用 C 编写的 Fossil 版本控制系统,我甚至实现了补充的静态分析程序,这些程序在编译 Fossil 源代码之前扫描它,查找编译器遗漏的问题。这对编译程序很有用。
SQL 语言模型比 C/C++ 抽象级别更高。在 SQLite 中,每个数据项都以“sqlite3_value”对象的格式存储在内存中。此对象有字符串、整数、浮点数、blob 和其他表示形式的子类。所有内容都在 SQLite 实现的 SQL 语言内部作为“sqlite3_value”对象传递,因此底层数据类型实际上并不重要。我从未发现严格类型强制在像 TCL 和 SQLite 这样的语言中有所帮助,这些语言具有用于表示任何数据元素的单个“Value”超类。Fossil 在其实现中广泛使用了 SQLite。在 Fossil 14 年的历史中,出现了许多错误,但我记不起有任何错误可能是由 SQLite 中的严格类型强制阻止的。某些 C 语言错误可能可以通过更好的类型强制来捕获(这就是我编写补充源代码扫描程序的原因),但没有 SQL 错误。
基于几十年的经验,我拒绝了严格类型强制有助于防止应用程序错误的论点。我将接受并相信一个略微修改过的论点:严格类型强制有助于防止在缺少单个顶级“Value”超类的语言中发生的应用程序错误。但 SQLite 确实具有单个“sqlite3_value”超类,因此该谚语不适用。
有些人认为,如果对模式施加严格的约束,特别是严格执行列数据类型,这将有助于防止不正确的数据添加到数据库中。这是不正确的。类型强制确实可能有助于防止极其不正确的数据进入系统。但类型强制无法防止记录细微不正确的数据。
例如,严格的类型强制可以成功地防止将客户姓名(文本)插入到整数 Customer.creditScore 列中。另一方面,如果发生此错误,则很容易发现问题并找到所有受影响的行。但是,如果客户的姓氏和名字颠倒了,类型强制也无济于事,因为两者都是文本字段。
通过抑制易于检测的错误并只传递难以检测的错误,严格的类型强制实际上可能使查找和修复错误变得更加困难。数据错误往往会聚集。如果您有 20 个不同的数据源,大多数数据错误通常只会来自其中的 2 或 3 个源。严重错误的存在(例如整数列中的文本)是一个方便的早期预警信号,表明某些地方出了问题。可以快速跟踪问题的来源并对严重错误的来源进行额外审查,从而有望修复细微的错误。当抑制严重错误时,您会失去一个帮助您检测和修复细微错误的重要信号。
数据错误是不可避免的。无论进行多少类型检查,它们都会发生。严格的类型强制只能捕获一小部分情况——最明显的情况。它无助于查找和修复更细微的情况。而且,通过抑制哪些数据源存在问题,它有时会使细微错误更难定位。
因为 SQLite 限制较少并且允许您做更多事情,所以在其他数据库引擎上工作的 SQL 脚本通常也会在 SQLite 上工作,但最初为 SQLite 编写的脚本可能在更严格的数据库引擎上无法工作。当开发人员使用 SQLite 进行原型设计和测试,然后将其应用程序迁移到更严格的 SQL 引擎进行部署时,这可能会导致问题。如果应用程序(无意中)利用了 SQLite 中提供的灵活类型,那么它在迁移后将失败。
人们利用这个问题来争论 SQLite 应该对数据类型更严格。但是,您也可以轻松地将这个论点反过来,说其他数据库引擎应该对数据类型更灵活。毕竟,应用程序在迁移之前在 SQLite 下运行良好。如果严格的类型强制真的那么有用,为什么它会破坏以前运行良好的应用程序?
从 SQLite 3.37.0 版(2021-11-27)开始,SQLite 使用STRICT 表支持这种开发风格。
如果您发现了一个现实世界的案例,其中 STRICT 表阻止了或本可以阻止应用程序中的错误,请在SQLite 论坛上发布消息,以便我们可以在本文档中添加您的故事。
如果 SQL 数据库中的灵活类型对您来说是一个新概念,我鼓励您尝试一下。它可能不会给您带来任何问题,并且它可能会使您的程序更简单、更容易编写和维护。我认为,即使您一开始持怀疑态度,只要您尝试使用灵活类型,您最终都会意识到这是一种更好的方法,并开始鼓励其他数据库供应商至少支持 ANY 数据类型,如果不是完全的 SQLite 风格的类型灵活性的话。
大多数情况下,灵活类型无关紧要,因为列存储单个定义良好的类型。但偶尔您会遇到一些情况,在这些情况下,拥有灵活的类型系统可以使问题的解决方案更简洁、更容易。
此页面上次修改于 2024-07-14 22:39:43 UTC