一、Sqlite刪除記錄后數據庫文件大小不變
原因是:
sqlite采用的是變長紀錄存儲,當你從Sqlite刪除數據后,未使用的磁盤空間被添加到一個內在的”空閑列表”中用於存儲你下次插入的數據,用於提高效率,磁盤空間並沒有丟失,但也不向操作系統返回磁盤空間,這就導致刪除數據乃至清空整個數據庫后,數據文件大小還是沒有任何變化,還是很大。
解決方法:兩種
1、手動:
在數據刪除后,手動執行VACUUM命令,執行方式很簡單
sqlite> vacuum;
在Navicat中可以直接執行,eg:
DELETE FROM Company WHERE Age < 190;VACUUM;
但是此語句在C#代碼中執行不生效,可以查看下文的方法。
2、自動:
在數據庫文件建成中,將auto_vacuum設置成“1”。
參考:
壓縮Sqlite數據文件大小,解決數據刪除后占用空間不變的問題
擴展:C# 中如何解決?
方法也是兩種:
1、手動釋放空間
先寫一個執行sql語句的函數:
private void ExecuteSql(string sDbPath, string sqlStr) { using (SQLiteConnection conn = new SQLiteConnection("data source = " + sDbPath)) { using (SQLiteCommand cmd = new SQLiteCommand()) { cmd.Connection = conn; conn.Open(); cmd.CommandText = sqlStr; cmd.ExecuteNonQuery(); conn.Close(); } } }
在刪除數據表/大量數據后,調用上述函數(dbPath為數據庫的地址)。
ExecuteSql(dbPath, "VACUUM");
參考代碼:SQLiteRepository.cs
以上代碼視sqlite-net.dll 的版本而改變,
SQLiteCommand cmd = new SQLiteCommand(SQLiteConnection); //sqlite-net-pcl\1.7.335 SQLiteCommand SQLiteCmd = SQLiteConnection.CreateCommand(sqlStr); //sqlite-net-pcl\1.4.118
2、設置數據庫為自動釋放空間
當數據庫中無數據表時,設置其屬性:
ExecuteSql(dbPath, "PRAGMA auto_vacuum = 1;");
測試了一下發現效果也挺好的。
比較兩種方法,自動更方便。但是需要注意的是,在進行頻繁的插入、更新、刪除操作時,數據庫會產生大量的內存碎片。自動釋放空間的方法只能釋放空閑數據頁,但是並不會對內存碎片進行整理,在這個過程中反而會產生額外的內存碎片;
而手動方式可以同時釋放空閑空間和整理內存碎片。
推薦使用第一種方式。
二、Sqlite執行效率優化
實踐:

