0%

SQLite引擎

source: inside-sqlite

SQLite 引擎

后端最顶层的模块在SQLite术语中通常被称之为虚拟数据库引擎,或虚拟机(VM)。VM是SQLite的核心,并且是前端和后端之间的接口。在VM中会处理一些核心信息。它在原生的操作系统上层实现了一个抽象的虚拟的机器,并且它可以执行用SQLite内部的字节码程序语言编写的程序。这个程序语言是特地设计用来搜索,读取和修改数据库的。VM接受由前端生成的字节码程序,并且执行这个程序(您可能还记得字节码程序是准备好的语句)。VM使用B+树提供的”基础能力”来执行字节码程序,并且输出程序的执行结果。

VM不会做任何的查询语句优化操作。它会无条件地执行字节码程序。这样,它可以将数据从一个格式转换为另一个格式。即时的数据转换,是VM的主要任务。所有事情都会在它所执行的字节码程序的控制之下。

一个内存对象 sqlite3_stmt(内部为Vdbe)封装了一个字节码程序。

一个Vdbe对象包含以下的内部状态:

  • 一个字节码程序
  • 所有结果列的名字和数据类型
  • 绑定到输入参数的值
  • 一个程序计数器
  • 操作对象的执行栈
  • 任意数量的“编号”存储单元
  • 其他运行时的状态信息(例如打开的BTree对象,排序器,列表和集合)

1 字节码程序语言

SQLite定义了一种内部的编程语言来准备字节码程序。该语言类似于物理机和虚拟机使用的汇编语言:它定义了字节码指令。一个字节码指令按照如下格式<opcode, P1, P2, P3>, opcode 标识了特定的字节码操作,并且P1, P2, P3是这个操作符的操作数。每一个字节码操作符定义了少量的VM工作。P1操作数是一个32位的有符号整型数。P2 操作数是一个31位的非负整型,在任何一个可能会导致跳转的操作中,P2永远是跳转的目标,当然它也会被用在其他用途。P3操作数是一个指向具有Null终止符的字符串的指针,或者是一个指向不同的结构化的对象或者NULL指针。有些操作符使用三个操作数,一些典型的操作符会忽略一个或者两个操作数,并且很多甚至会忽略三个操作数。

操作符是VM内部的操作名,并且它们不是SQLite接口规范的一部分。所以,它们的操作语义可能会从一个版本更改为另一个版本。SQLite的开发者团队不鼓励SQLite的用户自己去写字节码程序。

表6-1表述了一个等价于 SELECT * FROM t1 的典型的字节码程序。表t1有两列,命名为x和y。除了第一行以外,后面的每一行都是一个字节码指令。

Address Opcode p1 p2 p3
0 Goto 0 11
1 Integer 0 0
2 OpenRead 0 2
3 SetNumColumn 0 2 #t1
4 Rewind 0 9
5 Column 0 0 #x
6 Column 0 1 #y
7 Callback 2 0
8 Next 0 5
9 Close 0 0
10 Halt 0 0
11 Transaction 0 0
12 VerifyCookie 0 1
13 Goto 0 1

如果说VM是一个解释器,那么下面的就是它的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (; pc < nOp && rc == SQLITE_OK; pc++){ 
switch (aOp[pc].opcode){
case OP_Add:
/* Implementation of the ADD operation here */
break;
case OP_Goto:
pc = op[pc].p2-1;
break;
case OP_Halt:
pc = nOp;
break;
/* other cases for other opcodes */ }
}

这个解释器是一个包含了一个巨大的Switch语句的循环体。每一个case的语句实现了一个字节码指令(操作码的名字以 OP_ 开头)。在每一次迭代中,VM从程序中拉取下一次的字节码指令。例如:从aOp数组中以pc作为下标索引获取下一个字节码指令。它会解码并且执行指定的指令。VM从指令号0开始执行一个字节码程序。

VM使用游标来访问数据库。在一个数据库上可以有若干个打开的游标。每一个游标都是一个指向单个表或者索引的树。光标可以通过一个指定的key直接检索到一个条目,或者遍历整个树上的条目。VM在光标上的当前条目处插入新条目,检索键/数据或删除条目。

