Sqlite WAL日志原理

个人学习笔记,有理解错误的请赐教,有没写清楚,或者原理上有漏掉比较重要的地方没写的,请大家留言,一起学习交流,谢谢

文件结构

WAL日志共有两个文件WAL文件和WAL-INDEX文件,WAL文件是记录redo日志的文件,WAL-INDEX文件是一个map-shared文件,用来加快检索WAL文件的日志

WAL文件

wal文件由一个文件头和多个frame组成,frame由frame头和数据页的修改组成。

结构图如下:
​​​​​​​​在这里插入图片描述

WAL-INDEX内存

哈希索引的目的是为了快速查找给定页对应的Frame序号。

WAL-INDEX多进程场景下使用map内存,单进程使用堆内存。WAL-INDEX内存由相同大小的索引块组成,索引块大小为32K。WAL-INDEX的头存放在第一个索引块内,所以第一个索引块存放索引的数量比其他索引块少。

Wal-Index头结构如下:
在这里插入图片描述
索引块分为两部分,一部分存放了page序号的数组(下标是frame序号),共4096个(第一个索引块为4062个);另一部分存放了一个HashTable(key是page序号,value是frame序号)。

整个文件的结构如下:
在这里插入图片描述
HashTable大小为4096 * 2 个 frame序号(第一个索引块除外),每个序号占2个字节,page序号占4个字节,因此一个索引块大小为4K * 2 * 2 + 4K * 4 = 32K。

基础原理

校验和CheckSum

校验和有两个值aCkSum0, aCkSum1, 初始值为0。

计算一段给定内存buf的checksum值算法为:

	aCkSum0 += aCkSum1 + *(uint32_t *)((uint32_t *)buf + i);
	aCkSum1 += aCkSum0  + *(uint32_t *)((uint32_t *)buf + i + 1);

因此buf的长度必须是8字节的倍数
在这里插入图片描述
Wal日志的校验和是累加计算,也就是说Frame1的aCksum0, aCksum1的初始值为0, Frame2的aCksum0, aCksum1的初始值是Frame1计算出来的校验和。

wal index的插入与查找

插入:
  1. 每个索引块存储的编号数量是固定的,根据frame序号可算出该frame序号所在的索引块
    在这里插入图片描述
   iHashBlock = (iFrameNo + HASHTABLE_NPAGE-HASHTABLE_NPAGE_ONE-1) / HASHTABLE_NPAGE
  1. 用pageNo进行Hash,计算出iHashKey = (iPage*HASHTABLE_HASH_1) % HASHTABLE_NSLOT;
  2. 如果哈希冲突,即HashTable[iHashKey] 已经有值,iHashKey = (iHashKey + 1) %HASHTABLE_NSLOT
  3. HashTable[iHashKey] = frameNo,PageNo[frameNo - StartFrameNo] = pageNo
查找:

通过页号pageNo查找最新的frame序号frameNo

  1. frameNo 搜索范围为[minFrame, maxFrame], minFrame为当前最小的没有执行checkpoint的frame序号,maxFrame为当前最大的frame序号。根据frameNo搜索范围确定搜索的wal-index块范围[iFirst, iLast]
  2. 从iLast索引块开始往前查找,计算pageNo的哈希值,得到iHashKey。
  3. 记frameNo = HashTable[iHashKey], 如果frameNo != 0 且 PageNo[frameNo - StartFrameNo] != pageNo, 说明哈希冲突,iHashKey = (iHashKey + 1) %HASHTABLE_NSLOT, 如果frameNo != 0 且 PageNo[frameNo - StartFrameNo] == pageNo,查找成功,返回
  4. 遍历所有范围内的索引块,都没找到对应的frame,说明该页在checkpoint后并没有写过frame日志,db文件的页已经是最新的

写日志

在这里插入图片描述

假设当前状态如图,写事务已经开始一段时间,已经修改过三个页,并产生了Frame1、Frame2、Frame3三个Wal日志Frame,此时pWal->mxFrame = Frame3 记为CurMaxFrame, 写事务开始时的maxFrame也就是索引内存块内WalIndexHdr1->mxFrame,记为TrxBeginMaxFrame。
现在有两种情况:

  1. 修改一个新页D,Wal日志只需要追加写在Frame3后,写一个新的Frame,同时将CurMaxFrame+1,并将对应的Frame4和PageD插入索引内。
  2. 修改一个旧页B,将新日志覆盖Frame2,同时记下被覆盖的最小序号的Frame序号,记为iReCksum。当事务提交时,从iReCksum 序号的Frame开始计算校验和,重新覆写Frame的校验和。(因为Wal是累加校验,当Frame2发生改变时,意味着Frame3在Frame2发生改变之前计算的CheckSum是不对的,需要重新计算)。

