SQLite内存使用情况分析
sqlite的一个显著的特点就是占用内存量很小,这作为一个嵌入式的DBMS是非常重要的,那么我下面就对这个问题从根本上分析它是如何做到“小内存”的。
一、ORDER BY查询中内存使用情况
由于sqlite的执行都是先把sql语句转化成指令再执行,所以下面就先一条条的分析一下它所用到的指令。先建立一个表再插入下面的数据,然后再用explain操作来得到那些指令。下面就是它们的具体操作以及解释。
sqlite> create table stu (sno int,name text,sex text,age int);
sqlite > insert into stu values (1,'aa','n',21);
sqlite > insert into stu values (2,'bb','m',18);
sqlite > insert into stu values (3,'yy',17);
sqlite > insert into stu values (4,'xx',19);
sqlite > insert into stu values (5,'ee',20);
sqlite > insert into stu values (5,24);
sqlite > insert into stu values (6,'fe',34);
sqlite > explain select * from stu order by age;
下面是制作一个record的过程。
下面是record5的数据结构,也就是执行第十条指令后的结果:(记录1)
Hdr-size |
Int |
Text |
Text |
Int |
Sno |
Name |
Sex |
age |
再执行11-14条指令后得到的结果为:(记录2)
@H_502_138@Hdr-size
Int
Text
Text
age
sequence
Record5
再执行第15条指令是将最后得到的一个record插入到临时数据库文件1中,也就是执行Open Ephemeral后得到的数据文件。就象上面一直循环将所有要排序的数据插入到临时数据库中。
再执行第19条指令,打开的是个临时表,这个表的特点就是这个表中只有一条记录,每当插入第二条记录第一条就会自动被删除,这样可以节约内存的使用。
再执行第21条指令,这是从数据库1中得到第3列放到amem[]第5个位中,也就是取出上面记录2中的Record5值,也就是记录1,放到第五位。
再执行第22条指令,它是把一个整数值(代表一个键值)放到第九位。
再下来执行第23条指令,它是将amem第5位中的数据(记录1)和第9位的键值取出来插入到表2中,此时得到的数据就是原来的数据。
再下来的指令就是从这条指令中把数据取出做下面的工作,比如输出或者是计数等操作。
这里实现排序功能的就是IdxInsert指令和,因为它是将得到的带有排序关键字的记录插入到B树的合适位置,再执行第21条指令的时候一条条的把数据取出来,这样就是有序的数据了。
而对于内存的使用来说,也就是在这个问题上,因为其它操作都是在计算,不用什么内存的,而它用内存就是看它是把数据存放到什么地方了,很明显它是存在数据库1中的,那么问题就是说数据库1的建立是的是在磁盘上还是内存中,也就是指令OP_OpenEphemeral的执行情况:
allocateCursor();//先给它分配游标
再下来创建数据库:
sqlite3BtreeFactory (db,1,sqlITE_DEFAULT_TEMP_CACHE_SIZE,openFlags,&pCx->pBt);
在这个函数中会判断它是创建什么类型的数据库,这里是用第二个参数决定类型的,如果不为空且是个具体的名字,那么就调用sqlite3BtreeOpen()函数打开即可,如果是":memory:",那么直接在内存中建立,如果为0,说明就是虚数据库,可以是在内存也可以是数据库文件,这会决定于两个条件:
sqlITE_TEMP_STORE和db->temp_store==2,下面是它们的决定方式:
这说明这个排序也不会给内存使用带来很大的影响。
下面再根据代码分析它执行IdxInsert的内存使用情况:
它也是执行函数:sqlite3BtreeInsert(pCrsr,zKey,nKey,"",pOp->p3);只是它在这里不插入数据只插入关键字Key,这个zKey其实就是上面所说的记录2,执行这个函数过程中,首先它申请临时空间,调用函数allocateTempSpace(pBt),这个操作申请到的空间一般是固定的,为1024B,也就是一个页面的大小,随着page_size变化。这1K内存就是它执行排序时比其它不排序的多出来的内存使用量。但是这个TempSpace对于一个Btshared对象来说只有一个,当申请后再就不能申请了。
上面那个插入操作函数再调用fillInCell(),这里是真正的将数据插入到临时数据库中的操作,首先是将数据插入到申请到的临时内存空间中,如果空间用完就再从磁盘中申请页面来存储溢出数据。
二、下面再具体算一下总共所用的3M内存用到何处了。
1) 首先打开一个数据库自动要分配2000个页面,也就是2000K
2) 再下来执行指令Open Ephemeral,内存分配情况如下图:
3) 再就是OpenRead指令:这里也是为主数据库分配一个游标,大小为300K
4) 再下来执行Idxinsert要分配1024B的临时内存空间
5) 执行OpenPseudo指令打开一个游标也要300B的空间
共使用2502K空间
从上面可以看出,其实内存空间的应该只是它自身的一些初始分配,对于这个操作,额外的应用是很少的,2502K这个数和经测试的3M也相差不多。
三、再下来讨论一下Pager是如何来管理页面的
在打开数据库的时候首先会把Pager的页面数设置成2000个,执行函数sqlite3_open()的时候它会调用openDatabase()函数,再调用sqlite3BtreeFactory(),这个函数的一个参数在这里是用了一个默认值sqlITE_DEFAULT_CACHE_SIZE,它是2000,这是Pager的最大的页面数,再通过sqlite3BtreeSetCacheSize(*ppBtree,nCache)设置这个最大页面数,接下来应用内存的操作是在遍历数据库的时候,首先执行指令OP_Rewind,它会找到要查询的B树的第一个记录,也同时把第一个页面调入内存,通过执行getAndInitPage(pBt,pCur->pgnoRoot,&pCur->apPage[0])操作,这样再继续执行下面的指令来取数据以及对数据进行处理,等到循环第二次的时候执行OP_next指令,它其实是要找到下一条数据,首先判断此数据的索引值是否可以在现在游标所处的页面的内存单元之外,如果是那么说明在这个页面中已经找不到,那么就再在内存中找这个页面,因为这个页面有可能因为以前的操作已经把它放入内存中了,如果能找到就返回这个页面指针,如果找不到的话再去已经闲置不用的队列中找合适的页面来用,如果能找到就用这个,如果找不到就再新建一个页面。
那么再下来对上面得到的页面进行初始化:数据就是在数据库文件中到需要的页面并且将这个页面的数据读到刚才得到的新页面中,这样就完成了一次数据的搜索,每次执行OP_next指令的操作都是这样,如此反复直到读完数据为止……
四、多表连接的内存使用情况分析
多表连接的做法已经在前面查询优化中讲过了,至于它的内存使用其实和普通的查询基本是一样的,只不过是多了几个游而已,有几个表连接就打开几个对应的游标,再另加一个用于输出的游标,所以内存和普通查询基本相同,这里不再叙述。
五、建立索引的内存使用情况。
索引的建立是将数据生成关键字再把它们插入到B树的合适位置中去,B树是存储在文件中的,所以使用内存的数量和前面讨论过的排序中执行指令idxinsert是相同的,所以内存使用也是很小,这里不再叙述。
六、插入操作的内存使用情况
插入操作分两种情况,一种就是自动提交,这样就是执行一次就提交一次,这个过程中,存在内存中的数据量不会很大,所以插入操作使用内存情况就是一条数据的大小。
而如果是将插入放到BEGIN和COMMIT语句之间的话,那么这就是手动提交,中间执行的操作都会保存在内存中,插入多少条保存多少的数据,这很明显占内存量就是很大了。与在这两个语句中插入的数据量成正比。
七、更新操作的内存使用情况
更新操作使用内存比较大,因为在sqlite中,更新会采用两个步骤进行,第一步先是把满足条件的数据找出来存到一个RowSet的内存单元中,对数据遍历完一遍后再将这些数据从RowSet中取出来,再一条条的更新,所以数据多的话使用内存就会很大,其中放到RowSet中的指令为OP_RowSetAdd,它会执行一个插入函数sqlite3RowSetInsert(),在这个函数中要对内存空间进行管理,如果还有空间就继续插入,没有空间就再申请。
八、删除操作的内存使用情况
删除操作使用内存比较大,因为在sqlite中,删除会采用两个步骤进行,第一步先是把满足条件的数据找出来存到一个RowSet的内存单元中,对数据遍历完一遍后再将这些数据从RowSet中取出来,再一条条的删除,所以数据多的话使用内存就会很大,其中放到RowSet中的指令为OP_RowSetAdd,它会执行一个插入函数sqlite3RowSetInsert(),在这个函数中要对内存空间进行管理,如果还有空间就继续插入,没有空间就再申请。 原文链接:https://www.f2er.com/sqlite/199578.html