VM使用操作数栈和一个任意大小的内存来保存中间结果。许多操作码都从栈中获取使用操作数。计算结果也保存在栈上。每一个栈或者内存位置都保存一个单个数据值。内存位置通常用于保存作为较大表达式一部分的标量SELECT的结果。

VM会一直执行字节码程序,一直到它处理一个停止指令或者遇到一个错误(在解释程序中,rc变量存储了指令的执行状态),或者程序计数器指向了最后一条指令。当程序终止的时候,它会释放所有已经分配的内存,并且关闭所有的游标。如果执行因为错误而终止了,VM会终止事务或者子事务,并且移除当前事务或者子事务引起的变更。

2 记录的格式

VM把数据值以记录(record)的形式存储在B/B+树内。每一条记录包含一个key项可选的数据项。VM仅负责维护key和数据的内部结构(尽管B+-tree模块可以在树(叶或内部)和多个溢出页之间拆分单个记录,但是VM是把每一条记录看做一个逻辑上连续的字节串)。VM在表记录和索引记录上使用了两种不同但是类似的记录格式。

有两种格式来格式化data/key记录:固定长度和可变长度。对于固定的长度来说,对于表或者索引的所有记录都是使用了相同大小的空间;在表或者索引的创建的时候就已经知道每一个字段的大小了。对于可变长度的格式来说,每一个字段的空间大小可能根据不同的记录而不一样。SQLite使用可变的变量长度来格式化记录,因为它有几个优势。数据库因为没有空间的浪费而变得更小。同样也会让整个系统跑得更快,因为在内存和磁盘之间需要同步的bytes数量更小。另外,使用可变长度的记录可以允许SQLite可以使用显式类型而不是静态类型。

每一个数据库内存储的或者VM操作的值,都有一个相关联的数据类型。称之为数据值的存储类型。我们可以对数据进行的操作取决于数据的类型。大部分的SQL数据库使用静态类型:一个数据类型与表内的列相关联,只允许将该特定数据类型的值存储在该列中。SQLite通过使用显式类型放宽了此限制。在显式类型中,数据的类型是数据自己本身的一个属性,而不属于存储这个值的列或者变量。SQLite使用显式类型(即使SQL语言规范要求静态类型),即把数据类型作为数据的一部分来存储。SQLite允许用户不用管当前列声明的数据类型,而在该列中存储任何类型的任何数据。(有一个例外是,一个整型类型的主键列只允许存整型。)

表记录的格式

Header size Type 1 Type 2 Data1 Data2

上面给出了一个表记录(row data)的格式。记录有两部分组成:头部和记录内容。头部由一个大小字段开始,后面跟着字段的类型。头部后面跟着记录的数据项。Header size是Data1之前的字节数。这个大小是一个哈夫曼编码的64位的可变长度的integer值,并且它包含了它自身所占用的大小。这个大小也可以被用作Data1项的指针。在header size大小之后紧跟着的是数据类型字段,每一个数据值都按照它在字段中出现的顺序排列。每一个类型字段Type i是一个可变长的无符号整型(最大是2^64),对应编码了数据字段Data i的存储类型。

VM支持5种存储类型:有符号整型,有符号的浮点类型,字符串,字节流和SQL NULL。每一个在内存或者文件中存储的数据必须是以上五种之一。注意到一些数据可能在某个时间有多种含义。举个例子,123可以是一个整型数据,浮点小数,或者一个字符串。字节流和NULL没有其他的含义。所以从一种类型隐式转换为另一种类型也是有必要的。

存储类型取值和它们的含义

类型值 含义 数据长度
0 NULL 0
N in {1..4} 有符号整数 N
5 有符号整数 6
6 有符号整数 8
7 IEEE 浮点 8
8 常数 0 0
9 常数 1 0
10、11 扩展保留 N/A
N>=12 偶数 字节流 (N-12)/2
N>=12 奇数 纯文本 (N-13)/2

