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 | for (; pc < nOp && rc == SQLITE_OK; pc++){ |
这个解释器是一个包含了一个巨大的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类型声明中的拼写错误完全不严格):
- 如果SQL类型包含子字符串INT,则该列具有INTEGER关联。
- 如果SQL类型包含任何子字符串CHAR,BLOB或TEXT,则该列具有TEXT关联性。 (SQL类型VARCHAR包含字符串CHAR,因此也具有TEXT关联性。)
- 如果SQL类型包含子字符串BLOB,或者未指定类型,则列具有NONE关联。
- 如果SQL类型包含任何子串REAL,FLOA或DOUB,则列具有REAL关联。
- 除此之外, 列具有NUMERIC关联。
VM按照与上述相同的顺序评估规则。模式匹配不区分大小写。例如,如果声明的列的SQL类型为BLOBINT,则关联性为INTEGER,而不是NONE。如果使用create table table1 as select ...语句创建了SQL表,则所有列都没有SQL类型,并且它们的关联性为NONE。隐式rowid的类型始终是整数.
3.1.3 类型转换
在关联类型和存储类型之间有点关系。如果用户为列提供的值不满足该关系,则该值将被拒绝或转换为适当的格式。在将值插入列中时,VM首先分配最合适的存储类型(请参见“输入数据类型”部分),然后尝试将初始存储类型转换为其与列相关联类型的格式(请参见“列关联”部分)。它会按照下面的规则执行:
- 具有TEXT关联性的列存储所有具有NULL,TEXT或BLOB存储类型的数据。如果将数值(整数或实数)插入列中,则该值将转换为文本形式,并且最终的存储类型为TEXT。
- 具有NUMERIC关联性的列可能包含所有五种存储类型的值。将文本值插入NUMERIC列时,将尝试将值转换为整数或实数。如果转换成功,则使用INTEGER或REAL存储类型存储转换后的值。如果无法执行转换,则使用TEXT存储类型存储该值。不会尝试转换NULL或BLOB值。
- 具有INTEGER关联性的列的行为与具有NUMERIC关联性的列的行为相同,不同之处在于,如果插入没有浮点分量的实数值(或转换为此类的文本值),则该值将转换为整数并使用INTEGER存储类型。
- 具有REAL关联性的列的行为类似于具有NUMERIC关联性的列,不同之处在于它强制将整数值转换为浮点表示形式。(作为一种优化,整数值以整数形式存储在磁-盘上,以占用更少的空间,并且仅当从表中读取该值时才将其转换为浮点数。)
- 关联性为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进制格式的。
- Header有4个字节:一个字节是用来标识头部大小,后续的三个数据都有一个字节来标识数据类型。数字4会被编码为单个字节的0x04。
- Type 1是被编码为单个字节的2.代表有两个字节的无符号整数
- Type 2是被编码为单个字节的0.代表NULL
- Type 3是被编码为单个字节的23.代表纯文本,文本长度为(23-13/2=)5个字节。
- Data 1是一个2个字节长的00B1,也就是117。这里117是无法编码为单个字节的,因为B1是-79而不是177.
- Data 2是NULL,它不会在记录里占据任何的字节大小。
- 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
根据以下规则,比较的结果取决于要比较的两个值的存储类型:
- 如果运算符左侧的值是NULL,那么这个值一般都会认为比其他的小。(如果其他的值中有NULL,也是这样)
- INTEGER或REAL比TEXT或BLOB值小。如果INTEGER或REAL和另一个INTEGER或REAL比较,那么就是常规的数字比较了。
- TEXT值比BLOB值小。如果两个TEXT值比较,那么就是使用标准C库的memcmp函数来比较。但是这个函数是可以被用户自定义的函数重写的。
- 当两个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|1SELECT 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返回的列的关联性,该组件在该位置具有列值(而不是其他某种表达式)