SQLite源码分析8 索引#

数据和索引到底是怎么存在磁盘的?#

  • 一般默认会创建表b树,直接存数据。

  • b树本身肯定不能存所有数据,因为数据可能巨大。
    b树只作为一个架子,存所有数据的key,每个节点只有一页数据,其他大量的实际数据的页号另存成链表,放在overflow页。

  • b树节点的子节点关系都是存在磁盘的,也就是b树的结构都是存好在文件里的。

  • 实际数据只有一个副本。架子里的节点指向对应的数据副本。
    其他index树的节点可以说都是指向这些唯一的数据副本。

  • index树必须基于实际数据来创建。当数据发生改变时必须更新相关的index树。

观察create index的流程。

sqlite> explain create index i1 on t2(data);
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     33    0                    00  Start at 33  
1     Noop           0     32    0                    00               
2     CreateBtree    0     1     2                    00  r[1]=root iDb=0 flags=2
3     OpenWrite      0     1     0     5              00  root=1 iDb=0; sqlite_master
4     String8        0     3     0     index          00  r[3]='index' 
5     String8        0     4     0     i1             00  r[4]='i1'    
6     String8        0     5     0     t2             00  r[5]='t2'    
7     SCopy          1     6     0                    00  r[6]=r[1]    
8     String8        0     7     0     CREATE INDEX i1 on t2(data)  00  r[7]='CREATE INDEX i1 on t2(data)'
9     NewRowid       0     2     0                    00  r[2]=rowid   
10    MakeRecord     3     5     8     BBBDB          00  r[8]=mkrec(r[3..7])
11    Insert         0     8     2                    18  intkey=r[2] data=r[8]
12    SorterOpen     3     0     1     k(2,,)         00               
13    OpenRead       1     6     0     2              00  root=6 iDb=0; t2
14    Rewind         1     20    0                    00               
15      Column         1     1     10                   00  r[10]=t2.data
16      Rowid          1     11    0                    00  r[11]=rowid  
17      MakeRecord     10    2     9                    00  r[9]=mkrec(r[10..11])
18      SorterInsert   3     9     0                    00  key=r[9]     
19    Next           1     15    0                    00               
20    OpenWrite      2     1     0     k(2,,)         11  root=1 iDb=0 
21    SorterSort     3     26    0                    00               
22      SorterData     3     9     2                    00  r[9]=data    
23      SeekEnd        2     0     0                    00               
24      IdxInsert      2     9     0                    10  key=r[9]     
25    SorterNext     3     22    0                    00               
26    Close          1     0     0                    00               
27    Close          2     0     0                    00               
28    Close          3     0     0                    00               
29    SetCookie      0     1     5                    00               
30    ParseSchema    0     0     0     name='i1' AND type='index'  00               
31    Expire         0     1     0                    00               
32    Halt           0     0     0                    00               
33    Transaction    0     1     4     0              01  usesStmtJournal=1
34    Goto           0     1     0                    00
  • 先是往sqlite_master表插入新的index的信息。
    再可以看到它会遍历所有t2.data,把每条t2的data作为key,t2的rowid作为数据拿出来,排序。
    再一个个插入新建的索引b树。

  • 这样看,index树和表b树本质是差不多的,都是以一个key确定b树的结构。
    只不过表b树用的key是默认的rowid,index树的key是用户指定。

  • 如果数据量很大,每个index(对应每棵b树)都会占据很大的空间。在数据量很大的表上操作索引也很耗时。
    这和平时使用同类型数据库观察到的现象是一致的。

创建索引后可以explain一些查询,观察b树的使用情况。
select * from sqlite_master;可以看到数据库所有表和index,和对应的b树。

第一列说明是索引还是表。
第二列是索引或表的名字。
第三列是所在的表名字。
第四列是对应的b树的根页号,对应explain里的root=xxx,这样就能看到用的是哪个b树。
第五列是创建信息。