NULL类型代表了SQL的NULL值。对于INTEGER类型,数据值是一个有符号的整型,根据数据大小的不同,存储在1,2,3,4,6或8个字节内。对于REAL类型,数据值是一个浮点小数,按照IEEE浮点数表示标准中的规定,存储在8个字节内。8,9两个数据类型,分别代表了整型常量0和1.对于TEXT类型,数据值就是一个文本字符串,使用默认编码(UTF-8, UTF-16BE, or UTF-16-LE)格式存储文本。对于后两者来说,字节顺序分别是大端或者小端。(每个数据库文件只会用一种UTF编码)对于BLOB类型,数据值是一个字节流,完全和用户输入的字节流一致。

在SQLite中,每一个B+-Tree必须有一个唯一的key。尽管一个定义好的关系型表不包含相同的行,但是实际上用户是可以在关系表中存储重复的行的。但是数据库系统必须有办法来区分这些相同的行。该系统必须能够关联其他信息以实现差异化目的。这也就意味着,系统需要为这个关系提供一个新的唯一的属性。因此,在内部,每个表都有唯一的主键,并且该键由表的创建者或SQLite来定义。

对于每一个SQL表,SQLite指定了一列作为rowid(或oid或_rowid_),这一列的值唯一标识了在表中的每一行。它是表的隐式主键,表B+树的唯一搜索关键字。如果表内的任何一列被声明为 integer primary key,那么这列就会被当做这个表的rowid(作为别名)。否则的话,SQLite会创建一个独立的名为rowid作为唯一主键列。因此,每一个表不论是否声明了 integer primary key列,都有一个唯一的整型key,命名为rowid。对于后一种情况,rowid本身在内部被视为表的整数主键。无论哪种情况,rowid都是[-2^63, 2^63-1]范围内的有符号整数值

下面的表展示了一个典型的SQL表的内容,这个表由SQL语句:create table t1(x, y)创建。rowid列由SQLite新增。rowid值通常由SQLite确定。不过你可以通过insert into t1(rowid, x, y) values(100, 'hello', 'world')为rowid插入任何值。

一个典型的SQL表

rowid x y
-5 abc xyz
1 abc 12345
2 456 def
100 hello world
54321 NULL 987

注意

如果rowid作为一个列的别名出现(例如,声明了 INTEGER PRIMARY KEY),数据库的用户将要负责对这个列的值的维护。如果rowid是由SQLite添加的,那么SQLite负责这个值的生成,并且它需要保证这个值的唯一性。当一行被插入的时候,如果没有指定一行内的rowid的值,SQLite会访问表的B+树并且为rowid找到一个没有使用过的整型数、

这个rowid的值,如果作为数据记录的一部分存储,那么就会具有一个内部的整型类型。而如果作为key来存储,那么它就是一个可变长度的哈夫曼编码。rowid允许可以使用负数,但是他们通常使用9个字节来存储,所以一般不鼓励这样使用。当rowid由SQLite生成的时候,它们通常是非负的,尽管你可以指定这个rowid为一个负数,上面的表里-5就是一个例子。

在前面的一小节里,你已经看到了,每个表的B+树的key是一个整型,并且数据记录就是表内其中的一行。索引表也保留了这个设计安排。对于一个索引记录来说,键(key)是要存储在索引表中的行的所有索引列的组合值,而数据是行的rowid。要访问具有某些用于索引列的特定值的表行,SQLite首先搜索索引表以找到相应的rowid整数值,然后使用该整数值在表的B+树中查找完整记录。

索引记录的格式

Header size Type 1 Type 2 ··· Data1 Data2 rowid

SQLite把索引也当做一个表,并且在它自己的B-Tree中存储索引。它将一个搜索的key映射到了一个rowid上。它可以有自己的关键字的比较器,例如,使用排序方法来排序索引条目。每一个索引记录包含了索引列值的拷贝,并且紧跟着被索引的这个行的rowid。上面给出了索引记录的格式。整个记录都当做了B-Tree的key;没有数据部分。索引记录的编码和数据表的记录的一样,除了后面跟了一个rowid,并且rowid的数据类型,没有在记录头部显式声明。因为rowid的类型只能是有符号整数并且是用哈夫曼编码表示的(不是内部的整型类型)。(其它数据的值和数据的类型都从表内拷贝)在x列上的内容索引,如下所示.

