SQLite剖析之臨時文件、內存數據庫

原文地址:http://www.cnblogs.com/5211314jackrose/p/5816013.html

一、7種臨時文件
    SQLite中,一個數據庫由單個磁盤文件構成,簡化了SQLite的使用,因爲移動或備份數據庫只要拷貝單個文件即可。這也使得SQLite適合用作應用程序文件格式。但是,當在單個文件中存儲一個數據庫時,SQLite會在處理數據庫的過程中使用許多臨時文件。
    SQLite目前使用7種不同類型的臨時文件:
    * 回滾日誌(Rollback journals)
    * 主日誌(Master journals)
    * SQL語句日誌(Statement journals)
    * 臨時數據庫(TEMP databases)
    * 視圖和子查詢的持久化(Materializations of views and subqueries)
    * 臨時索引(Transient indices)
    * VACUUM使用的臨時數據庫(Transient databases used by VACUUM)

    (1)回滾日誌
    回滾日誌是一個臨時文件,用來實現原子提交和回滾功能。回滾日誌總是位於與數據庫文件相同的目錄下,文件名爲數據庫文件名後加"-journal"。回滾日誌通常在一個事務首次開始時創建,在一個事務提交或回滾時刪除。如果沒有回滾日誌,SQLite將不能回滾一個未完成的事務,並且在事務執行的中間某時刻若發生系統崩潰或斷電,數據庫也會被損壞。回滾日誌通常在事務的起點和終點創建和銷燬,但也會有一些例外規則。
    如果崩潰或斷電發生在事務的中間某時刻,則在硬盤上會留有回滾日誌。在下次另外一個應用程序嘗試打開數據庫文件時,它會通知存在回滾日誌(“熱日誌”),並使用日誌中的信息來把數據庫恢復到未完成事務開始之前的狀態。這就是SQLite實現原子提交的基本原理。
    如果應用程序使用指令"PRAGMA locking_mode=EXCLUSIVE;"把SQLite置於排斥鎖模式下,則SQLite在帶排斥鎖模式會話的事務開始時創建一個新的回滾日誌,在事務結束不會刪除回滾日誌。回滾日誌可能會被縮小,或者它的頭部可能會被清零(取決於你使用的SQLite版本),但文件不會被刪除,直到排斥訪問模式退出時回滾日誌纔會被刪除。
    回滾日誌的創建和刪除也可以用日誌模式PRAGMA指令來更改。默認的日誌模式是DELETE,即在每個事務結束時刪除回滾日誌。PERSIST日誌模式則放棄刪除日誌文件,而是把日誌文件的頭部清零,以防止其他進程回滾日誌,因此這與刪除日誌文件有同樣的效果,雖然實際上並沒有從磁盤上刪除日誌文件。也就是說,日誌模式PERSIST展示的行爲與EXCLUSIVE鎖模式相同。OFF日誌模式讓SQLite放棄在開始時創建回滾日誌,它會禁用SQLite的原子提交和回滾功能,讓ROLLBACK命令不可用。如果使用OFF日誌模式的事務在中間某時刻發生崩潰或斷電,則數據庫文件不能恢復,可能會被損壞。

    (2)主日誌文件
    主日誌文件用於多數據庫操作的原子提交過程中,即一個事務修改多個數據庫,這些數據庫通過ATTACH命令被關聯在一個數據庫連接上。主日誌文件總是位於與主數據庫文件(主數據庫文件是在調用sqlite3_open()、sqlite3_open16()或sqlite3_open_v2()創建數據庫連接時使用的數據庫)相同的目錄下,後跟一個隨機的後綴。主日誌文件中包含所有關聯的輔助數據庫名稱。多數據庫事務提交時主日誌文件就會被刪除。
    主日誌文件只會在這樣的情況下創建:一個數據連接與通過ATTACH關聯的兩個或多個數據庫進行會話,並且一個事務修改多個數據庫文件。如果沒有主日誌文件,多數據庫事務對每個單獨數據庫的提交是原子性的,但對整個多數據庫一起則不是原子性的。也就是說,如果提交在中間某時刻因爲崩潰或斷電而中斷,則可能對一個數據庫的更改完成,而對另一個數據庫的更改被回滾。主日誌文件確保所有數據庫的所有更改要麼一起回滾,要麼一起提交。

    (3)SQL語句日誌文件
    SQL語句日誌文件用於回滾大型事務中一個單獨SQL語句的部分結果。例如,假設一條UPDATE語句嘗試修改數據庫中的100行,但在修改完50行後,因爲意外情況而終止。SQL語句日誌用來撤消這50行的更改,以便數據庫恢復到語句執行前的狀態。
    SQL語句日誌只會在一條UPDATE或INSERT語句修改數據庫的多行,且意外終止或在觸發器中拋出異常因而需要撤消部分結果的情況下創建。如果UPDATE或INSERT沒有包含在BEGIN...COMMIT中,且在同一數據庫連接上沒有其他活動的SQL語句,則無需創建語句日誌,因爲可以使用原來的回滾日誌。如果使用了可靠的衝突解決算法,則語句日誌也會被忽略,例如:

