小巧。快速。可靠。
三选二。
SQLite 中的怪癖、注意事项和陷阱

1. 概述

SQL 语言是一种“标准”。即便如此,没有两个 SQL 数据库引擎的工作方式完全相同。每个 SQL 实现都有其自身的特点和怪癖,SQLite 也不例外。

本文档力求突出 SQLite 与其他 SQL 实现之间的主要区别,以帮助开发人员将应用程序移植到或从 SQLite 移植,或者构建可在多个数据库引擎上运行的系统。

如果您是 SQLite 用户,并且偶然发现了此处未提及的 SQLite 的某些怪癖,请通过在 SQLite 论坛 上发布简短消息告知开发人员。

2. SQLite 是嵌入式的,而不是客户端-服务器模式的

在将 SQLite 与其他 SQL 数据库引擎(如 SQL Server、PostgreSQL、MySQL 或 Oracle)进行比较时,首先要认识到 SQLite 不是作为任何这些系统的替代品或竞争对手。SQLite 是 无服务器 的。没有单独的服务器进程来管理数据库。应用程序使用函数调用与数据库引擎交互,而不是向单独的进程或线程发送消息。

SQLite 是嵌入式的并且 无服务器 而不是客户端/服务器,这是一个特性,而不是一个错误。

像 MySQL、PostgreSQL、SQL Server、Oracle 等客户端/服务器数据库是现代系统的重要组成部分。这些系统解决了重要的问题。但 SQLite 解决的是不同的问题。SQLite 和客户端/服务器数据库都有其作用。比较 SQLite 与其他 SQL 数据库引擎的开发人员需要清楚地理解这种区别。

请参阅 SQLite 的适用场景 文档以获取更多信息。

3. 灵活的类型

SQLite 在数据类型方面很灵活。数据类型是建议性的,而不是强制性的。

一些评论家说 SQLite 是“弱类型”的,而其他 SQL 数据库是“强类型”的。我们认为这些术语不准确,甚至带有贬义。我们更愿意说 SQLite 是“灵活类型”的,而其他 SQL 数据库引擎是“严格类型”的。

请参阅 SQLite 中的数据类型 文档,详细了解 SQLite 中的类型系统。

关键点是 SQLite 对您放入数据库中的数据类型非常宽容。例如,如果一个列的数据类型为“INTEGER”,并且应用程序将文本字符串插入到该列中,SQLite 将首先尝试将文本字符串转换为整数,就像其他所有 SQL 数据库引擎一样。因此,如果将 '1234' 插入到 INTEGER 列中,该值将转换为整数 1234 并存储。但是,如果将非数字字符串(如 'wxyz')插入到 INTEGER 列中,与其他 SQL 数据库不同,SQLite 不会抛出错误。相反,SQLite 会将实际的字符串值存储在列中。

类似地,SQLite 允许您将 2000 个字符的字符串存储到 VARCHAR(50) 类型的列中。其他 SQL 实现要么会抛出错误,要么会截断字符串。SQLite 会存储整个 2000 个字符的字符串,不会丢失任何信息,也不会发出任何抱怨。

最终导致问题的是,开发人员使用 SQLite 进行一些初始编码工作,使他们的应用程序能够运行,然后尝试转换为另一个数据库(如 PostgreSQL 或 SQL Server)进行部署。如果应用程序最初利用了 SQLite 的灵活类型,那么当它迁移到另一个对数据类型更挑剔的数据库时,就会失败。

灵活类型是 SQLite 的一个特性,而不是一个错误。灵活类型关乎自由。然而,我们认识到,此特性有时会让习惯于使用其他数据库(这些数据库对数据类型规则更严格)的开发人员感到困惑。回想起来,如果 SQLite 只是实现了一个 ANY 数据类型,以便开发人员可以在需要使用灵活类型时明确说明,而不是将灵活类型作为默认设置,也许会不那么令人困惑。为了适应那些期望严格类型的用户,SQLite 3.37.0 版(2021 年 11 月 27 日)引入了 STRICT 表 的选项。这些选项要么强制执行其他 SQL 数据库引擎中的强制数据类型约束,要么允许显式使用 ANY 数据类型以保留 SQLite 的灵活类型。

3.1. 没有单独的 BOOLEAN 数据类型

与大多数其他 SQL 实现不同,SQLite 没有单独的 BOOLEAN 数据类型。相反,TRUE 和 FALSE 通常分别表示为整数 1 和 0。这似乎没有造成很多问题,因为我们很少收到关于它的投诉。但认识到这一点很重要。

从 SQLite 3.23.0 版(2018 年 4 月 2 日)开始,SQLite 还识别 TRUE 和 FALSE 关键字作为整数 1 和 0 的别名。这提供了与其他 SQL 实现更好的兼容性。但为了向后兼容,如果存在名为 TRUE 或 FALSE 的列,则关键字将被视为引用这些列的标识符,而不是 BOOLEAN 字面量。

3.2. 没有单独的 DATETIME 数据类型

