介绍了rowid的应用和查询效率,讨论了rowid表和无rowid表的优缺点,并对两者的结构和效率进行了对比和总结。
rowid是什么
在SQLite的数据库中,大多数的表都被称为传统表(也叫rowid表),特点是拥有一个唯一的数值键rowid。rowid列是隐含的,如果没有显式定义主键或其他唯一索引,SQLite会自动创建和管理ROWID列。rowid是自增长的64位的integer类型,通过rowid查询会比普通的主键或者索引快两倍。
下面的语句创建一张Fruits表,SQLite会自动创建一个隐藏的ROWID列:
SQL
CREATE TABLE Fruits (name TEXT, color TEXT, price REAL);
INSERT INTO Fruits (name, color, price) VALUES ('Orange', 'Orange', 4);
INSERT INTO Fruits (name, color, price) VALUES ('Apple', 'green', 4.2);
INSERT INTO Fruits (name, color, price) VALUES ('Lemo', 'yellow', 1.2);
INSERT INTO Fruits (name, color, price) VALUES ('Apple', 'red', 7.9);
通过select * from Fruits;
查看会发现并不能获取rowid这列数据,因为rowid是隐藏列。必须显式的指定select rowid, * from Fruits
才可以获取rowid列。
如果为某个单列声明为主键,且该列的数据类型为INTEGER (INTEGER不区分大小写),则在SQLite内部,将该列名作为一个新的rowid别名,如下fid便是rowid的别名,和rowid是等效的。
SQL
CREATE TABLE Fruits2 (fid INTEGER PRIMARY KEY, name TEXT, color TEXT, price REAL);
rowid的作用
假设在上面创建的Fruits表中有8条数据,杂乱无序的放在数据库中。我们如何用最快的速度通过指定的水果名字找到想要的水果呢?我们唯一能做的就是遍历每一个水果,检查他们的名字,直到找到了我们想要的名字为止。那么在最坏的情况下,我们就要访问8条记录,所以此次查找的时间复杂度就是O(N)。
假设能够给每一条数据提供一个数值类型的ID,能够保证ID是顺序排列的。此时再让我们通过序号找到指定的水果,我们就可以通过2分查找法的方式进行查找。在有八条数据的情况下,找到任何一条数据都需要访问三次。所以像这样去查找的时间复杂度就是O(log n) 。很明当数据量较大时O(log n)的查询效率提升很大。rowid便是这样的ID。
rowid表的查询原理
在SQLite数据库中,rowid表本质上的存储结构都是通过B+树来完成的。
1、通过主键rowid查询O(log n)
B+树是一种多路平衡查找树,它的特点是非叶子节点只存键,叶子节点才存储值。这里的键指的就是真正的主键,也就是我们的rowid。在这种多路平衡查找树中,查找数据的时间复杂度就是O(log n) 。所以当我们通过rowid去查询数据的时候,就相当于根据键去直接搜索B+树。所以时间复杂度也是O(log n)。
2、通过自定义主键查询O(2log n)
SQL
//这里创建的表指定fid(TEXT类型)作为主键,但是rowid才是真正的主键,所以fid其实只是一个唯一索引
CREATE TABLE Fruits (fid TEXT PRIMARY KEY, name TEXT, color TEXT, price REAL);
INSERT INTO Fruits (fid, name, color, price) VALUES ('Orange_fid', 'Orange', 'Orange', 4);
select * FROM Fruits WHERE fid = "Orange_fid";
在SQLite中,索引的实现方式其实就是普通的B树。
B树同样是一种多路平衡查找树,它的特点就是键和值都存储在所有的节点上。作为索引的话,值指的肯定就是要索引的列的内容。由于B树从根结点到叶子结点都能够存放数据,所以查询B树的时间复杂度就是O(log n)。
所以上述语句通过自定义主键fid查询的情况就被分解成了两步。
ini
1. 首先,如果使用B树找到fid = "Orange_fid" 的rowid
2. 使用rowid 搜索B+树找到记录
在经历了两次搜索之后,时间的复杂度就变成了O(2log n)。
3、通过非主键查询O(2log n)
SQL
select * FROM Fruits WHERE name = "Orange";
如果查询条件既不是rowid又不是主键,像这种情况的话,不管是B树还是必B树都没有发挥作用,只能通过遍历表中的每一条数据的方式来查询。所以时间复杂度就变成了O(n)。不过如果name字段是我们经常要作为查询条件的字段,最好的方式还是手动的为这个字段创建一个索引。这样一来,时间复杂度就重新的变成了O(2log n)。
在开发中也会遇到这样一种情况,那就是搜索索引之后得到的结果不止一条。那么在这种情况下就要根据索引的结果多次查询rowid表的B+树。这个时候时间复杂度就变成了O((X+1)log n),X就等于索引值在表中的重复次数。在大多数的情况下,只要X的值不是太大,所以查询效率肯定还是要高于直接遍历B树。
rowid表的缺点
- 只有通过rowid来查询,时间复杂度才能够达到最优的O(log n),如果业务上不能够提供数值类型的主键,rowid一般都是通过自动生成和自动增长的方式来实现的,那么业务代码就很难提前拿到rowid并且作为查询条件。
- 当我们用非rowid的主键去查询的时候,时间复杂度会变成O(2log n),也就是会慢上一倍。
- 每创建一个索引就要额外的存储。大量的记录会额外的浪费很多的存储空间。
为了优化这些问题,我们就可以使用无rowid表。
without rowid表
without rowid表指的就是我们通过WITHOUT ROWID
关键字创建出来的表,这种表不会自动添加rowid数值键,同时每个without rowid表都必须声明PRIMARY KEY。如
SQL
CREATE TABLE Fruits (fid int PRIMARY KEY,
name TEXT,
color TEXT,
price REAL
)WITHOUT ROWID;
sql
1、必须自己指定一个非INTEGER类型的主键或者联合主键。
2、主键的一列必须不能为空。
3、不能够使用自动增长机制。
WITHOUT ROWID
关键字创建出来的表我们所设置的主键就变成了真正的主键,它的实现原理是聚合索引。无rowid表的存储结构是B Tree,不再是B+Tree。
因为without rowid表主键的索引不再是简单的唯一性索引,而是真正的聚合索引。也就是说我们只要能够找到主键,就能够立刻的找到数据。
由于存储结构从B+Tree变成了B Tree,查询的时间复杂度也变成了O (log n)。
关于rowid表和无rowid表,我们还可以从结构上做一下对比。其实也就是B Tree和B+Tree的区别。除了我们刚刚说过的查询时间复杂度上的区别,它们之间还有一个区别,就是B Tree的单行数据会影响查询效率。因为数据是在每个节点上都存储的,节点越大查询效率也会变得越低。那么B+Tree就不存在这个问题。因为所有的数据都只在叶子节点上当我们通过非叶子的节点去进行查询的时候,直到获取到数据之前,单行数据的大小都不会影响查询效率。
总结
无rowid表的使用场景:
1、业务能够提供一个非INTEGER类型的主键或者是组合主键,而不是需要数据库来提供自动增长的主键。
2、无rowid表不适合存储单行数据过大的数据。原因就是单行数据过大的时候,B Tree会影响查询效率。所以无rowid表不适合存储大字符串和BLOBs(二进制大对象)类型。另外一点就是当单行数据不超过1/20数据库页大小的时候,无rowid表能够获得最佳的性能。这个数据来自SQLite的官方文档。
即使用无rowid表可以在特定的场景下让主建的查询效率提升一倍,并且能够节约大量的主键的索引相关的字段的存储。