/// <summary> /// 執行時間測試 /// </summary> public class ExecuteTimeHelper { public static void Test() { SQLiteConnection connection = Run(() => new SQLiteConnection("Test.db"), "連接對象初始化並打開連接"); Run(() => connection.CreateTable<Info>(), "創建數據表Info"); SQLiteCommand command = Run(() => new SQLiteCommand(connection), "命令對象初始化"); Run(() => { command.CommandText = $"DELETE FROM Info;VACUUM;UPDATE sqlite_sequence SET seq ='0' where name ='Info';"; command.ExecuteNonQuery(); }, "執行DELETE命令及收縮數據庫"); int count = 200; Run(() => { for (int i = 0; i < count; i++) { command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')"; command.ExecuteNonQuery(); //將返回操作所影響的記錄條數 } command.ExecuteScalar<object>(); }, $"[---不使用事務---]事務執行INSERT命令,插入{count}條數據"); Run(() => { command.CommandText = $"DELETE FROM Info;VACUUM;UPDATE sqlite_sequence SET seq ='0' where name ='Info';"; command.ExecuteNonQuery(); }, "執行DELETE命令及收縮數據庫"); Run(() => connection.BeginTransaction(), "開始事務"); int count2 = 3000; Run(() => { for (int i = 0; i < count2; i++) { command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')"; command.ExecuteNonQuery(); } var result = command.ExecuteScalar<object>(); }, $"[---使用事務---]執行INSERT命令,插入{count2}條數據"); Run(() => connection.Commit(), "提交事務"); //或者用事務方法 執行批量sql語句 connection.RunInTransaction(() => { int count2 = 3000; Run(() => { for (int i = 0; i < count2; i++) { command.CommandText = $"INSERT INTO Info(Name, Age) VALUES ('A{i:000}','{i}')"; command.ExecuteNonQuery(); } var result = command.ExecuteScalar<object>(); }, $"[---使用事務---]執行INSERT命令,插入{count2}條數據"); }); Run(() => connection.Close(), "關閉連接"); Console.ReadKey(); } public static void Run(Action action, string description) { Stopwatch sw = Stopwatch.StartNew(); action(); Console.WriteLine($"--> {description}: {sw.ElapsedMilliseconds}ms"); } public static T Run<T>(Func<T> func, string description) { Stopwatch sw = Stopwatch.StartNew(); T result = func(); Console.WriteLine($"--> {description}: {sw.ElapsedMilliseconds}ms"); return result; } } class Info { public long ID { set; get; } public string Name { set; get; } public long Age { set; get; } }
- 無論是執行插入或查詢操作,使用事務比不使用事務快,尤其是在批量插入操作時,減少得時間非常明顯;比如在不使用事務的情況下插入3000條記錄,執行所花費的時間為17.252s,而使用事務,執行時間只用了0.057s,效果非常明顯,而SQL Server不存在這樣的問題。
- 不能每次執行一條SQL語句前開始事務並在SQL語句執行之后提交事務,這樣的執行效率同樣是很慢,最好的情況下,是在開始事務后批量執行SQL語句,再提交事務,這樣的效率是最高的。
即使只插入一條數據也是有很大差異的:
三、SQLite.SQLiteException異常: database is locked、Busy
這就涉及到sqlite的並發讀寫問題:
1.sqlite3支持多線程同時讀操作,但不支持多線程同時寫操作。
2.同一時刻只能有一個線程去進行寫操作,並且在一個線程進行寫操作的時候,其他線程是不能進行讀操作的。
當一個線程正在寫操作時,其他線程的讀寫都會返回操作失敗的錯誤,顯示數據庫文件被鎖住。
1、sqlite3的鎖及事務類型
- UNLOCKED ,無鎖狀態。數據庫文件沒有被加鎖。
- SHARED 共享狀態。數據庫文件被加了共享鎖。可以多線程執行讀操作,但不能進行寫操作。
- RESERVED 保留狀態。數據庫文件被加保留鎖。表示數據庫將要進行寫操作。
- PENDING 未決狀態。表示即將寫入數據庫,正在等待其他讀線程釋放 SHARED 鎖。一旦某個線程持有 PENDING 鎖,其他線程就不能獲取 SHARED 鎖。這樣一來,只要等所有讀線程完成,釋放 SHARED 鎖后,它就可以進入 EXCLUSIVE 狀態了。
- EXCLUSIVE 獨占鎖。表示它可以寫入數據庫了。進入這個狀態后,其他任何線程都不能訪問數據庫文件。因此為了並發性,它的持有時間越短越好。
說明:
- 當執行select即讀操作時,需要獲取到SHARED鎖(共享鎖),
- 當執行insert/update/delete操作(即內存寫操作時),需要進一步獲取到RESERVERD鎖(保留鎖),
- 當進行commit操作(即磁盤寫操作時),需要進一步獲取到EXCLUSIVE鎖(排它鎖)。
- 對於RESERVERD鎖,sqlite3保證同一時間只有一個連接可以獲取到保留鎖,也就是同一時間只有一個連接可以寫數據庫(內存),但是其它連接仍然可以獲取SHARED鎖,也就是其它連接仍然可以進行讀操作(這里可以認為寫操作只是對磁盤數據的一份內存拷貝進行修改,並不影響讀操作)。
- 對於EXCLUSIVE鎖,是比保留鎖更為嚴格的一種鎖,在需要把修改寫入磁盤即commit時需要在保留鎖/未決鎖的基礎上進一步獲取到排他鎖,顧名思義,排他鎖排斥任何其它類型的鎖,即使是SHARED鎖也不行,所以,在一個連接進行commit時,其它連接是不能做任何操作的(包括讀)。
- PENDING鎖(即未決鎖),則是比較特殊的一種鎖,它可以允許已獲取到SHARED鎖的事務繼續進行,但不允許其它連接再獲取SHARED鎖,當已存在的SHARED鎖都被釋放后(事務執行完成),持有未決鎖的事務就可以獲得commit的機會了。sqlite3使用這種鎖來防止writer starvation(寫餓死)。
sqlite3只支持庫級鎖,庫級鎖意味着什么?
意味着同時只能允許一個寫操作,也就是說,即事務T1在A表插入一條數據,事務T2在B表中插入一條數據,這兩個操作不能同時進行,即使你的機器有100個CPU,也無法同時進行,而只能順序進行。表級都不能並行,更別說元組級了——這就是庫級鎖。
但是,SQLite盡量延遲申請X鎖,直到數據塊真正寫盤時才申請X鎖,這是非常巧妙而有效的。
2、死鎖的情況
3、對SQLITE_BUSY的處理
更多:Sqlite WAL原理
How to open SQLite connection in WAL mode