SQLite中的特点和注意事项

/ 默认分类 / 没有评论 / 1447浏览

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

本文档努力强调 SQLite 和其他 SQL 实现之间的主要区别,以帮助正在移植到 SQLite 或从 SQLite 移植或试图构建跨多个数据库引擎工作的系统的开发人员。

如果您是 SQLite 用户,并且偶然发现了此处未提及的 SQLite 的一些怪癖,请给我们发送电子邮件,以便我们记录问题。

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

每当将 SQLite 与其他 SQL 数据库引擎(如 SQL Server、PostgreSQL、MySQL 或 Oracle)进行比较时,首先要意识到 SQLite 并非旨在替代任何这些系统或与之竞争,这一点很重要。 SQLite 是无服务器的。 没有单独的服务器进程来管理数据库。 应用程序使用函数调用与数据库引擎交互,而不是通过将消息发送到单独的进程或线程。

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

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

有关其他信息,请参阅 SQLite适用场景

可变类型

SQLite 在数据类型方面非常灵活。

一些评论员说 SQLite 是“弱类型的”,而其他 SQL 数据库是“强类型的”。我们认为这些术语是不准确和贬义的。我们更愿意说 SQLite 是“灵活类型化”,而其他 SQL 数据库是“刚性类型化”。

有关 SQLite 中类型系统的详细讨论,请参阅 SQLite 版本 3 中的数据类型文档。

关键是 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 灵活键入功能的数百万个应用程序和数万亿个数据库文件,现在就无法改变这种情况。

没有单独的Boolean类型

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

从 SQLite 版本 3.23.0 (2018-04-02) 开始,SQLite 还将 TRUE 和 FALSE 关键字分别识别为整数值 1 和 0 的别名。 这提供了与其他 SQL 实现的更好兼容性。 但是为了保持向后兼容性,如果存在名为 TRUE 或 FALSE 的列,则将关键字视为引用这些列的标识符,而不是 BOOLEAN 文字。

没有单独的 DATETIME 数据类型

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

作为 ISO-8601 格式的文本字符串。 示例:'2018-04-02 12:13:46'。 自 1970 年以来的整数秒数(也称为“unix 时间”)。 作为一个 REAL 值,即小数儒略日数。 SQLite 内置的日期和时间函数可以理解上述所有格式的日期/时间,并且可以在它们之间自由切换。 您使用哪种格式,完全取决于您的应用程序。

数据类型是可选的

由于 SQLite 在数据类型方面灵活且宽容,因此可以创建完全没有指定数据类型的表列。例如:

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

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

外键强制默认关闭

SQLite 已经分析了外键约束的时间,但在版本 3.6.19 (2009-10-14) 中添加了实际强制执行这些约束的能力。 到添加外键约束强制执行时,已经有数百万个包含外键约束的数据库在流通,其中一些是不正确的。 为了避免破坏这些遗留数据库,SQLite 中默认关闭外键约束强制执行。

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

PRIMARY KEY 有时可以包含 NULL

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

INTEGER PRIMARY KEY 列的值必须始终是非 NULL 整数。 WITHOUT ROWID 表的 PRIMARY KEY 列也必须为非 NULL。

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

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

SQLite 不强制执行此限制。 聚合查询的输出列可以是包含在 GROUP BY 子句中找不到的列的任意表达式。 此功能有两个用途:

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

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

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

在上面的查询中,first_name 和 last_name 列的值将对应于满足 max(salary) 条件的行。 如果查询根本不包含聚合函数,则可以添加 GROUP BY 子句来替代 DISTINCT ON 子句。 换句话说,对输出行进行过滤,以便 GROUP BY 子句中的每组不同的值只显示一行。 如果两个或多个输出行本来具有相同的 GROUP BY 列值集,则任意选择其中一行。 (SQLite 支持 DISTINCT 但不支持 DISTINCT ON,其功能由 GROUP BY 提供。)

默认情况下不进行完整的 Unicode 大小写折叠

SQLite 不知道所有 unicode 字符的大写/小写区别。 像 upper() 和 lower() 这样的 SQL 函数只对 ASCII 字符起作用。 有两个原因:

虽然现在很稳定,但在 SQLite 最初设计时,unicode 大小写折叠规则仍在不断变化。 这意味着行为可能会随着每个新的 unicode 版本而改变,在此过程中会中断应用程序并损坏索引。 进行完整和正确的 unicode 大小写折叠所需的表比整个 SQLite 库大。 如果 SQLite 使用 -DSQLITE_ENABLE_ICU 选项编译并链接到 International Components for Unicode 库,则 SQLite 支持完整的 unicode 大小写折叠。

接受双引号字符串文字

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) 开始,可以在运行时使用 SQLITE_DBCONFIG_DQS_DDL 和 SQLITE_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。

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

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

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

CREATE TABLE union(true INT, with BOOLEAN);

由于使用关键字“union”、“true”和“with”作为标识符,相同的 SQL 语句将在我们知道的所有其他 SQL 实现上失败。

使用关键字作为标识符的能力促进了向后兼容性。 随着新关键字的添加,恰好使用这些关键字作为表名或列名的旧模式继续工作。 然而,使用关键字作为标识符的能力有时会导致令人惊讶的结果。 例如:

CREATE TABLE tableZ(INTEGER PRIMARY KEY);

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

允许可疑 SQL 没有任何错误或警告

SQLite 的最初实现试图遵循 Postel 定律,该定律部分指出“对你所接受的事物保持自由”。 这曾经被认为是好的设计 - 系统会接受狡猾的输入并尝试做到最好而不会抱怨太多。 但是最近,人们开始意识到有时严格要求您接受的内容会更好,以便更容易地发现输入中的错误。

AUTOINCREMENT 与 MySQL 不同

SQLite 中的 AUTOINCREMENT 功能与 MySQL 中的工作方式不同。 对于最初在 MySQL 上学习 SQL 然后开始使用 SQLite 并期望两个系统以相同方式工作的人来说,这通常会引起混淆。

有关 AUTOINCREMENT 在 SQLite 中做什么和不做什么的详细说明,请参阅 SQLite AUTOINCREMENT 文档。

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

NUL字符(ASCII码0×00和Unicode\ u0000的)可能会出现在SQLite的字符串的中间。这可能会导致意外的行为。请参阅文件“字符串中的NULL字符”以获取更多信息。