SQLite剖析之事務處理技術
前言
事務處理是DBMS中最關鍵的技術,對SQLite也一樣,它涉及到併發控制,以及故障恢復等等。在資料庫中使用事務可以保證資料的統一和完整性,同時也可以提高效率。假設需要在一張表內一次插入20個人的名字才算是操作成功,那麼在不使用事務的情況下,如果插入過程中出現異常或者在插入過程中出現一些其他資料庫操作的話,就很有可能影響了操作的完整性。所以事務可以很好地解決這樣的情況,首先事務是可以把啟動事務過程中的所有操作視為事務的過程。等到所有過程執行完畢後,我們可以根據操作是否成功來決定事務是否進行提交或者回滾。提交事務後會一次性把所有資料提交到資料庫,如果回滾了事務就會放棄這次的操作,而對原來表的資料不進行更改。
SQLite中分別以BEGIN、COMMIT和ROLLBACK啟動、提交和回滾事務。見如下示例:
@try{ char *errorMsg; if (sqlite3_exec(_database, "BEGIN", NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@”啟動事務成功”); sqlite3_free(errorMsg); sqlite3_stmt *statement; if (sqlite3_prepare_v2(_database, [@"insert into persons(name) values(?);" UTF8String], -1, &statement, NULL)==SQLITE_OK) { //繫結引數 const char *text=[@”張三” cStringUsingEncoding:NSUTF8StringEncoding]; sqlite3_bind_text(statement, index, text, strlen(text), SQLITE_STATIC); if (sqlite3_step(statement)!=SQLITE_DONE) { sqlite3_finalize(statement); } }if (sqlite3_exec(_database, "COMMIT", NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@”提交事務成功”); } sqlite3_free(errorMsg); } else{ sqlite3_free(errorMsg); } } @catch(NSException *e){ char *errorMsg; if (sqlite3_exec(_database, "ROLLBACK", NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@”回滾事務成功”); } sqlite3_free(errorMsg); } @finally{ }
在SQLite中,如果沒有為當前的SQL命令(SELECT除外)顯示的指定事務,那麼SQLite會自動為該操作新增一個隱式的事務,以保證該操作的原子性和一致性。當然,SQLite也支援顯示的事務,其語法與大多數關係型資料庫相比基本相同。見如下示例:
sqlite> BEGIN TRANSACTION; sqlite> INSERT INTO testtable VALUES(1); sqlite> INSERT INTO testtable VALUES(2); sqlite> COMMIT TRANSACTION; --顯示事務被提交,資料表中的資料也發生了變化。 sqlite> SELECT COUNT(*) FROM testtable; COUNT(*) ---------- 2 sqlite> BEGIN TRANSACTION; sqlite> INSERT INTO testtable VALUES(1); sqlite> ROLLBACK TRANSACTION; --顯示事務被回滾,資料表中的資料沒有發生變化。 sqlite> SELECT COUNT(*) FROM testtable; COUNT(*) ---------- 2
Page Cache之事務處理——SQLite原子提交的實現
下面通過具體示例來分析SQLite原子提交的實現(基於Version 3.3.6的程式碼):
CREATE TABLE episodes( id integer primary key,name text, cid int); insert into episodes(name,cid) values("cat",1); --插入一條記錄
它經過編譯器處理後生成的虛擬機器程式碼如下:
sqlite> explain insert into episodes(name,cid) values("cat",1); 0|Trace|0|0|0|explain insert into episodes(name,cid) values("cat",1);|00| 1|Goto|0|12|0||00| 2|SetNumColumns|0|3|0||00| 3|OpenWrite|0|2|0||00| 4|NewRowid|0|2|0||00| 5|Null|0|3|0||00| 6|String8|0|4|0|cat|00| 7|Integer|1|5|0||00| 8|MakeRecord|3|3|6|dad|00| 9|Insert|0|6|2|episodes|0b| 10|Close|0|0|0||00| 11|Halt|0|0|0||00| 12|Transaction|0|1|0||00| 13|VerifyCookie|0|1|0||00| 14|Transaction|1|1|0||00| 15|VerifyCookie|1|0|0||00| 16|TableLock|0|2|1|episodes|00| 17|Goto|0|2|0||00|
1、初始狀態(Initial State)
當一個數據庫連線第一次開啟時,狀態如圖所示。圖中最右邊(“Disk”標註)表示儲存在儲存裝置中的內容。每個方框代表一個扇區。藍色的塊表示這個扇區儲存了原始資料。圖中中間區域是作業系統的磁碟緩衝區。開始的時候,這些快取是還沒有被使用,因此這些方框是空白的。圖中左邊區域顯示SQLite使用者程序的記憶體。因為這個資料庫連線剛剛開啟,所以還沒有任何資料記錄被讀入,所以這些記憶體也是空的。
2、獲取讀鎖(Acquiring A Read Lock)
在SQLite寫資料庫之前,它必須先從資料庫中讀取相關資訊。比如,在插入新的資料時,SQLite會先從sqlite_master表中讀取資料庫模式(相當於資料字典),以便編譯器對INSERT語句進行分析,確定資料插入的位置。
在進行讀操作之前,必須先獲取資料庫的共享鎖(shared lock),共享鎖允許兩個或更多的連線在同一時刻讀取資料庫。但是共享鎖不允許其它連線對資料庫進行寫操作。
shared lock存在於作業系統磁碟快取,而不是磁碟本身。檔案鎖的本質只是作業系統的核心資料結構,當作業系統崩潰或掉電時,這些核心資料也會隨之消失。
3、讀取資料
一旦得到shared lock,就可以進行讀操作。如圖所示,資料先由OS從磁碟讀取到OS快取,然後再由OS移到使用者程序空間。一般來說,資料庫檔案分為很多頁,而一次讀操作只讀取一小部分頁面。如圖,從8個頁面讀取3個頁面。
4、獲取Reserved Lock
在對資料進行修改操作之前,先要獲取資料庫檔案的Reserved Lock,Reserved Lock和shared lock的相似之處在於,它們都允許其它程序對資料庫檔案進行讀操作。Reserved Lock和Shared Lock可以共存,但是隻能是一個Reserved Lock和多個Shared Lock——多個Reserved Lock不能共存。所以,在同一時刻,只能進行一個寫操作。
Reserved Lock意味著當前程序(連線)想修改資料庫檔案,但是還沒開始修改操作,所以其它的程序可以讀資料庫,但不能寫資料庫。
5、建立恢復日誌(Creating A Rollback Journal File)
在對資料庫進行寫操作之前,SQLite先要建立一個單獨的日誌檔案,然後把要修改的頁面的原始資料寫入日誌。回滾日誌包含一個日誌頭(圖中的綠色)——記錄資料庫檔案的原始大小。所以即使資料庫檔案大小改變了,我們仍知道資料庫的原始大小。
從OS的角度來看,當一個檔案建立時,大多數OS(Windows、Linux、Mac OS X)不會向磁碟寫入資料,新建立的檔案此時位於磁碟快取中,之後才會真正寫入磁碟。如圖,日誌檔案位於OS磁碟快取中,而不是位於磁碟。
以上5步的實現程式碼:
//事務指令的實現 //p1為資料庫檔案的索引號--0為main database;1為temporary tables使用的檔案 //p2不為0,一個寫事務開始 case OP_Transaction: { //資料庫的索引號 int i = pOp->p1; //指向資料庫對應的btree Btree *pBt; assert( i>=0 && i<db->nDb ); assert( (p->btreeMask & (1<<i))!=0 ); //設定btree指標 pBt = db->aDb[i].pBt; if( pBt ){ //從這裡btree開始事務,主要給檔案加鎖,並設定btree事務狀態 rc = sqlite3BtreeBeginTrans(pBt, pOp->p2); if( rc==SQLITE_BUSY ){ p->pc = pc; p->rc = rc = SQLITE_BUSY; goto vdbe_return; } if( rc!=SQLITE_OK && rc!=SQLITE_READONLY /* && rc!=SQLITE_BUSY */ ){ goto abort_due_to_error; } } break; } //開始一個事務,如果第二個引數不為0,則一個寫事務開始,否則是一個讀事務 //如果wrflag>=2,一個exclusive事務開始,此時別的連線不能訪問資料庫 int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ BtShared *pBt = p->pBt; int rc = SQLITE_OK; btreeIntegrity(p); /* If the btree is already in a write-transaction, or it ** is already in a read-transaction and a read-transaction ** is requested, this is a no-op. */ //如果b-tree處於一個寫事務;或者處於一個讀事務,一個讀事務又請求,則返回SQLITE_OK if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){ return SQLITE_OK; } /* Write transactions are not possible on a read-only database */ //寫事務不能訪問只讀資料庫 if( pBt->readOnly && wrflag ){ return SQLITE_READONLY; } /* If another database handle has already opened a write transaction ** on this shared-btree structure and a second write transaction is ** requested, return SQLITE_BUSY. */ //如果資料庫已存在一個寫事務,則該寫事務請求時返回SQLITE_BUSY if( pBt->inTransaction==TRANS_WRITE && wrflag ){ return SQLITE_BUSY; } do { //如果資料庫對應btree的第一個頁面還沒讀進記憶體 //則把該頁面讀進記憶體,資料庫也相應的加read lock if( pBt->pPage1==0 ){ //加read lock,並讀頁面到記憶體 rc = lockBtree(pBt); } if( rc==SQLITE_OK && wrflag ){ //對資料庫檔案加RESERVED_LOCK鎖 rc = sqlite3pager_begin(pBt->pPage1->aData, wrflag>1); if( rc==SQLITE_OK ){ rc = newDatabase(pBt); } } if( rc==SQLITE_OK ){ if( wrflag ) pBt->inStmt = 0; }else{ unlockBtreeIfUnused(pBt); } }while( rc==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE && sqlite3InvokeBusyHandler(pBt->pBusyHandler) ); if( rc==SQLITE_OK ){ if( p->inTrans==TRANS_NONE ){ //btree的事務數加1 pBt->nTransaction++; } //設定btree事務狀態 p->inTrans = (wrflag?TRANS_WRITE:TRANS_READ); if( p->inTrans>pBt->inTransaction ){ pBt->inTransaction = p->inTrans; } } btreeIntegrity(p); return rc; } /* **獲取資料庫的寫鎖,發生以下情況時去除寫鎖: ** * sqlite3pager_commit() is called. ** * sqlite3pager_rollback() is called. ** * sqlite3pager_close() is called. ** * sqlite3pager_unref() is called to on every outstanding page. **pData指向資料庫的開啟的頁面,此時並不修改,僅僅只是獲取 **相應的pager,檢查它是否處於read-lock狀態 **如果開啟的不是臨時檔案,則開啟日誌檔案. **如果資料庫已經處於寫狀態,則do nothing */ int sqlite3pager_begin(void *pData, int exFlag){ PgHdr *pPg = DATA_TO_PGHDR(pData); Pager *pPager = pPg->pPager; int rc = SQLITE_OK; assert( pPg->nRef>0 ); assert( pPager->state!=PAGER_UNLOCK ); //pager已經處於share狀態 if( pPager->state==PAGER_SHARED ){ assert( pPager->aInJournal==0 ); if( MEMDB ){ pPager->state = PAGER_EXCLUSIVE; pPager->origDbSize = pPager->dbSize; }else{ //對檔案加 RESERVED_LOCK rc = sqlite3OsLock(pPager->fd, RESERVED_LOCK); if( rc==SQLITE_OK ){ //設定pager的狀態 pPager->state = PAGER_RESERVED; if( exFlag ){ rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK); } } if( rc!=SQLITE_OK ){ return rc; } pPager->dirtyCache = 0; TRACE2("TRANSACTION %d\n", PAGERID(pPager)); //使用日誌,不是臨時檔案,則開啟日誌檔案 if( pPager->useJournal && !pPager->tempFile ){ //為pager開啟日誌檔案,pager應該處於RESERVED或EXCLUSIVE狀態 //會向日志文件寫入header rc = pager_open_journal(pPager); } } } return rc; } //建立日誌檔案,pager應該處於RESERVED或EXCLUSIVE狀態 static int pager_open_journal(Pager *pPager){ int rc; assert( !MEMDB ); assert( pPager->state>=PAGER_RESERVED ); assert( pPager->journalOpen==0 ); assert( pPager->useJournal ); assert( pPager->aInJournal==0 ); sqlite3pager_pagecount(pPager); //日誌檔案頁面點陣圖 pPager->aInJournal = sqliteMalloc( pPager->dbSize/8 + 1 ); if( pPager->aInJournal==0 ){ rc = SQLITE_NOMEM; goto failed_to_open_journal; } //開啟日誌檔案 rc = sqlite3OsOpenExclusive(pPager->zJournal, &pPager->jfd, pPager->tempFile); //日誌檔案的位置指標 pPager->journalOff = 0; pPager->setMaster = 0; pPager->journalHdr = 0; if( rc!=SQLITE_OK ){ goto failed_to_open_journal; } /*一般來說,OS此時建立的檔案位於磁碟快取,並沒有實際 **存在於磁碟,下面三個操作就是為了把結果寫入磁碟,而對於 **windows系統來說,並沒有提供相應API,所以實際上沒有意義. */ //fullSync操作對windows沒有意義 sqlite3OsSetFullSync(pPager->jfd, pPager->full_fsync); sqlite3OsSetFullSync(pPager->fd, pPager->full_fsync); /* Attempt to open a file descriptor for the directory that contains a file. **This file descriptor can be used to fsync() the directory **in order to make sure the creation of a new file is actually written to disk. */ sqlite3OsOpenDirectory(pPager->jfd, pPager->zDirectory); pPager->journalOpen = 1; pPager->journalStarted = 0; pPager->needSync = 0; pPager->alwaysRollback = 0; pPager->nRec = 0; if( pPager->errCode ){ rc = pPager->errCode; goto failed_to_open_journal; } pPager->origDbSize = pPager->dbSize; //寫入日誌檔案的header--24個位元組 rc = writeJournalHdr(pPager); if( pPager->stmtAutoopen && rc==SQLITE_OK ){ rc = sqlite3pager_stmt_begin(pPager); } if( rc!=SQLITE_OK && rc!=SQLITE_NOMEM ){ rc = pager_unwritelock(pPager); if( rc==SQLITE_OK ){ rc = SQLITE_FULL; } } return rc; failed_to_open_journal: sqliteFree(pPager->aInJournal); pPager->aInJournal = 0; if( rc==SQLITE_NOMEM ){ /* If this was a malloc() failure, then we will not be closing the pager ** file. So delete any journal file we may have just created. Otherwise, ** the system will get confused, we have a read-lock on the file and a ** mysterious journal has appeared in the filesystem. */ sqlite3OsDelete(pPager->zJournal); }else{ sqlite3OsUnlock(pPager->fd, NO_LOCK); pPager->state = PAGER_UNLOCK; } return rc; } /*寫入日誌檔案頭 **journal header的格式如下: ** - 8 bytes: 標誌日誌檔案的魔數 ** - 4 bytes: 日誌檔案中記錄數 ** - 4 bytes: Random number used for page hash. ** - 4 bytes: 原來資料庫的大小(kb) ** - 4 bytes: 扇區大小512byte */ static int writeJournalHdr(Pager *pPager){ //日誌檔案頭 char zHeader[sizeof(aJournalMagic)+16]; int rc = seekJournalHdr(pPager); if( rc ) return rc; pPager->journalHdr = pPager->journalOff; if( pPager->stmtHdrOff==0 ){ pPager->stmtHdrOff = pPager->journalHdr; } //設定檔案指標指向header之後 pPager->journalOff += JOURNAL_HDR_SZ(pPager); /* FIX ME: ** ** Possibly for a pager not in no-sync mode, the journal magic should not ** be written until nRec is filled in as part of next syncJournal(). ** ** Actually maybe the whole journal header should be delayed until that ** point. Think about this. */ memcpy(zHeader, aJournalMagic, sizeof(aJournalMagic)); /* The nRec Field. 0xFFFFFFFF for no-sync journals. */ put32bits(&zHeader[sizeof(aJournalMagic)], pPager->noSync ? 0xffffffff : 0); /* The random check-hash initialiser */ sqlite3Randomness(sizeof(pPager->cksumInit), &pPager->cksumInit); put32bits(&zHeader[sizeof(aJournalMagic)+4], pPager->cksumInit); /* The initial database size */ put32bits(&zHeader[sizeof(aJournalMagic)+8], pPager->dbSize); /* The assumed sector size for this process */ put32bits(&zHeader[sizeof(aJournalMagic)+12], pPager->sectorSize); //寫入檔案頭 rc = sqlite3OsWrite(pPager->jfd, zHeader, sizeof(zHeader)); /* The journal header has been written successfully. Seek the journal ** file descriptor to the end of the journal header sector. */ if( rc==SQLITE_OK ){ rc = sqlite3OsSeek(pPager->jfd, pPager->journalOff-1); if( rc==SQLITE_OK ){ rc = sqlite3OsWrite(pPager->jfd, "\000", 1); } } return rc; }
其實現過程如下圖所示:
6、修改位於使用者程序空間的頁面(Changing Database Pages In User Space)
頁面的原始資料寫入日誌之後,就可以修改頁面了——位於使用者程序空間。每個資料庫連線都有自己私有的空間,所以頁面的變化只對該連線可見,而對其它連線的資料仍然是磁碟快取中的資料。從這裡可以明白一件事:一個程序在修改頁面資料的同時,其它程序可以繼續進行讀操作。圖中的紅色表示修改的頁面。
7、日誌檔案刷入磁碟(Flushing The Rollback Journal File To Mass Storage)
接下來把日誌檔案的內容刷入磁碟,這對於資料庫從意外中恢復來說是至關重要的一步。而且這通常也是一個耗時的操作,因為磁碟I/O速度很慢。
這個步驟不只把日誌檔案刷入磁碟那麼簡單,它的實現實際上分成兩步:首先把日誌檔案的內容刷入磁碟(即頁面資料);然後把日誌檔案中頁面的數目寫入日誌檔案頭,再把header刷入磁碟(這一過程在程式碼中清晰可見)。
程式碼如下:
/* **Sync日誌檔案,保證所有的髒頁面寫入磁碟日誌檔案 */ static int syncJournal(Pager *pPager){ PgHdr *pPg; int rc = SQLITE_OK; /* Sync the journal before modifying the main database ** (assuming there is a journal and it needs to be synced.) */ if( pPager->needSync ){ if( !pPager->tempFile ){ assert( pPager->journalOpen ); /* assert( !pPager->noSync ); // noSync might be set if synchronous ** was turned off after the transaction was started. Ticket #615 */ #ifndef NDEBUG { /* Make sure the pPager->nRec counter we are keeping agrees ** with the nRec computed from the size of the journal file. */ i64 jSz; rc = sqlite3OsFileSize(pPager->jfd, &jSz); if( rc!=0 ) return rc; assert( pPager->journalOff==jSz ); } #endif { /* Write the nRec value into the journal file header. If in ** full-synchronous mode, sync the journal first. This ensures that ** all data has really hit the disk before nRec is updated to mark ** it as a candidate for rollback. */ if( pPager->fullSync ){ TRACE2("SYNC journal of %d\n", PAGERID(pPager)); //首先保證髒頁面中所有的資料都已經寫入日誌檔案 rc = sqlite3OsSync(pPager->jfd, 0); if( rc!=0 ) return rc; } rc = sqlite3OsSeek(pPager->jfd, pPager->journalHdr + sizeof(aJournalMagic)); if( rc ) return rc; //頁面的數目寫入日誌檔案 rc = write32bits(pPager->jfd, pPager->nRec); if( rc ) return rc; rc = sqlite3OsSeek(pPager->jfd, pPager->journalOff); if( rc ) return rc; } TRACE2("SYNC journal of %d\n", PAGERID(pPager)); rc = sqlite3OsSync(pPager->jfd, pPager->full_fsync); if( rc!=0 ) return rc; pPager->journalStarted = 1; } pPager->needSync = 0; /* Erase the needSync flag from every page. */ //清除needSync標誌位 for(pPg=pPager->pAll; pPg; pPg=pPg->pNextAll){ pPg->needSync = 0; } pPager->pFirstSynced = pPager->pFirst; } #ifndef NDEBUG /* If the Pager.needSync flag is clear then the PgHdr.needSync ** flag must also be clear for all pages. Verify that this ** invariant is true. */ else{ for(pPg=pPager->pAll; pPg; pPg=pPg->pNextAll){ assert( pPg->needSync==0 ); } assert( pPager->pFirstSynced==pPager->pFirst ); } #endif return rc; }
8、獲取排斥鎖(Obtaining An Exclusive Lock)
在對資料庫檔案進行修改之前(注:這裡不是記憶體中的頁面),我們必須得到資料庫檔案的排斥鎖(Exclusive Lock)。得到排斥鎖的過程可分為兩步:首先得到Pending lock;然後Pending lock升級到exclusive lock。
Pending lock允許其它已經存在的Shared lock繼續讀資料庫檔案,但是不允許產生新的shared lock,這樣做目的是為了防止寫操作發生餓死情況。一旦所有的shared lock完成操作,則pending lock升級到exclusive lock。
9、修改的頁面寫入檔案(Writing Changes To The Database File)
一旦得到exclusive lock,其它的程序就不能進行讀操作,此時就可以把修改的頁面寫回資料庫檔案,但是通常OS都把結果暫時儲存到磁碟快取中,直到某個時刻才會真正把結果寫入磁碟。
以上2步的實現程式碼:
//把所有的髒頁面寫入資料庫 //到這裡開始獲取EXCLUSIVEQ鎖,並將頁面寫回作業系統檔案 static int pager_write_pagelist(PgHdr *pList){ Pager *pPager; int rc; if( pList==0 ) return SQLITE_OK; pPager = pList->pPager; /* At this point there may be either a RESERVED or EXCLUSIVE lock on the ** database file. If there is already an EXCLUSIVE lock, the following ** calls to sqlite3OsLock() are no-ops. ** ** Moving the lock from RESERVED to EXCLUSIVE actually involves going ** through an intermediate state PENDING. A PENDING lock prevents new ** readers from attaching to the database but is unsufficient for us to ** write. The idea of a PENDING lock is to prevent new readers from ** coming in while we wait for existing readers to clear. ** ** While the pager is in the RESERVED state, the original database file ** is unchanged and we can rollback without having to playback the ** journal into the original database file. Once we transition to ** EXCLUSIVE, it means the database file has been changed and any rollback ** will require a journal playback. */ //加EXCLUSIVE_LOCK鎖 rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK); if( rc!=SQLITE_OK ){ return rc; } while( pList ){ assert( pList->dirty ); rc = sqlite3OsSeek(pPager->fd, (pList->pgno-1)*(i64)pPager->pageSize); if( rc ) return rc; /* If there are dirty pages in the page cache with page numbers greater ** than Pager.dbSize, this means sqlite3pager_truncate() was called to ** make the file smaller (presumably by auto-vacuum code). Do not write ** any such pages to the file. */ if( pList->pgno<=pPager->dbSize ){ char *pData = CODEC2(pPager, PGHDR_TO_DATA(pList), pList->pgno, 6); TRACE3("STORE %d page %d\n", PAGERID(pPager), pList->pgno); //寫入檔案 rc = sqlite3OsWrite(pPager->fd, pData, pPager->pageSize); TEST_INCR(pPager->nWrite); } #ifndef NDEBUG else{ TRACE3("NOSTORE %d page %d\n", PAGERID(pPager), pList->pgno); } #endif if( rc ) return rc; //設定dirty pList->dirty = 0; #ifdef SQLITE_CHECK_PAGES pList->pageHash = pager_pagehash(pList); #endif //指向下一個髒頁面 pList = pList->pDirty; } return SQLITE_OK; }
10、修改結果刷入儲存裝置(Flushing Changes To Mass Storage)
為了保證修改結果真正寫入磁碟,這一步必不可少。對於資料庫存的完整性,這一步也是關鍵的一步。由於要進行實際的I/O操作,所以和第7步一樣,將花費較多的時間。
以上幾步實現程式碼如下(以上幾步是在函式sqlite3BtreeSync()--btree.c中呼叫的):
//同步btree對應的資料庫檔案 //該函式返回之後,只需要提交寫事務,刪除日誌檔案 int sqlite3BtreeSync(Btree *p, const char *zMaster){ int rc = SQLITE_OK; if( p->inTrans==TRANS_WRITE ){ BtShared *pBt = p->pBt; Pgno nTrunc = 0; #ifndef SQLITE_OMIT_AUTOVACUUM if( pBt->autoVacuum ){ rc = autoVacuumCommit(pBt, &nTrunc); if