複製代碼
UPDATE OR FAIL ...
UPDATE OR IGNORE ...
UPDATE OR REPLACE ...
INSERT OR FAIL ...
INSERT OR IGNORE ...
INSERT OR REPLACE ...
REPLACE INTO ....
複製代碼

    SQL語句日誌文件使用隨機的文件名,不一定要在與主數據庫相同的目錄下,在事務結束時自動刪除。SQL語句日誌的空間大小隻是UPDATE或INSERT語句完成的更改部分的比例大小。

    (4)臨時數據庫
    使用"CREATE TEMP TABLE"命令創建的表格只在執行這條命令的數據庫連接上可見。這些TEMP表格,以及任何關聯的索引、觸發器和視圖,一起存放在一個單獨的臨時數據庫文件中,這個臨時數據庫在首次遇到"CREATE TEMP TABLE"命令時創建。這個單獨的臨時數據庫文件也有一個關聯的回滾日誌。用來存儲TEMP表格的臨時數據庫會在使用sqlite3_close()關閉數據庫連接時自動刪除。
    臨時數據數據庫文件與通過ATTACH命令添加的輔助數據庫文件非常類似,不過帶有一些特殊屬性。臨時數據庫文件總是在數據庫連接關閉時自動刪除。臨時數據庫總是使用synchronous=OFF和journal_mode=PERSIST這兩條PRAGMA指令設置。並且,臨時數據庫不能使用DETACH,別的進程也不能通過ATTACH關聯臨時數據庫。臨時數據庫文件和它的回滾日誌只有在應用程序使用"CREATE TEMP TABLE"命令時纔會被創建。

    (5)視圖和子查詢的持久化
    包含子查詢的查詢命令必須在某個時刻單獨執行子查詢並把結果存儲在一個臨時表格中,然後使用臨時表格中的內容來執行外部查詢。我們稱之爲“持久化”子查詢。SQLite的查詢優化器會嘗試避免持久化,但有時候這是不可避免的。持久化過程創建的每個臨時表格存儲在它們自己單獨的臨時文件中,在查詢結束時自動刪除。這些臨時表格的大小取決於子查詢實體的數據數量。
    位於IN操作符右邊的子查詢通常必須被持久化,例如:

SELECT * FROM ex1 WHERE ex1.a IN (SELECT b FROM ex2);

    在上面的查詢命令中,子查詢"SELECT b FROM ex2"的執行結果被存儲在一個臨時表格中(實際爲一個臨時索引),它通過二進制搜索的方式來確定是否存在一個值ex2.b。一旦這個臨時表格被創建,就運行外部查詢,對每個預期的結果行檢查ex1.a是否包含在臨時表中,如果爲true,則輸出這個結果行。
    爲了避免創建臨時表格,查詢可以重寫爲以下形式:

SELECT * FROM ex1 WHERE EXISTS(SELECT 1 FROM ex2 WHERE ex2.b=ex1.a);

    如果在列ex2.b上有索引,則3.5.4及以後版本的SQLite會自動做這樣的重寫。
    如果IN操作符的右邊部分是值列表,像下面這樣:

SELECT * FROM ex1 WHERE a IN (1,2,3);

    位於IN右邊的值列表被認爲是一個子查詢,必須要持久化,也就是說此查詢行爲相當於下面這樣:

SELECT * FROM ex1 WHERE a IN (SELECT 1 UNION ALL
                              SELECT 2 UNION ALL
                              SELECT 3);

    當IN右邊是一個值列表時,會用一個臨時索引來持有這些值。
    當子查詢出現在SELECT命令的FROM子句中時也會進行持久化,例如:

SELECT * FROM ex1 JOIN (SELECT b FROM ex2) AS t ON t.b=ex1.a;

    根據查詢,SQLite可能需要持久化"(SELECT b FROM ex2)"子查詢到一個臨時表格中,然後在ex1和臨時表格之間執行連接。查詢優化器會嘗試“扁平化(flattening)”這個查詢來避免子查詢的持久化。在這個例子中,查詢可以被扁平化,SQLite將自動把這個查詢轉換成:

SELECT ex1.*, ex2.b FROM ex1 JOIN ex2 ON ex2.b=ex1.a;

    更復雜的查詢可能會,也可能不會進行扁平化處理以避免臨時表格。是否扁平化處理取決於子查詢或外部查詢是否包含聚合函數、ORDER BY子句、GROUP BY子句或LIMIT子句等等。

    (6)臨時索引
    SQLite使用臨時索引來實現很多SQL語言特性,包括:
    * ORDER BY或GROUP BY子句
    * 聚合查詢中的DISTINCT關鍵字
    * 複合式SELECT語句,即有UNION, EXCEPT或INTERSECT等連接子句
    每個臨時索引存放在它自己的臨時文件中,在SQL語句執行結束時被自動刪除。
    SQLite會嘗試使用已存在的索引來實現ORDER BY子句。如果在指定的字段上已存在索引,SQLite將遍歷該索引(而不是創建臨時索引)來提取需要的信息,並且以指定的排序輸出結果行。如果SQLite沒有找到合適的索引,則執行查詢並把每行存儲在一個臨時索引中,索引的關鍵字爲ORDER BY指定的字段。然後SQLite返回並從頭到尾遍歷臨時索引,以指定的排序輸出每行。
    對於GROUP BY子句,SQLite根據指定字段對輸出行進行排序。每個輸出行與先前行進行比較,看它是否屬於新的組。GROUP BY字段的排序與ORDER BY字段的排序是相同的。如果有存在的索引就使用它,如果沒有已存在的索引,則創建臨時索引。
    聚合查詢上的DISTINCT關鍵字會在一個臨時文件中創建臨時索引,並把每行結果存儲到索引中。對新的結果行,如果在臨時索引中已存在,則忽略它。
    複合查詢的UNION運算符會在一個臨時文件創建臨時索引,並把左邊和右邊子查詢結果存儲到索引中,忽略重複的行。當兩個子查詢執行完後,從頭到尾遍歷臨時索引來產生最後的輸出。
    複合查詢的EXCEPT運算符會在一個臨時文件創建臨時索引,並把左邊子查詢結果存儲到臨時索引中,然後從索引中移除右邊子查詢的結果,最後從頭到尾遍歷臨時索引以得到最後的輸出。
    複合查詢的EXCEPT運算符會創建兩個獨立的臨時索引,它們位於兩個獨立的臨時文件中。左邊和右邊子查詢被執行並存放到各自的臨時索引中。然後一起遍歷兩個索引,輸出同時存在於兩個索引中的結果。
    注意複合查詢的UNION ALL運算符自己並不使用臨時索引,當然UNION ALL左邊和右邊的子查詢可能會單獨使用臨時索引,這取決於它們是怎麼複合的。

    (7)VACUUM命令使用的臨時數據庫
    VACUUM命令會先創建一個臨時文件,然後重建整個數據庫並寫入到該臨時文件中。之後將臨時文件中的內容拷貝回原有的數據庫文件中,最後刪除該臨時文件。VACUUM命令創建的臨時文件不會比原有數據庫文件大。