x rowid
NULL 54321
456 2
abc -5
abc 1
hello 100

SQLite也支持多列索引。下表展示了y列和x列的索引内容。索引的条目根据他们的第一列的值排序,后面的列用作子排序。

y列和x列的索引

y x rowid
987 NULL 54321
12345 abc 1
def 456 2
xyz abc -5
world hello 100

索引主要是用来加速数据库查询的。举个例子,思考一下以下查询SELECT y FROM t1 WHERE x=。SQLite 在索引t1(x)上做一次索引搜索,并且找到所有x=?的rowid;对于每一个rowid,再在表t1的B+树上做一次搜索来获取到y列的值。

3 数据类型管理

SQLite的数据处理是发生在VM模块的。VM是数据库内数据存储的唯一操纵者;所有事情都是通过字节码的执行来控制的。它决定了在某个地方存储什么数据,以及在某个地方检索什么数据。给合适的数据分配合适的数据类型,并且做必要的数据转换都是VM的主要任务。有三个数据交换的地方可能会触发数据转换:从应用到引擎,从引擎到应用,从引擎到引擎。对于前两个情况,VM为用户数据分配数据类型,VM会尽可能将用户提供的数据转换为在列上声明的数据类型,反之亦然。对于后一种情况,表达式转换会引起数据转换。在下面三个小节中,我们讨论一下这三种数据转换。

在记录格式那一节中,我们讨论了表和索引记录的存储格式。每一个记录的字段值都有一个存储类型。每一个提交给SQLite的值,不论是否是作为文字嵌入到SQL语句中,亦或是准备语句的时候绑定的值,在语句执行的时候都会被指派一个数据类型。这个类型是用来将这个值实际编码为一个合适的”物理格式”。VM通过三个步骤来决定一个值的数据类型:它首先确定输入数据的存储类型,然后确定列的声明的SQL类型,最后,如果需要,它再进行类型转换。在后面章节的讲述中,SQLite可能会在查询期间在数字存储类型(INTEGER和REAL)与TEXT之间转换数据。

3.1.1 输入数据类型

VM根据如下情况,为一个用户数据指派一个初始化的存储类型。一个嵌入在SQL语句中的文字将会被指派以下的几个存储类型中的一个:

  • 如果值被单双引号包含,那么指派为TEXT
  • 如果值是不带小数点或指数的,不带引号的数字,那么指派为INTEGER
  • 如果值是带小数点或指数的不带引号的数字,那么指派为REAL
  • 如果值是字符串NULL,且周围没有引号,那么指派为NULL
  • 如果使用X’ABCD’标记指定值,那么指派为BLOB

除这些之外,输入的值会被拒绝,并且查询会失败。使用sqlite3_bind_ * API函数提供的SQL参数值将分配给与函数名绑定最接近的存储类型(例如,sqlite3_bind_blob 绑定一个具有存储类型BLOB的值)。

SQL标量运算符结果的值的存储类型取决于表达式的最外层运算符。用户定义的函数可以返回任何存储类型的值。通常在SQL语句的准备时间阶段无法确定表达式结果的类型。VM在运行时获取值的时候分配存储类型。

3.1.2 列关联性

尽管每个列(整数主键除外)都可以存储任何类型的值,但是该值也是会具有其声明的SQL类型的关联性。其他SQL数据库引擎使用限制性更强的静态类型,其中类型与容器关联,而不与值关联。为了最大程度地提高SQLite与其他数据库引擎之间的兼容性,SQLite支持在列上使用类型关联性的概念。对于该列中存储的值,建议使用列类型相似性:”建议使用,不是必需的。”任何列仍可以存储任何类型的数据。只是,某些列(在有选择的情况下)宁愿使用关联的那种类型而不是其他类型。

注意

SQLite是无类型的,即没有域约束。它允许在已经声明了数据类型的任何一列内存储任何类型。(除了rowid的这一列,这一列只能存整型,不能存任何其他类型)SQLite可以让你在创建语句中忽略掉SQL类型。例如,create table T1(a, b, c)在SQLite中是一个有效的SQL语句。

