、概述

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-04-02) 开始,SQLite 还将 TRUE 和 FALSE 关键字分别识别为整数值 1 和 0 的别名。这提供了与其他 SQL 实现更好的兼容性。但为了向后兼容,如果有名为 TRUE 或 FALSE 的列,则关键字将被视为引用这些列的标识符,而不是 BOOLEAN 文字。

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

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

  • 作为 ISO-8601 格式的文本字符串。示例:“2018-04-02 12:13:46”。
  • 自 1970 年以来的整数秒数(也称为“unix 时间”)。
  • 作为一个 REAL 值,它是分数 Julian day number

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

通常(INTEGER PRIMARY KEY表和 WITHOUT ROWID表除外)SQLite 表中的 PRIMARY KEY 实际上与 UNIQUE 约束相同。由于历史疏忽,此类 PRIMARY KEY 的列值允许为 NULL。这是一个 bug,但当问题被发现时,有如此多的流通数据库依赖于 bug,因此决定支持 bug 行为向前发展。

INTEGER PRIMARY KEY列 的值必须始终是非 NULL 整数。WITHOUT ROWID表的 PRIMARY KEY 列 也必须是非 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.默认情况下不进行完整的 Unicode 大小写折叠

SQLite 不知道所有 unicode 字符的大写/小写区别。upper() 和 lower() 等 SQL 函数仅适用于 ASCII 字符。有两个原因:

  1. 虽然现在很稳定,但在最初设计 SQLite 时,unicode 大小写折叠的规则仍在不断变化。这意味着行为可能随着每个新的 unicode 版本而改变,从而中断应用程序并破坏进程中的索引。
  2. 进行完整和正确的 unicode 大小写折叠所需的表比整个 SQLite 库要大。

如果使用-DSQLITE_ENABLE_ICU选项编译并链接到 International Components for Unicode ,则 SQLite 支持完整的 unicode 大小写折叠。

8.接受双引号字符串文字

SQL 标准要求在标识符周围使用双引号,在字符串文字周围使用单引号。例如:

  • “这是一个合法的 SQL 列名”
  • '这是一个 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) 开始,可以在运行时使用 对sqlite3_db_config()的SQLITE_DBCONFIG_DQS_DDLSQLITE_DBCONFIG_DQS_DML操作禁用双引号字符串文字可以使用-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。

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”的列。该列没有指定数据类型,但它是 PRIMARY KEY。该列不是表的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 字符”文档。