SQLite 没有 DATETIME 数据类型。相反,日期和时间可以以以下任何方式存储

SQLite 的内置 日期和时间函数 理解以上所有格式的日期/时间,并且可以在它们之间自由转换。使用哪种格式完全取决于您的应用程序。

3.3. 数据类型是可选的

因为 SQLite 在数据类型方面灵活且宽容,所以可以创建没有指定数据类型的表列。例如

CREATE TABLE t1(a,b,c,d);

表“t1”有四列“a”、“b”、“c”和“d”,没有分配任何特定的数据类型。您可以在任何这些列中存储任何您想要的内容。

4. 外键约束默认情况下未启用

SQLite 从很久以前就开始解析外键约束,但直到很久以后才添加了实际执行这些约束的功能,即 3.6.19 版(2009 年 10 月 14 日)。在添加外键约束执行功能时,已经有数百万个包含外键约束的数据库在流通,其中一些约束是不正确的。为了避免破坏这些遗留数据库,SQLite 中默认情况下会关闭外键约束执行功能。

应用程序可以使用 PRAGMA foreign_keys 语句在运行时激活外键约束执行功能。或者,可以使用 -DSQLITE_DEFAULT_FOREIGN_KEYS=1 编译时选项在编译时激活外键约束执行功能。

5. PRIMARY KEY 有时可以包含 NULL 值

SQLite 表中的 PRIMARY KEY 通常只是一个 UNIQUE 约束。由于历史上的疏忽,允许 PRIMARY KEY 的列值为 NULL。这是一个错误,但当发现此问题时,已经有太多依赖于此错误的数据库在流通,因此决定在未来继续支持这种有问题的行为。您可以通过为 PRIMARY KEY 的每一列添加 NOT NULL 约束来解决此问题。

例外情况

6. 聚合查询可以包含不在 GROUP BY 子句中的非聚合结果列

在大多数 SQL 实现中,聚合查询的输出列只能引用聚合函数或 GROUP BY 子句中命名的列。引用聚合查询中的普通列没有意义,因为每个输出行都可能由输入表中的两个或多个行组成。

SQLite 不强制执行此限制。聚合查询的输出列可以是任意表达式,包括 GROUP BY 子句中未找到的列。此特性有两个用途

  1. 使用 SQLite(但据我们所知,没有其他 SQL 实现),如果聚合查询包含单个 min() 或 max() 函数,则输出列的值将取自实现 min() 或 max() 值的行。如果两个或多个行具有相同的 min() 或 max() 值,则将从这些行中的一个任意选择列值。

    例如,要查找收入最高的员工

    SELECT max(salary), first_name, last_name FROM employee;
    

    在上面的查询中,first_name 和 last_name 列的值将对应于满足 max(salary) 条件的行。

  2. 如果查询根本不包含任何聚合函数,则可以添加 GROUP BY 子句来代替 DISTINCT ON 子句。换句话说,过滤输出行,以便仅显示 GROUP BY 子句中每组唯一值的其中一行。如果两个或多个输出行原本具有 GROUP BY 列的相同值集,则会任意选择其中一行。(SQLite 支持 DISTINCT,但不支持 DISTINCT ON,其功能由 GROUP BY 提供。)

7. SQLite 默认情况下不执行完整的 Unicode 大小写折叠

SQLite 不了解所有 Unicode 字符的大小写区别。像 upper() 和 lower() 这样的 SQL 函数仅适用于 ASCII 字符。有两个原因:

  1. 虽然现在很稳定,但在 SQLite 最初设计时,Unicode 大小写折叠规则仍在不断变化。这意味着行为可能会随着每个新的 Unicode 版本而改变,从而破坏应用程序并在此过程中破坏索引。

  2. 执行完整且正确的 Unicode 大小写折叠所需的表比整个 SQLite 库都大。

如果 SQLite 使用 -DSQLITE_ENABLE_ICU 选项编译并链接到 国际组件 Unicode 库,则支持完整的 Unicode 大小写折叠。

8. 接受双引号字符串字面量

SQL 标准要求标识符使用双引号括起来,字符串字面量使用单引号括起来。例如

SQLite 接受以上两者。但是,为了与 MySQL 3.x(SQLite 首次设计时使用最广泛的 RDBMS 之一)兼容,如果双引号字符串与任何有效标识符不匹配,SQLite 也会将其解释为字符串字面量。

此功能缺陷意味着拼写错误的双引号标识符将被解释为字符串字面量,而不是生成错误。它还会诱使 SQL 语言的新手养成使用双引号字符串字面量的坏习惯,而他们真正需要学习的是使用正确的单引号字符串字面量形式。

事后看来,我们不应该尝试让 SQLite 接受 MySQL 3.x 语法,也不应该允许双引号字符串字面量。但是,有无数应用程序使用了双引号字符串字面量,因此我们继续支持此功能以避免破坏旧版功能。

从 SQLite 3.27.0(2019-02-07)开始,使用双引号字符串字面量会导致警告消息发送到 错误日志