sqlite> SELECT * FROM sqlite_master;
table|tbl1|tbl1|2|CREATE TABLE tbl1(one varchar(10), two smallint)
table|xcc|xcc|3|CREATE TABLE xcc(id int primary key not null)
index|sqlite_autoindex_xcc_1|xcc|4|
table|sqlite_stat1|sqlite_stat1|5|CREATE TABLE sqlite_stat1(tbl,idx,stat)
table|t2|t2|6|CREATE TABLE t2(id int, data int)
index|i1|t2|8|CREATE INDEX i1 on t2(id)
index|i2|t2|9|CREATE INDEX i2 on t2(id, data)
table|t3|t3|7|CREATE TABLE t3(id int, d1 int, d2 int)
index|t3_i2|t3|10|CREATE INDEX t3_i2 on t3(id, d1)
index|t3_i3|t3|11|CREATE INDEX t3_i3 on t3(id, d1, d2)

对比一下创建index前后,进行数据查找的opcode。

sqlite> explain select * from t2 where data > 1000;
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     10    0                    00  Start at 10  
1     OpenRead       0     6     0     2              00  root=6 iDb=0; t2
2     Rewind         0     9     0                    00               
3       Column         0     1     1                    00  r[1]=t2.data 
4       Le             2     8     1     (BINARY)       54  if r[1]<=r[2] goto 8
5       Column         0     0     3                    00  r[3]=t2.id   
6       Column         0     1     4                    00  r[4]=t2.data 
7       ResultRow      3     2     0                    00  output=r[3..4]
8     Next           0     3     0                    01               
9     Halt           0     0     0                    00               
10    Transaction    0     0     4     0              01  usesStmtJournal=0
11    Integer        1000  2     0                    00  r[2]=1000    
12    Goto           0     1     0                    00               

可以看到是不断在比较并Next,依次比较cursor的下一个数据,如果<=1000,看下一个数据,否则输出当前数据,再看下个数据。这样是强行遍历所有原始数据来查找。

sqlite> create index i1 on t2(data);
sqlite> explain select * from t2 where data > 1000;
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     11    0                    00  Start at 11  
1     OpenRead       0     6     0     2              00  root=6 iDb=0; t2
2     OpenRead       1     7     0     k(2,,)         00  root=7 iDb=0; i1
3     Integer        1000  1     0                    00  r[1]=1000    
4     SeekGT         1     10    1     1              00  key=r[1]     
5       DeferredSeek   1     0     0                    00  Move 0 to 1.rowid if needed
6       Column         0     0     2                    00  r[2]=t2.id   
7       Column         1     0     3                    00  r[3]=t2.data 
8       ResultRow      2     2     0                    00  output=r[2..3]
9     Next           1     5     0                    00               
10    Halt           0     0     0                    00               
11    Transaction    0     0     5     0              01  usesStmtJournal=0
12    Goto           0     1     0                    00   

创建index后,可见新的index的b树根页号root=7。默认数据表的根页号是6。

搜索用的是index树,效率提高。


联合索引的原理?#

sqlite> explain create index t3_i2 on t3(id, d1);

14    Rewind         1     21    0                    00               
15      Column         1     0     10                   00  r[10]=t3.id  
16      Column         1     1     11                   00  r[11]=t3.d1  
17      Rowid          1     12    0                    00  r[12]=rowid  
18      MakeRecord     10    3     9                    00  r[9]=mkrec(r[10..12])
19      SorterInsert   3     9     0                    00  key=r[9]     
20    Next           1     15    0                    00    
  • 单个索引把一个字段组成一条数据插入索引b树。

  • 联合索引把多个字段组成一条数据插入索引b树。多个字段组成数据并不会进行什么特殊处理,只相当于依次排开,使两条数据种的列能依次进行比较。

观察最简单的例子 where d1 > 1000 and id > 9

可以看到不创建联合索引,而创建单个索引,会走单个索引。