判断是旧页还是新页的依据:在[TrxBeginMaxFrame, CurMaxFrame]区间内存在着一个Frame,这个Frame记录的是该页的Wal日志。

扫描日志

Sqlite使用WalIterator结构体来辅助读取指定frame序号以后的frame,并且是按页号升序的方式读取frame。
walIteratorInit函数缓存了每个索引块的PageNo数组地址,并将PageNo数组归并升序排序。即WalIterator保存了多个升序的数组。
在这里插入图片描述
如图:从aIndex按序取出来的frame序号iFrame(相对值,真实值需要加startFrameNo),再从aPgNo取出来的页号是升序的。

比较每个Segment中aPgno[aIndex[iNext]],最小值即为本次需要取出来的pageNo, aIndex[iNext] + startFrameNo为本次取出来的frame号,取完值后将对应段的iNext ++。

CheckPoint

  1. 假设上一次执行checkpoint刷盘时的最大frame序号为nBackfill, 执行CheckPoint时的所有读事务时的可读的最小frame序号为mxSafeFrame, 那么本次刷盘的frame序号范围为[nBackfill+1, mxSafeFrame]
  2. 按页号升序的方式扫描Wal日志,如果frame序号范围在区间内,则读取页数据并刷盘到数据文件中。
    在这里插入图片描述

锁与并发