列的首选类型称为其关联类型。每一列只能分配为以下五个类型之一:TEXT, NUMERIC, INTEGER, REAL, and NONE。(你可能注意到了一些与上文的冲突,”text,” “integer,”和”real”也是用于存储类型的名字。但是,您可以根据上下文确定类型类别)根据CREATE TABLE语句中声明的SQL类型,根据以下规则确定列的关联类型(SQLite对SQL类型声明中的拼写错误完全不严格):

  1. 如果SQL类型包含子字符串INT,则该列具有INTEGER关联。
  2. 如果SQL类型包含任何子字符串CHAR,BLOB或TEXT,则该列具有TEXT关联性。 (SQL类型VARCHAR包含字符串CHAR,因此也具有TEXT关联性。)
  3. 如果SQL类型包含子字符串BLOB,或者未指定类型,则列具有NONE关联。
  4. 如果SQL类型包含任何子串REAL,FLOA或DOUB,则列具有REAL关联。
  5. 除此之外, 列具有NUMERIC关联。

VM按照与上述相同的顺序评估规则。模式匹配不区分大小写。例如,如果声明的列的SQL类型为BLOBINT,则关联性为INTEGER,而不是NONE。如果使用create table table1 as select ...语句创建了SQL表,则所有列都没有SQL类型,并且它们的关联性为NONE。隐式rowid的类型始终是整数.

3.1.3 类型转换

在关联类型和存储类型之间有点关系。如果用户为列提供的值不满足该关系,则该值将被拒绝或转换为适当的格式。在将值插入列中时,VM首先分配最合适的存储类型(请参见“输入数据类型”部分),然后尝试将初始存储类型转换为其与列相关联类型的格式(请参见“列关联”部分)。它会按照下面的规则执行:

  1. 具有TEXT关联性的列存储所有具有NULL,TEXT或BLOB存储类型的数据。如果将数值(整数或实数)插入列中,则该值将转换为文本形式,并且最终的存储类型为TEXT。
  2. 具有NUMERIC关联性的列可能包含所有五种存储类型的值。将文本值插入NUMERIC列时,将尝试将值转换为整数或实数。如果转换成功,则使用INTEGER或REAL存储类型存储转换后的值。如果无法执行转换,则使用TEXT存储类型存储该值。不会尝试转换NULL或BLOB值。
  3. 具有INTEGER关联性的列的行为与具有NUMERIC关联性的列的行为相同,不同之处在于,如果插入没有浮点分量的实数值(或转换为此类的文本值),则该值将转换为整数并使用INTEGER存储类型。
  4. 具有REAL关联性的列的行为类似于具有NUMERIC关联性的列,不同之处在于它强制将整数值转换为浮点表示形式。(作为一种优化,整数值以整数形式存储在磁-盘上,以占用更少的空间,并且仅当从表中读取该值时才将其转换为浮点数。)
  5. 关联性为NONE的列不会更偏向于哪个存储类型。 VM不会尝试转换任何输入值。

注意

所有的SQL数据库引擎都会执行数据转换。它们一般都会拒绝一些无法转换为目标类型的输入数据。区别是在于,当不能格式化数据的时候,SQLite依旧会存储这个值。举个例子,如果你有一个表列,声明了SQL类型为INTEGER,并且尝试插入一个字符串(如:”123”或者”abc”),VM会鉴别一下这个字符串,看看它是否是像一个数字。如果字符串看起来像是一个数字(例如”123”),它就会被转换为一个数字(如果数字没有小数部分,那就会被转换为整型),并且存储为一个REAL或者INTEGER存储类型。但是,如果这个字符串的内容不是一个数字格式的话(如:”abc”),它就会被存储为TEXT存储类型。一个具有TEXT关联性的列在存储之前会尝试把数字转换为ASCII码文本。但是BLOBs存储在TEXT列的时候就不会发生转换,因为SQLite无法转换。SQLite允许把字符串的值转换为整型。这是一个特性,不是bug。

3.1.4 一个简单的例子

一个典型的表数据记录

CREATE TABLE T1(a,b,c);
INSERT INTO T1 VALUES(177,NULL,'hello');