从 SQLite 3.29.0(2019-07-10)开始,可以使用 SQLITE_DBCONFIG_DQS_DDLSQLITE_DBCONFIG_DQS_DML 操作对 sqlite3_db_config() 在运行时禁用双引号字符串字面量。可以使用 -DSQLITE_DQS=N 编译时选项在编译时更改默认设置。鼓励应用程序开发人员使用 -DSQLITE_DQS=0 进行编译,以便默认情况下禁用双引号字符串字面量功能缺陷。如果这不可能,则可以使用类似以下的 C 代码为单个数据库连接禁用双引号字符串字面量

sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DDL, 0, (void*)0);
sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DML, 0, (void*)0);

或者,如果默认情况下禁用了双引号字符串字面量,但需要为某些历史数据库连接有选择地启用它们,则可以使用与上面显示的相同的 C 代码,只是将第三个参数从 0 更改为 1。

从 SQLite 3.41.0(2023-02-21)开始,CLI 中默认情况下禁用了 SQLITE_DBCONFIG_DQS_DDL 和 SQLITE_DBCONFIG_DQS_DML。如果需要,可以使用“.dbconfig”点命令重新启用旧版行为。

9. 关键字通常可以用作标识符

SQL 语言包含丰富的关键字。大多数 SQL 实现不允许将关键字用作标识符(表或列的名称),除非它们用双引号括起来。但 SQLite 更加灵活。许多关键字可以用作标识符,而无需加引号,只要这些关键字在上下文中清楚地表明它们意在用作标识符即可。

例如,以下语句在 SQLite 中有效

CREATE TABLE union(true INT, with BOOLEAN);

由于使用了“union”、“true”和“with”作为标识符,因此相同的 SQL 语句在其他我们知道的每个 SQL 实现中都会失败。

能够使用关键字作为标识符可以促进向后兼容性。随着添加新的关键字,恰好使用这些关键字作为表或列名称的旧版架构将继续工作。但是,能够将关键字用作标识符有时会导致意外的结果。例如

CREATE TRIGGER AFTER INSERT ON tableX BEGIN
  INSERT INTO tableY(b) VALUES(new.a);
END;

前面语句创建的触发器名为“AFTER”,它是一个“BEFORE”触发器。“AFTER”标记用作标识符而不是关键字,因为这是解析语句的唯一方法。另一个例子

CREATE TABLE tableZ(INTEGER PRIMARY KEY);

tableZ 表有一个名为“INTEGER”的单列。该列没有指定数据类型,但它是主键。该列不是表的 INTEGER PRIMARY KEY,因为它没有数据类型。“INTEGER”标记用作列名的标识符,而不是数据类型关键字。

10. 允许有问题的 SQL 且没有任何错误或警告

SQLite 的原始实现试图遵循 Postel 定律,该定律部分指出“对接受的内容要宽容”。这曾经被认为是良好的设计——系统将接受有问题的输入并尽力而为,而无需过多抱怨。但最近,人们逐渐意识到,在某些情况下,对接受的内容严格一些更好,以便更容易地找到输入中的错误。

11. AUTOINCREMENT 的工作方式与 MySQL 不同

SQLite 中的 AUTOINCREMENT 功能的工作方式与 MySQL 中的不同。这常常会让最初在 MySQL 上学习 SQL 然后开始使用 SQLite 并期望这两个系统以相同方式工作的人感到困惑。

有关 AUTOINCREMENT 在 SQLite 中的功能和不的功能的详细说明,请参阅 SQLite AUTOINCREMENT 文档

12. 文本字符串中允许使用 NUL 字符

NUL 字符(ASCII 码 0x00 和 Unicode \u0000)可能出现在 SQLite 中字符串的中间。这可能导致意外行为。有关更多信息,请参阅“字符串中的 NUL 字符”文档。

13. SQLite 区分整数和文本字面量

SQLite 表示以下查询返回 false

SELECT 1='1';

它之所以这样做是因为整数不是字符串。其他所有主要的 SQL 数据库引擎都表示这是 true,原因是 SQLite 的创建者不理解。

14. SQLite 获取逗号连接的优先级错误

SQLite 将所有连接运算符赋予相同的优先级,并从左到右处理它们。但这并不完全正确。它应该是逗号连接的优先级低于所有其他连接运算符。换句话说,类似这样的 FROM 子句

... FROM a, b RIGHT JOIN c, d ...

应按如下方式解析

JOIN JOIN D RIGHT JOIN A B C

但 SQLite 反而将 FROM 子句解析为

JOIN RIGHT JOIN D JOIN C A B

仅当在同一个 FROM 子句中使用 RIGHT OUTER JOIN 或 FULL OUTER JOIN 与逗号连接时,此问题才会影响结果,这在实践中很少发生。并且可以通过在 FROM 子句中使用括号轻松克服此问题

... FROM a, (b RIGHT JOIN c), d ...

此页面上次修改于 2024-08-14 17:04:32 UTC