Wal总共有8个锁位,锁都存放在wal-index内。(详细请看
Wal支持一写多读同时并发,拥有一个写锁位,五个读锁位,其中Rd0锁位
在这里插入图片描述
锁保护的区域如下:
WAL_WRITE_LOCK:保护wal-index内的WalIndexHdr的并发,在写事务内全程加锁
WAL_CKPT_LOCK:保护checkpoint, 使得不同线程调用checkpoint只能串行执行
WAL_RECOVER_LOCK: 保护wal-index的恢复。
事实上walIndexRecover流程需要加三把锁WAL_WRITE_LOCK、WAL_CKPT_LOCK和WAL_RECOVER_LOCK。加写锁是为了防止在恢复过程中被写事务修改,加WAL_CKPT_LOCK锁也是为了防止被checkpoint修改。除了这两个流程会修改wal-index外,不会有其它流程修改。加WAL_RECOVER_LOCK锁是为了让读事务判断是不是在恢复过程中。
WAL_READ_LOCK:保护对应aReadMark的并发,在读事务内全程加锁。
WAL_READ_LOCK(0) 是个特殊的锁,意味着当前读事务会直接从Db文件中读取页,而不会从wal日志中读取页内容。因为开启读事务时所有页都已经刷入Db文件中 。

WalIndexHdr

  1. 开启读事务和写事务都需要从wal-index中拷贝WalIndexHdr到pWal中
  2. 写事务日志提交时会将pWal写回到wal-index中。
  3. 写回到wal-index中,先写第二个WalIndexHdr,再写第一个WalIndexHdr。开启读事务时,先读取第一个,再读取第二个,并判断是两个WalIndexHdr是否相同,相同才能开启读事务。
    因为sqlite是可以读写同时并发的,也就是说可能存在写事务在写WalIndexHdr到一半时,读事务开始读WalIndexHdr,这样就会导致读事务读取到的WalIndexHdr结构体不一致。

重置日志

将wal日志重置到初始值,长度截断,文件头也重新init。
必须满足没有读事务、其他进程/线程也没有写事务时才可以重置。
时机:checkpoint、写事务写日志时

结构体解析

WalIndexHdr:WAL-INDEX文件头

struct WalIndexHdr {
  u32 iVersion; // wal index 版本号
  u32 unused; // 未使用
  u32 iChange;                    /* Counter incremented each transaction */
  u8 isInit; // 1 表示已经wal index init过了
  u8 bigEndCksum;  // 是否是大端格式
  u16 szPage;  // 数据页大小(16字节最大值为64K-1,因此设定szPage = 1时表示为64K)
  u32 mxFrame; // 更新结构体WalIndexHdr时最大的合法frame序号
  u32 nPage; //已提交的数据库页个数, 提交时更新
  u32 aFrameCksum[2]; // 日志中最后一个frame的校验和
  u32 aSalt[2]; // 加盐值
  u32 aCksum[2]; // WalIndexHdr结构体的校验和
};

WalIndexHdr结构体在Wal-Index内存有两份,在Wal管理结构体中也有一份,

WalCkptInfo:checkpoint信息

struct WalCkptInfo {
  u32 nBackfill;                  /* 转换写到数据文件中的WAL Frame的数量 */
  u32 aReadMark[WAL_NREADER];     // 读mark位,开启读事务时写入当前最大frame号
  u8 aLock[SQLITE_SHM_NLOCK];     // 读事务锁
  u32 nBackfillAttempted;         /* 准备写到数据文件中的WAL Frame 序号*/
  u32 notUsed0; 
};

准备往DB文件中写入数据页时,将nBackfillAttempted 设置为当前可写的最大frame序号,写完后再将nBackfill设置为nBackfillAttempted。

Wal:Wal日志管理结构体

struct Wal {
	sqlite3_vfs *pVfs;         /* The VFS used to create pDbFd */
	sqlite3_file *pDbFd;       // db文件句柄
	sqlite3_file *pWalFd;      // wal文件句柄
	u32 iCallback;             /* Value to pass to log callback (or 0) */
	i64 mxWalSize;             // 当WAL文件大小超过mxWalSize,就对WAL文件进行截断。截断策略
	int nWiData;               // apWiData数据长度
	volatile u32 **apWiData;   // 存储wal-index各个索引块的首地址
	int szFirstBlock;          /* Size of first block written to WAL file */
	u32 szPage;                // 数据页大小
	i16 readLock;              // 加读锁的序号, -1 表示没人加读锁
	u8 syncFlags;              // sync wal文件头的flag
	u8 exclusiveMode;          // wal index的内存是使用堆内存还是map内存。(多进程模式使用map内存,单进程模式使用堆内存)
	u8 writeLock;              // true表示正在执行一个写事务
	u8 ckptLock;               // true 表示当前连接已经加了ckpt锁
	u8 readOnly;               // true 表示 wal只支持读事务,开启写事务时会报错
	u8 truncateOnCommit;       // true表示当事务提交时会对WAL文件进行截断
	u8 syncHeader;             // true 表示每次write Wal文件头后都会进行sync
	u8 padToSectorBoundary;    // wal 日志是否边界对齐
	u8 bShmUnreliable;         // 不可靠模式,使用的是堆内存去存储wal-index索引,当wal-index在当前连接不可写时触发
	WalIndexHdr hdr;           // 当前事务的wal-index头
	u32 minFrame;              // 没转换写入数据文件的最小frame序号
	u32 iReCksum;              // 提交时,frame序号从iReCksum开始,要重新计算checksum
	const char *zWalName;      // WAL 文件的文件名
	u32 nCkpt;                 // 日志绕回WAL文件开头的次数
};

每个连接会有一个wal结构体

WalIterator:扫描WAL日志时用的迭代器

struct WalIterator {
  u32 iPrior; // 上一次取出来的页号
  int nSegment; // Segment 个数,一个Segment 对应一个索引页
  struct WalSegment {
    int iNext;  // aIndex[iNext]之前的页号都被读走了
    ht_slot *aIndex; // 根据aPgno[aIndex[i]]得出来的页号是升序的
    u32 *aPgno; // 索引页page序号数组地址
    int nEntry; // 当前索引页page序号数组的长度
    int iZero; // 索引页的起始frame序号
  } aSegment[1];
};

WalWriter

typedef struct WalWriter {
  Wal *pWal;                  // wal
  sqlite3_file *pFd;          // wal 文件句柄
  sqlite3_int64 iSyncPoint;    // 当开启了边界对齐时,写日志写到边界对齐的地方会拆成两次写,并且在边界对齐的地方sync
  int syncFlags;               // fsync flag
  int szPage;                // 页大小
} WalWriter;

WalHashLoc

struct WalHashLoc {
  volatile ht_slot *aHash;  // 索引块中的哈希表地址
  volatile u32 *aPgno;      // 当前索引块的帧序号和页序号映射数组地址 
  u32 iZero;         // 当前索引页的帧编号和页编号映射的逻辑数组中的起始下标
};

索引块的描述结构体,记录了哈希表地址和页序号数组地址、当前索引块的frame序号偏移,即aPgno[index],所表示的是iZero + index的frame序号对应的页序号

主要函数及原理

黑色背景为对外接口,灰色背景为内部函数

sqlite3WalOpen

int sqlite3WalOpen(sqlite3_vfs *pVfs, sqlite3_file *pDbFd, const char *zWalName,  int bNoShm, i64 mxWalSize, Wal **ppWal)

作用:初始化一个WAL实例

参数含义
sqlite3_vfs *pVfssqlite的文件管理系统
sqlite3_file *pDbFd数据文件句柄
const char *zWalNameWal文件名
int bNoShmtrue选择堆内存模式,false选择map内存模式,详见Wal结构体字段exclusiveMode
int mxWalSizeWal文件的最大长度,详见Wal结构体字段mxWalSize
Wal **ppWal出参,初始化好的WAL实例

sqlite3WalBeginWriteTransaction/sqlite3WalEndWriteTransaction

Wal只支持一个写事务。

int sqlite3WalBeginWriteTransaction(Wal *pWal)int sqlite3WalEndWriteTransaction(Wal *pWal);

作用:开启/结束写事务

参数含义
Wal *pWalWal 实例

sqlite3WalBeginWriteTransaction 会加上一个写锁,并置pWal->writeLock为true,同时检查pWal->hdr和WAL-Index的写事务WalIndexHdr是否一致,不一致则报错。
sqlite3WalEndWriteTransaction 解锁并至pWal->writeLock为false

sqlite3WalBeginReadTransaction/sqlite3WalEndReadTransaction

int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged);
void sqlite3WalEndReadTransaction(Wal *pWal)