创建联合索引,会走联合索引,但也只会搜一列,再遍历所有结果,比较另一列。

sqlite> explain select * from t3 where d1 > 1000 and id > 9;
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     14    0                    00  Start at 14  
1     OpenRead       0     7     0     3              00  root=7 iDb=0; t3
2     OpenRead       1     10    0     k(3,,,)        00  root=10 iDb=0; t3_i2
3     Integer        9     1     0                    00  r[1]=9       
4     SeekGT         1     13    1     1              00  key=r[1]     
5       DeferredSeek   1     0     0                    00  Move 0 to 1.rowid if needed
6       Column         1     1     2                    00  r[2]=t3.d1   
7       Le             3     12    2     (BINARY)       54  if r[2]<=r[3] goto 12
8       Column         1     0     4                    00  r[4]=t3.id   
9       Column         1     1     5                    00  r[5]=t3.d1   
10      Column         0     2     6                    00  r[6]=t3.d2   
11      ResultRow      4     3     0                    00  output=r[4..6]
12    Next           1     5     0                    00               
13    Halt           0     0     0                    00               
14    Transaction    0     0     28    0              01  usesStmtJournal=0
15    Integer        1000  3     0                    00  r[3]=1000    
16    Goto           0     1     0                    00

可以看到搜索的key用的是id>9,那么问题来了。只有一棵索引b树,搜了id>9,如何同时用b树搜d1>1000?
结果是做不到,至少不能有效地做到。只能定一些规则,尽量利用索引。

  • 联合索引并不是简单拿几列定义一下就大功告成了。并不能支持随意搜任何列组合。

  • 联合索引是有顺序区分的,查找时会按从左到右的顺序来。
    比如create index on t(a, b),这时如果只查询b是走不了索引的,必须包含a。
    因为创建index树时的插入操作就是先比较a,如果a相等再比较b和后面的列。
    就像字典一样,必须从左到右依次来查,如果跳过一个字,就属于无脑瞎找。

从代码来看#

排序、创建索引、b树查找数据用的比较函数都是

sqlite3VdbeRecordCompare
    sqlite3VdbeRecordCompareWithSkip
        依次比较两条数据的列1, 列2, 列3, 列4...
  • 例如创建时按a,b,c三列MakeRecord,按上面的比较函数插入b树。

  • 搜索时可以搜a,ab或abc。只能依次增加,后面可以缺,中间不能跳过,否则不符合插入的比较规则。

总结的一些规则#

(仅限于sqlite。不确定其他同类数据库的情况。不一定完整。比如可能会有b树里再套b树,这样可实现随意查多列。)

对于联合索引(a, b, c):

  1. 查找时必须包含a。否则一定不会走联合索引。

  2. 包含a的大于或小于查询。where a < 100 and b > 0 ...;
    会以a作为key查b树。再遍历所有查a的结果数据,每条数据判断b或c的情况。
    这个情况其实和用单个a的索引相似,区别在于联合索引的key值都在b树节点,可以直接比较。而用单个索引的话,如果需要比较b或c,必须另行读出实际数据,会增加大量磁盘io。

  3. 包含a的等于查询,且没查b。where a = 100 and c < 666 and d > 5;
    同上,以a作为key查b树,再遍历查其他列。

  4. 包含a的等于查询,b的大于小于查询。where a = 100 and b > 0。
    以(a, b)作为key查b树。

  5. 用什么key去查b树很重要,key里包含的信息越多越好。
    上一条可以扩展到更多列的联合索引。比如 where a = 100 and b = 200 and c > 0 会以(a, b, c) 作为key查b树。只要出现依次的等于,就可以把更多的列加到key里。限定越多,就越能利用b树,后续的遍历就会越少。
    最好的情况就是每个列都查相等,那就不需要任何遍历,b树一次就能搜到。
    所以创建联合索引时,要尽量把查等于的列放左边。