二、SQLITE_TEMP_STORE編譯時參數和PRAGMA指令
    回滾日誌、主日誌和SQL語句日誌文件總是會被寫入磁盤,但其它類型的臨時文件可能存放在內存中而不會寫入磁盤(這樣可以減少大量的IO操作),是寫入磁盤還是存放於內存中取決於SQLITE_TEMP_STORE編譯時參數,temp_store pragma運行時指令,以及臨時文件的大小。對於SQLite來說,回滾日誌、主數據庫日誌和SQL語句日誌文件在需要的時候SQLite都會將它們寫入磁盤文件,但是對於其它類型的臨時文件,SQLite是可以將它們存放在內存中以取代磁盤文件的,這樣在執行的過程中就可以減少大量的IO操作了。要完成該優化主要依賴於以下三個因素:

    1. 編譯時參數SQLITE_TEMP_STORE
    SQLITE_TEMP_STORE編譯時參數是源代碼中的宏定義(#define),其取值範圍是0到3(缺省值爲1),如下:
    * 等於0時,臨時文件總是存儲在磁盤上,而不會考慮temp_store pragma指令的設置。
    * 等於1時,臨時文件缺省存儲在磁盤上,但是該值可以被temp_store pragma指令覆蓋。
    * 等於2時,臨時文件缺省存儲在內存中,但是該值可以被temp_store pragma指令覆蓋。
    * 等於3時,臨時文件總是存儲在內存中,而不會考慮temp_store pragma指令的設置。

  2. 運行時指令temp_store pragma
    temp_store pragma指令的取值範圍是0到2(缺省值爲0),在程序運行時該指令可以被動態的設置,如下:
    * 等於0時,臨時文件的存儲行爲完全由SQLITE_TEMP_STORE編譯期參數確定。
    * 等於1時,如果編譯期參數SQLITE_TEMP_STORE指定使用內存存儲臨時文件,那麼該指令將覆蓋這一行爲,使用磁盤存儲。否則直接使用SQLITE_TEMP_STORE的行爲。
    * 等於2時,如果編譯期參數SQLITE_TEMP_STORE指定使用磁盤存儲臨時文件,那麼該指令將覆蓋這一行爲,使用內存存儲。否則直接使用SQLITE_TEMP_STORE的行爲。

    重申一下,SQLITE_TEMP_STORE編譯時參數temp_store pragma指令隻影響除回滾日誌和主日誌之外的其它臨時文件的存儲策略。換句話說,回滾日誌和主數據庫日誌將總是將數據寫入磁盤,而不會關注以上兩個參數的值。

  3. 臨時文件的大小
    對於以上兩個參數,都有參數值表示缺省情況是存儲在內存中的,只有當臨時文件的大小超過一定的閾值後纔會根據一定的算法,將部分數據寫入到磁盤中,以免臨時文件佔用過多的內存而影響其它程序的執行效率。


三、其他臨時文件優化策略
    SQLite對當前讀寫的數據庫頁面採用了Page Cache的緩衝優化機制,因此即便臨時文件被指定存儲在磁盤上,也只有當該文件的大小增長到一定的尺寸後(導致頁面緩存填滿)纔有可能被SQLite刷新到磁盤文件上,在此之前它們仍將駐留在內存中。這就意味着對於大多數場景,如果臨時表和臨時索引的數據量相對較少(頁面緩存足夠存放它們),那麼它們是不會被寫到磁盤中的,當然也就不會有磁盤IO發生。只有當它們增長到內存不能容納的時候纔會被刷新到磁盤文件中的。
    每個臨時表格和索引都有自己的頁緩存,它們能存放最大多少個數據庫頁面由SQLITE_DEFAULT_TEMP_CACHE_SIZE編譯期參數來確定,這個參數指定了臨時表和索引在佔用多少Page Cache時才需要被刷新到磁盤文件,該參數的缺省值爲500頁。這個參數值不能在運行時修改。


四、內存數據庫
    在SQLite中,數據庫通常存儲在磁盤文件中。然而在有些情況下,我們可以讓數據庫始終駐留在內存中。最常用的一種方式是在調用sqlite3_open()、sqlite3_open16()或sqlite3_open_v2()時,數據庫文件名參數指定爲":memory:",如:

rc = sqlite3_open(":memory:", &db);

    在調用完以上函數後,不會有任何磁盤文件被生成,取而代之的是,一個新的數據庫在純內存中被成功創建了。由於沒有持久化,該數據庫在當前數據庫連接被關閉後就會立刻消失。需要注意的是,每個:memory:數據庫是不同的數據庫,也就是說,用文件名":memory:"打開兩個數據庫連接將創建兩個獨立的內存數據庫。
    文件名":memory:"可以用在任何允許使用數據庫文件名的地方。例如,它可以用於ATTACH命令中,讓內存數據庫像其他普通數據庫一樣,附加到當前的連接中,如:

ATTACH DATABASE ':memory:' AS aux1;

    注意在創建內存數據庫時,只能用文件名":memory:",不能包含其他文本,例如"./:memory:",這樣會創建一個基於磁盤文件的數據庫。在使用URI格式的文件名時,也可以使用":memory:",例如:

rc = sqlite3_open("file::memory:", &db);

    或者

ATTACH DATABASE 'file::memory:' AS aux1;

    如果內存數據庫使用URI文件名打開,則它可以使用共享緩存。如果通過未修飾的":memory"名來指定內存數據庫,則這個數據庫總是有一個私有的對其他連接不可見的緩存。如果使用URI文件名,則同樣的內存數據庫可以被兩個或多個數據庫連接打開,例如:

rc = sqlite3_open("file::memory:?cache=shared", &db);

    或者 

ATTACH DATABASE 'file::memory:?cache=shared' AS aux1;

    這使得多個數據庫連接可以共享同一個內存數據庫。當然,共享一個內存數據庫的這些連接需要在同一個進程中。當最後一個數據庫連接關閉時,內存數據庫自動被刪除。
    如果需要在一個進程中使用多個不同的但可共享的內存數據庫,可以在URI文件名中附加mode=memory查詢參數來創建一個命名的內存數據庫:

rc = sqlite3_open("file:memdb1?mode=memory&cache=shared", &db);

    或者  

ATTACH DATABASE 'file:memdb1?mode=memory&cache=shared' AS aux1;

    以這種方式命名的內存數據庫,只會與名字精確相同的另一個連接共享它的緩存。


五、(空文件名對應的)臨時數據庫
    在調用sqlite3_open()函數或執行ATTACH命令時,如果數據庫文件參數傳的是空字符串,那麼一個新的臨時文件將被創建以作爲臨時數據庫的存儲文件,如:

rc = sqlite3_open("", &db);

    或者

ATTACH DATABASE '' AS aux2;

    每次都會創建不同的臨時文件,和內存數據庫非常相似,兩個連接創建的臨時數據庫也是各自獨立的,在連接關閉後臨時數據庫將自動消失,其存儲文件也將被自動刪除。
    儘管磁盤文件被創建用於存儲臨時數據庫中的數據信息,但是實際上臨時數據庫也會和內存數據庫一樣,通常駐留在內存中,唯一不同的是,當臨時數據庫中數據量過大時,SQLite爲了保證有更多的內存可用於其它操作,因此會將臨時數據庫中的部分數據寫到磁盤文件中,而內存數據庫則始終會將數據存放在內存中

發佈了84 篇原創文章 · 獲贊 208 · 訪問量 61萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章