作用:开启/结束读事务

参数含义
Wal *pWalWal 实例
int *pChanged上一次读事务之后,数据库已经发生了改变

DML操作会先开启读事务,读取数据库的schema信息。
如果是建库后的第一次开启读事务还会对wal-index初始化,如果是开库后的第一次开启读事务就重建wal-index。
wal-index初始化和重建详见

sqlite3WalFrames

int sqlite3WalFrames(Wal *pWal, int szPage, PgHdr *pList, Pgno nTruncate, int isCommit, int sync_flags)

作用:数据页的修改写入WAL日志,如果isCommit为true时,会将事务进行阶段性的提交。(写事务可能会多次提交,sqlite3WalFrames会将近期的修改提交)。必须在开启写事务之后使用。

参数含义
Wal *pWalWal 实例
int szPage数据页大小
PgHdr *pList脏页列表
Pgno nTruncate当事务提交后数据页的数量
int isCommit是否提交
int sync_flagssync标志,当CKPT_SYNC_FLAGS(sync_flags)与pWal->syncHeader都为true时,更新了WAL文件头(第一次写frame时),会sync到文件中当WAL_SYNC_FLAGS(sync_flags)与isCommit为true时,。。。
  1. 遍历脏页列表,查找开启写事务后是否有写过相同页号的frame,没有的话追加写到wal文件中,有的话则覆写原来的frame
  因为checkSum是累加每个frame进行校验,所以覆写后的frame会导致该frame后的所有frame的校验和改变。在日志提交时,我们需要重新计算checkSum,用pWal->iReCksum来标记最小的被改变的frame序号
  1. 日志如果需要提交,则从 pWal->iReCksum开始读取日志并计算校验和,再重新写入每个frame的校验和。初始化校验和为iReCksum的上一个frame,没有的话用wal文件头上的checksum
  2. 如果需要扇区边界对齐,那么将最后一个frame帧,重复写,直到超过或等于扇区边界。
  3. 如果配置了truncateOnCommit, 对wal文件超出有效日志区域进行截断
  4. 将追加写的frame插入wal-index内存中。
       int walIndexAppend(Wal *pWal, u32 iFrame, u32 iPage);
       
       1. 根据frame序号找到对应的索引块,取得索引块的描述结构体WalHashLoc
       index = (iFrame+HASHTABLE_NPAGE-HASHTABLE_NPAGE_ONE-1) / HASHTABLE_NPAGE;
       2. 对页序号进行哈希,插入到哈希表中
       1> iKey = (iPage * 383 ) % (4096 - 1)
       2> 如果哈希冲突,即hash[iKey] 已被使用,那就往iKey + 1找,到边界时再从0开始找,直到找到空位
       3. 将页序号插入到索引块的页序号数组中,下标为 iFrame - iZero - 1
    
  5. 更新pWal->hdr.mxFrame,如果需要提交,还需要更新pWal->hdr.iChange/npage, 并更新wal index内存头的WalIndexHdr

