Small. Fast. Reliable.
Choose any three.
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 Version 3文档中的数据类型。

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

同样,SQLite允许您将2000个字符的字符串存储到VARCHAR(50)类型的列中。其他SQL实现可能会抛出错误或截断字符串。SQLite存储整个2000个字符的字符串,而不会丢失任何信息,并且不会产生任何投诉。

最终导致问题的地方是开发人员使用SQLite进行一些初始编码工作并使其应用程序正常工作,然后尝试转换到另一个数据库(如PostgreSQL或SQL Server)进行部署。如果应用程序最初利用了SQLite的灵活类型,那么当它移到另一个使用更严格和不容忍的类型强制策略的数据库时,它将失败。

灵活类型被认为是SQLite的功能,而不是错误。但是,我们认识到,此功能有时会给习惯于与其他数据库进行数据类型判断的习惯的开发人员造成混淆和痛苦。回想起来,如果SQLite仅实现ANY数据类型,以便开发人员可以在需要使用灵活类型时明确声明而不是将灵活类型设置为默认值,则可能会更好。但是,如果不破坏已经使用SQLite灵活键入功能的数百万个应用程序和数万亿个数据库文件,现在就无法更改。

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

与大多数其他SQL实现不同,SQLite没有单独的BOOLEAN数据类型。取而代之的是,TRUE和FALSE(通常)分别表示为整数1和0。这似乎并不会引起很多问题,因为我们很少对此有任何抱怨。但是,认识到这一点很重要。

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

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

SQLite没有DATETIME数据类型。而是可以用以下任何一种方式存储日期和时间:

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

3.3。数据类型是可选的

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

创建表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.主键有时可以包含NULL

通常(例外是INTEGER PRIMARY KEY表和 WITHOUT ROWID表),SQLite表中的PRIMARY KEY实际上与UNIQUE约束相同。由于历史上的疏忽,此类PRIMARY KEY的列值允许为NULL。这是一个错误,但是到那时发现问题的时候,由于存在大量依赖该错误的数据库,因此决定支持该错误行为。

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选项编译并与Unicode国际组件 库链接,则 SQLite支持完全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)开始,可以在运行时使用对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语句。

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

在tableX BEGIN上插入后创建触发器
  插入表Y(b)值(new.a);
结尾;

由上一条语句创建的触发器称为“ AFTER”,它是“ BEFORE”触发器。“ AFTER”令牌用作标识符而不是关键字,因为这是解析该语句的唯一方法。另一个例子:

创建表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并期望这两个系统工作相同的人们来说,这常常会造成混乱。

请参阅SQLite AUTOINCREMENT文档以获取有关在SQLite中执行和不执行AUTOINCREMENT的详细说明。

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

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