Header size Type 1 Type 2 Type 3 Data 1 Data 2 Data 3
04 02 00 17 00 B1 68 65 6C 6C 6F

让我们更清晰的了解一个非常简单的例子。上面的表给出了一个典型的无类型的表记录。a,b,c三列的初始化的输入类型是integer,NULL和TEXT。每一列的关联类型是NONE,VM不会尝试去转换初始化的存储类型。在图表里,这个记录包含了11个字节(header+data)。所有数据都是16进制格式的。

  1. Header有4个字节:一个字节是用来标识头部大小,后续的三个数据都有一个字节来标识数据类型。数字4会被编码为单个字节的0x04。
  2. Type 1是被编码为单个字节的2.代表有两个字节的无符号整数
  3. Type 2是被编码为单个字节的0.代表NULL
  4. Type 3是被编码为单个字节的23.代表纯文本,文本长度为(23-13/2=)5个字节。
  5. Data 1是一个2个字节长的00B1,也就是117。这里117是无法编码为单个字节的,因为B1是-79而不是177.
  6. Data 2是NULL,它不会在记录里占据任何的字节大小。
  7. Data 3是5个字节长的字符串68 65 6C 6F。终止符0被忽略了。

sqlite3_column_*开头的API方法,在SQLite引擎外读取数据。这些方法会在合适的时候转换数据类型。举个例子,如果内部的类型是FLOAT,但是结果要求是一个字符串(方法 sqlite3_column_text),那么VM会使用sprintf()库方法在返回值给调用者的时候在内部做一次数据转换。下面的表格就是VM将内部数据为上层应用创建输出数据的转换规则。

数据转换规则

内部类型 要求数据类型 转换结果
NULL INTEGER 结果是 0
NULL FLOAT 结果是 0
NULL TEXT 结果是NULL指针
NULL BLOB 结果是NULL指针
INTEGER FLOAT 整型转换为浮点型
INTEGER TEXT 整数的ASCII呈现
INTEGER BLOB 和上面一样
FLOAT INTEGER 浮点型转换为整型
FLOAT TEXT 浮点数的ASCII呈现
FLOAT BLOB 和上面一样
TEXT INTEGER 使用atoi() C库函数
TEXT FLOAT 使用atof() C库函数
TEXT BLOB 没有变化
BLOB INTEGER 使用atoi() 转换
BLOB FLOAT 使用atof() 转换
BLOB TEXT 如果有必要的话,增加\000终止符

VM可能会先转换内部数据,然后再与另一个进行比较或评估表达式.它会使用以下规则来转换内部数据.

3.2.1 处理NULL值

NULL值可以在任何一个表列使用,除了主键列。NULL值的存储类型是NULL。无论它们的存储类型如何,NULL值与给定列的所有有效值都不同。SQL标准对于如何处理表达式列中的NULL值不是很具体。根据标准尚不清楚在所有情况下应如何正确处理NULL值。例如,我们如何比较NULL和其他值?SQLite以许多其他DBMS一样的方式处理NULL。在SELECT DISTINCT语句,使用UNION操作符的SELECT的组合语句,和GROUP BY语句中NULL是一样的。但是,NULL在UNIQUE列中却是不同的。NULL由SQL标准指定的内置SUM函数处理。对NULL进行算术运算会产生NULL。

3.2.2 表达式中的类型

SQLite支持三种比较操作符:

  • 二元比较运算符 =, <, <=, >, >= 和 !=
  • 成员运算符 IN
  • 三元比较运算符 BETWEEN

根据以下规则,比较的结果取决于要比较的两个值的存储类型:

  1. 如果运算符左侧的值是NULL,那么这个值一般都会认为比其他的小。(如果其他的值中有NULL,也是这样)
  2. INTEGER或REAL比TEXT或BLOB值小。如果INTEGER或REAL和另一个INTEGER或REAL比较,那么就是常规的数字比较了。
  3. TEXT值比BLOB值小。如果两个TEXT值比较,那么就是使用标准C库的memcmp函数来比较。但是这个函数是可以被用户自定义的函数重写的。
  4. 当两个BLOB比较的时候,始终使用memcmp函数比较。