sqlite3WalFindFrame

int sqlite3WalFindFrame(Wal *pWal, Pgno pgno,u32 *piRead);

作用:查找该页的最新的wal日志

参数含义
Wal *pWalWal 实例
Pgno pgno要查找的页序号
u32 *piRead出参,找到的frame序号,没找到为0(没找到说明该页在数据文件中已经是最新的),返回出错时,该值无效
  1. 确定搜索范围,从pWal->minFrame所在的索引页到pWal->hdr.mxFrame所在的索引页
  2. 在从最后的索引页中往前查找
	1> 先对pgno进行哈希,iKey = pgno * 383 % 4095。
	2> 如果aPgno[aHash[iKey] - 1] == pgno,记录frame序号(只保留最后搜索到的frame序号),继续向后搜索
	3> 如果aHash[iKey] == 0,当前索引页搜索结束。
	4> iKey = (iKey + 1) % 4095,继续执行2,3步
	5> 当前索引页没有搜索到,搜索前一个索引页,回到第一步

sqlite3WalReadFrame

int sqlite3WalReadFrame(Wal *pWal, u32 iRead, int nOut, u8 *pOut)

作用:读取iRead序号的frame中的页数据

参数含义
Wal *pWalWal 实例
u32 iReadframe序号
int nOut读取页数据的大小,从页头开始读
u8 *pOut出参,读取的数据
每个frame的大小是固定的,所以frameOffset = WAL_HDRSIZE + (iRead-1)*(pageSize+WAL_FRAME_HDRSIZE)
frameOffset + WAL_FRAME_HDRSIZE就是数据页的偏移

sqlite3WalUndo

int sqlite3WalUndo(Wal *pWal, int (*xUndo)(void *, Pgno), void *pUndoCtx)

作用:回滚当前写事务

参数含义
Wal *pWalWal 实例
xUndo由外部传入的函数,用于回滚pgno页
void *pUndoCtx传递给xUndo的参数
  1. 遍历当前写事务写过的frame序号,(从walIndexHdr->mxFrame到pWal->hdr->mxFrame)找到对应的页号,再调用xUndo函数进行还原
  2. 还原pWal->hdr到写事务开始时的状态,即拷贝walIndexHdr到pWal->hdr。
  3. 清除pWal->hdr->mxFrame后的wal index的记录。

sqlite3WalSavepoint/sqlite3WalSavepointUndo

void sqlite3WalSavepoint(Wal *pWal, u32 *aWalData);
int sqlite3WalSavepointUndo(Wal *pWal, u32 *aWalData);

作用:保存/回滚日志到还原点

参数含义
Wal *pWalWal 实例
aWalData还原点数据,4个uint32值分别是pWal->hdr.mxFrame,pWal->hdr.aFrameCksum[0], pWal->hdr.aFrameCksum[1], pWal->nCkpt

回滚时,还原当前pWal->hdr当前字段的对应四个字段,同时清除还原点以后的日志(frame > 保存的pWal->hdr.mxFrame)。
疑问:
在这里插入图片描述
假设在T1时刻,设置了还原点,之后修改了B页,导致B的Wal 日志进行了更新变成了B1,sqlite3WalSavepointUndo只能清除frame D、E,
并不能将B1 回滚到 B

sqlite3WalCheckpoint

int sqlite3WalCheckpoint(Wal *pWal, sqlite3 *db, int eMode, int (*xBusy)(void*), void *pBusyArg, int sync_flags, int nBuf, u8 *zBuf, int *pnLog, int *pnCkpt);

作用:wal frame转换成页数据写到数据文件中
约束:不能在写锁开启时调用,也不能在ckptLock被锁时调用

参数含义
Wal *pWalWal 实例
sqlite3 *db
int eModeSQLITE_CHECKPOINT_PASSIVE:被动式的,只刷页
SQLITE_CHECKPOINT_FULL:等待写事务完成,再刷页
SQLITE_CHECKPOINT_RESTART:等待写事务完成,并重头开始写日志
SQLITE_CHECKPOINT_TRUNCATE:等待读写事务完成,并truncate文件,然后重头开始写日志
除被动式外,其他都需要加写锁
xBusy繁忙时调用的函数
void *pBusyArg传递给xBusy的参数
int sync_flagssync参数
int nBuf缓存zBuf的大小
u8 *zBufcheckpoint 要用到的缓存
int *pnLogwal日志里frame的个数
int *pnCkpt回写到Db文件的frame个数

1.根据eMode判断是不是要加写锁
2.读取ckptInfo->aReadMark数组与pWal->hdr.mxFrame,取最小值,记mxSafeFrame,[nBackFilll, mxSafeFrame]就是这批要刷的frame
3.扫描日志,按页号升序刷页进db
4.如果eMode是SQLITE_CHECKPOINT_TRUNCATE,截断WAL文件
5.如果eMode是SQLITE_CHECKPOINT_RESTART或SQLITE_CHECKPOINT_TRUNCATE,需要重置WAL文件头。

Wal Index的初始化和重建

int walIndexRecover(Wal *pWal);

作用:初始化或重建wal index。

参数含义
Wal *pWalWal 实例

初始化

初始化Wal结构体的isInit、iVersion字段,然后计算WalIndexHdrT结构体的checkSum(存放于aCksum字段),同时拷贝到wal-index的两个WalIndexHdrT内存中。
walIndexRecover 是根据Wal文件的大小是否大于WAL_HDRSIZE来判断是初始化还是重建,小于则初始化。

重建

  1. 读取 Wal文件头,对文件头的校验和进行校验。
  2. 计算Wal Index页的个数, 逐页进行恢复。
    根据Wal文件大小可以算出frame的个数,根据frame的个数再算出有多少个Wal index页
    	 LastFrameIndex = (fileSize - WAL_HDRSIZE) / frameSize
    	 LastIndexPage = (LastFrameIndex  + HASHTABLE_NPAGE-HASHTABLE_NPAGE_ONE-1) / HASHTABLE_NPAGE
    	 HASHTABLE_NPAGE:除第一个索引块外的其他索引块存放frame序号的个数,值为4096
    	 HASHTABLE_NPAGE_ONE:第一个索引块存放的frame序号个数,值为 4062
    
  3. 计算Wal Index页存放的frame序号范围。
     iFirstFrame = 第一个Index页为1,其他:HASHTABLE_NPAGE_ONE+(索引页序号-1) * HASHTABLE_NPAGE
     iLastFrame = 最后一个页为LastFrameIndex, 其他HASHTABLE_NPAGE_ONE+索引页序号 * HASHTABLE_NPAGE
    
  4. 遍历读取iFirstFrame到iLastFrame wal日志,解码后得到frame序号和page序号,插入到Wal Index 中。
  5. 将pWal中的hdr拷贝到索引块的两个WalIndexHdr内存中
  6. 重置CkptInfo结构体字段nBackFill、nBackfillAttempted、aReadMark数组

walIteratorInit/walIteratorNext/walIteratorFree

int walIteratorInit(Wal *pWal, u32 nBackfill, WalIterator **pp);
int walIteratorNext(WalIterator *p, u32 *piPage, u32 *piFrame);
void walIteratorFree(WalIterator *p);

作用:扫描frame nBackfill以后的的frame,按照页序号升序的方式读出页序号和frame序号

参数含义
Wal *pWalWal 实例
u32 nBackfill扫描frame nBackfill以后的的frame
WalIterator *p迭代器
u32 *piPage出参,页号
u32 *piFrame出参,frame 号

WalIteraor 内存结构图
在这里插入图片描述

walIteratorInit时,申请iterator内存,并将各个段的页号,按照升序的方式排序,用p->aSegment[x].aPgno[p->aSegment[x].aIndex[i]]读出来是升序的
walIteratorNext 比较每个Segment 中aPgno第一个没被读出来过的页号,取最小值返回

sqlite3WalDbsize

Pgno sqlite3WalDbsize(Wal *pWal);

作用:返回数据库数据页个数,存储在pWal->hdr->nPage

参数含义
Wal *pWalWal 实例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值