在应用这些规则之前,VM的首要任务是确定比较运算符的操作数的存储类型。它首先确定操作数的初步存储类型,然后(如有必要)根据它们的相似性在类型之间转换值。最后,它使用以上四个规则进行比较。

如果一个表达式是某列,或者是使用别名指向的某列,或者是一个子查询返回的一个列,或者是rowid,那么这个列的关联性就会被用作这个表达式的关联性。否则,这个表达式没有SQL类型,并且它的关联性是NONE。SQLite会尝试在比较运算之前,在(INTEGER和REAL)和TEXT之间做转换。对于二元比较,会在下列情况下完成。这里说的表达式,是除了列值以外的任何SQL标量表达式或者文字。

  • 当两个列的值比较的时候,如果其中任何一列的值有NUMERIC关联性,那么这两个值会优先使用这个关联性。也就是说,VM尝试在比较之前转换其他列的值。
  • 当将列值与表达式的结果进行比较时,在进行比较之前,将列的关联性同样应用于这个表达式的结果。
  • 比较两个表达式的值时,将不进行任何转换。按照上述四个标准规则比较这些值。例如,如果将字符串与数字进行比较,则数字将始终小于字符串。

在SQLite中表达式a BETWEEN b AND c对于a >= b AND a <= c是等价的,尽管在两次比较中,a列的关联性会不一样。

对于表达式a IN (SELECT b ...)来说,就会使用上面提到的=号的二元操作符的规则来处理(例如,a=b)。举个例子,如果b是一个列的值,a是一个表达式,那么在比较之前b的关联性就会被应用到a上。SQLite处理表达式a IN (x,y,z)和处理a = x OR a = y OR a = z是一样的,尽管a的关联性是不一样的。

有些简单的例子。假设,你有一个通过CREATE TABLE t1(a TEXT,b NUMERIC, c BLOB)语句创建的表。你可以通过执行INSERT INTO t1 VALUES(‘500’,‘500’,‘500’)插入一条记录。那么最终,a,b,c三列的存储类型就会变成TEXT,INTEGER,TEXT。

  • SELECT a < 60,a < 40 FROM t1,会把60和40转换为”60”和”40”,因为a列具有TEXT关联性,值就会被当做TEXT来比较。并且整个语句返回1|0作为输出,因为”500”比”60”小,但是比”40”大。
  • SELECT b < 60,b < 600 FROM t1不会转换任何值,会被当做普通的数字比较,那么就会返回0|1
  • SELECT c < 60,c < 600 FROM t1不会转换值,因为c的关联性是NONE。存储的两个值(存储类类型是NUMERIC)都是小于”500”(存储类型是TEXT),那么就会返回0|0

3.2.3 操作符类型

所有的数学运算符(除了 || 串联运算符以外)都将NUMERIC关联到所有的操作数上,并求值。如果所有操作数都无法转换为NUMERIC,那么运算结果为NULL。对于串联运算符来说,TEXT将关联到两个操作数上。如果任何一个操作数都无法转换为TEXT(NULL或者BLOB),那么串联的结果为NULL。

3.2.4 ORDER BY里的类型

当值被ORDER BY语句排序的时候,在排序之前不会有存储类型的转换。遵循先前规定的标准比较规则:NULL在最前面,然后是按值大小的INTEGER和REAL,再然后是TEXT,最后是BLOB,后面两个通常是memcmp()排序顺序。同样的文本排序方法可以被用户定义的函数重写。

3.2.5 GROUP BY里的类型

当值被GROUP BY语句分组的时候,在分组之前不会有存储类型的转换。具有不同存储类型的值被认为是不同的,但INTEGER和REAL值除外,如果它们在数值上相等,则被视为相等。

3.2.6 复合SELECT里的类型

复合SELECT操作符(UNION,INTERSECT和EXCEPT),在值之间进行隐式比较。在执行这些比较之前,可以将关联性应用于每个值。相同的关联性(如果有的话)将应用于可能在复合SELECT结果集的单个列中返回的所有值。所应用的关联性是最左边的SELECT返回的列的关联性,该组件在该位置具有列值(而不是其他某种表达式)