SQLite的操作及其優化

掌握SQLite操作

最近用Ruby寫了一個七牛的demo參賽作品,使用了sqlite3,用到很多操作,利用假期的時間,簡單做一個快速掌握SQLite命令的小入門。

SQLite是一個開放源代碼的數據庫引擎,具有獨立,無服務器依賴,零配置,支持事務等特點。SQLite一直以輕量級爲特點,在移動和嵌入式設備上使用廣泛,官方稱其是世界上部署最廣泛的數據庫引擎。

強大的命令集

首先我們看一下sqlite提供了哪些強大的命令。

fileos:false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
sqlite> .help
.backup ?DB? FILE      Backup DB (default "main") to FILE
.bail ON|OFF           Stop after hitting an error.  Default OFF
.databases             List names and files of attached databases
.dump ?TABLE? ...      Dump the database in an SQL text format
                         If TABLE specified, only dump tables matching
                         LIKE pattern TABLE.
.echo ON|OFF           Turn command echo on or off
.exit                  Exit this program
.explain ?ON|OFF?      Turn output mode suitable for EXPLAIN on or off.
                         With no args, it turns EXPLAIN on.
.header(s) ON|OFF      Turn display of headers on or off
.help                  Show this message
.import FILE TABLE     Import data from FILE into TABLE
.indices ?TABLE?       Show names of all indices
                         If TABLE specified, only show indices for tables
                         matching LIKE pattern TABLE.
.load FILE ?ENTRY?     Load an extension library
.log FILE|off          Turn logging on or off.  FILE can be stderr/stdout
.mode MODE ?TABLE?     Set output mode where MODE is one of:
                         csv      Comma-separated values
                         column   Left-aligned columns.  (See .width)
                         html     HTML <table> code
                         insert   SQL insert statements for TABLE
                         line     One value per line
                         list     Values delimited by .separator string
                         tabs     Tab-separated values
                         tcl      TCL list elements
.nullvalue STRING      Print STRING in place of NULL values
.output FILENAME       Send output to FILENAME
.output stdout         Send output to the screen
.prompt MAIN CONTINUE  Replace the standard prompts
.quit                  Exit this program
.read FILENAME         Execute SQL in FILENAME
.restore ?DB? FILE     Restore content of DB (default "main") from FILE
.schema ?TABLE?        Show the CREATE statements
                         If TABLE specified, only show tables matching
                         LIKE pattern TABLE.
.separator STRING      Change separator used by output mode and .import
.show                  Show the current values for various settings
.stats ON|OFF          Turn stats on or off
.tables ?TABLE?        List names of tables
                         If TABLE specified, only list tables matching
                         LIKE pattern TABLE.
.timeout MS            Try opening locked tables for MS milliseconds
.vfsname ?AUX?         Print the name of the VFS stack
.width NUM1 NUM2 ...   Set column widths for "column" mode
.timer ON|OFF          Turn the CPU timer measurement on or off
sqlite>

以”.“開始的命令規則

看到了上面的全部命令,可以觀察到,所有的命令都是以”.“開始的。而常用的SQL語句是格式自由的,並且可以跨越多行,空白字符(whitespace)和註釋可以出現在任何地方。而SQLite中以.開始的命令有更多的限制,具體如下

  • 所有命令以 . 開始,並且 . 的左側不包含任何空白字符
  • 所有命令必須全部包含在一行輸入行中
  • 所有命令不能出現在SQL語句之中
  • 命令不識別註釋

常用操作

創建一個數據庫文件

fileos:false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#找一個不存在的文件
09:35:16-androidyue/tmp$ cat test.db
cat: test.db: No such file or directory

#使用sqlite3 想要創建的數據庫文件
09:35:28-androidyue/tmp$ sqlite3 test.db

#進入sqlite,執行建表語句
sqlite> CREATE TABLE qn_uploaded(filePath VARCHAR(255), bucket VARCHAR(63),  lastModified FLOAT);
#退出SQLite
sqlite> .exit

#查看指定的文件,創建成功
09:42:26-androidyue/tmp$ cat test.db
09:44:45-androidyue/tmp$ dedqn_uploadedCREATE TABLE qn_uploaded(filePath VARCHAR(255), bucket VARCHAR(63),  lastModified FLOAT)

打開已存在的數據庫文件

fileos:false
1
22:56:15-androidyue~ $ sqlite3 database_file.db

查看數據庫

fileos:false
1
2
3
4
5
sqlite> .databases
seq  name             file
---  ---------------  ----------------------------------------------------------
0    main             /home/androidyue/qiniu/.qiniu.db
1    temp

查看數據表

fileos:false
1
2
sqlite> .tables
qn_uploaded

查看建表語句

fileos:false
1
2
sqlite> .schema qn_uploaded
CREATE TABLE qn_uploaded(filePath VARCHAR(255), bucket VARCHAR(63),  lastModified FLOAT);

顯示字段名稱

fileos:false
1
2
3
4
5
6
7
8
9
#沒有開啓
sqlite> select * from qn_uploaded;
/home/androidyue/Documents/octopress/public//images/email.png|droidyue|1410096518.43964

#開啓之後
sqlite> .header on
sqlite> select * from qn_uploaded;
filePath|bucket|lastModified
/home/androidyue/Documents/octopress/public//images/email.png|droidyue|1410096518.43964

導出數據表結構和數據(文本形式)

fileos:false
1
2
3
4
5
6
sqlite> .dump qn_uploaded
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE qn_uploaded(filePath VARCHAR(255), bucket VARCHAR(63),  lastModified FLOAT);
INSERT INTO "qn_uploaded" VALUES('/home/androidyue/Documents/octopress/public/images/dotted-border.png','droidyue',1410096552.54864);
COMMIT;

調整輸出

sqlite3程序可以使用八種不同的格式顯示結果。 這些格式是”csv”, “column”, “html”, “insert”, “line”, “list”, “tabs”, and “tcl”. 你可以使用.mode命令來進行切換輸出格式

默認的輸出模式list,使用了list模式,每條查詢結果記錄都會輸出到一行,每一列使用一個分割符分割,默認的分割符是 “|“,list模式有一個常用的使用情況,就是當你想對查詢結果記性額外處理(比如AWK處理)時,會事半功倍。

列表模式輸出

fileos:false
1
2
sqlite> select * from qn_uploaded;
/home/androidyue/Documents/octopress/public//images/email.png|droidyue|1410096518.43964

修改列表模式分割符

fileos:false
1
2
3
sqlite> .separator ", "
sqlite> select * from qn_uploaded;
/home/androidyue/Documents/octopress/public//images/email.png, droidyue, 1410096518.43964

使用Line模式

每行的輸出格式爲 字段名 = 字段值

fileos:false
1
2
3
4
5
sqlite> .mode line
sqlite> select * from qn_uploaded;
    filePath = /home/androidyue/Documents/octopress/public//images/email.png
      bucket = droidyue
lastModified = 1410096518.43964

使用列模式

fileos:false
1
2
3
4
sqlite> .mode column
sqlite> select * from qn_uploaded;
/home/androidyue/Documents/octopress/public//images/email.png  droidyue    1410096518.43964
/home/androidyue/Documents/octopress/public/images/rss.png     droidyue    1410096552.54764

輸出內容

輸出結果

默認情況下,所有的查詢結果都是都是作爲標準的輸出展示。使用.output可以將輸出結果定向到文件中。

fileos:false
1
2
3
4
5
6
7
8
sqlite> .output /tmp/test.txt
sqlite> select * from qn_uploaded;
sqlite> .exit
17:48:54-androidyue~/Documents/octopress/qiniu (master)$ cat /tmp/test.txt
file  bucket         last
----  -------------  ----
/home/androidyue/Documents/octopress/public//images/email.png  droidyue       1410096518.43964
/home/androidyue/Documents/octopress/public/images/rss.png  droidyue       1410096552.54764

備份和恢復

備份

fileos:false
1
2
#語法 .backup ?DB? FILE      Backup DB (default "main") to FILE
sqlite> .backup main /tmp/main.txt

恢復

fileos:false
1
2
#語法.restore ?DB? FILE     Restore content of DB (default "main") from FILE
.restore main  /tmp/main.txt

Android 中 SQLite 性能優化

數據庫是應用開發中常用的技術,在Android應用中也不例外。Android默認使用了SQLite數據庫,在應用程序開發中,我們使用最多的無外乎增刪改查。縱使操作簡單,也有可能出現查找數據緩慢,插入數據耗時等情況,如果出現了這種問題,我們就需要考慮對數據庫操作進行優化了。本文將介紹一些實用的數據庫優化操作,希望可以幫助大家更好地在開發過程中使用數據庫。

建立索引

很多時候,我們都聽說,想要查找快速就建立索引。這句話沒錯,數據表的索引類似於字典中的拼音索引或者部首索引。

建立索引

創建索引的基本語法如下

1
CREATE INDEX index_name ON table_name;

創建單列索引

1
CREATE INDEX index_name ON table_name (column_name);

索引真的好麼

毋庸置疑,索引加速了我們檢索數據表的速度。然而正如西方諺語 “There are two sides of a coin”,索引亦有缺點:對於增加,更新和刪除來說,使用了索引會變慢,比如你想要刪除字典中的一個字,那麼你同時也需要刪除這個字在拼音索引和部首索引中的信息。建立索引會增加數據庫的大小,比如字典中的拼音索引和部首索引實際上是會增加字典的頁數,讓字典變厚的。爲數據量比較小的表建立索引,往往會事倍功半。所以使用索引需要考慮實際情況進行利弊權衡,對於查詢操作量級較大,業務對要求查詢要求較高的,還是推薦使用索引的。

編譯SQL語句

SQLite想要執行操作,需要將程序中的sql語句編譯成對應的SQLiteStatement,比如select * from record這一句,被執行100次就需要編譯100次。對於批量處理插入或者更新的操作,我們可以使用顯式編譯來做到重用SQLiteStatement。

想要做到重用SQLiteStatement也比較簡單,基本如下:

  • 編譯sql語句獲得SQLiteStatement對象,參數使用?代替
  • 在循環中對SQLiteStatement對象進行具體數據綁定,bind方法中的index從1開始,不是0

請參考如下簡單的使用代碼

1
2
3
4
5
6
7
8
9
10
11
private void insertWithPreCompiledStatement(SQLiteDatabase db) {
    String sql = "INSERT INTO " + TableDefine.TABLE_RECORD + "( " + TableDefine.COLUMN_INSERT_TIME + ") VALUES(?)";
    SQLiteStatement  statement = db.compileStatement(sql);
    int count = 0;
    while (count < 100) {
        count++;
        statement.clearBindings();
        statement.bindLong(1, System.currentTimeMillis());
        statement.executeInsert();
    }
}

顯式使用事務

在Android中,無論是使用SQLiteDatabase的insert,delete等方法還是execSQL都開啓了事務,來確保每一次操作都具有原子性,使得結果要麼是操作之後的正確結果,要麼是操作之前的結果。

然而事務的實現是依賴於名爲rollback journal文件,藉助這個臨時文件來完成原子操作和回滾功能。既然屬於文件,就符合Unix的文件範型(Open-Read/Write-Close),因而對於批量的修改操作會出現反覆打開文件讀寫再關閉的操作。然而好在,我們可以顯式使用事務,將批量的數據庫更新帶來的journal文件打開關閉降低到1次。

具體的實現代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void insertWithTransaction(SQLiteDatabase db) {
    int count = 0;
    ContentValues values = new ContentValues();
    try {
        db.beginTransaction();
        while (count++ < 100) {
            values.put(TableDefine.COLUMN_INSERT_TIME, System.currentTimeMillis());
            db.insert(TableDefine.TABLE_RECORD, null, values);
        }
          db.setTransactionSuccessful();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        db.endTransaction();
    }
}

上面的代碼中,如果沒有異常拋出,我們則認爲事務成功,調用db.setTransactionSuccessful();確保操作真實生效。如果在此過程中出現異常,則批量數據一條也不會插入現有的表中。

查詢數據優化

對於查詢的優化,除了建立索引以外,有以下幾點微優化的建議

按需獲取數據列信息

通常情況下,我們處於自己省時省力的目的,對於查找使用類似這樣的代碼

1
2
3
private void badQuery(SQLiteDatabase db) {
    db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ;
}

其中上面方法的第二個參數類型爲String[],意思是返回結果參考的colum信息,傳遞null表明需要獲取全部的column數據。這裏建議大家傳遞真實需要的字符串數據對象表明需要的列信息,這樣做效率會有所提升。

提前獲取列索引

當我們需要遍歷cursor時,我們通常的做法是這樣

1
2
3
4
5
6
private void badQueryWithLoop(SQLiteDatabase db) {
    Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
    while (cursor.moveToNext()) {
        long insertTime = cursor.getLong(cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME));
    }
}

但是如果我們將獲取ColumnIndex的操作提到循環之外,效果會更好一些,修改後的代碼如下:

1
2
3
4
5
6
7
8
private void goodQueryWithLoop(SQLiteDatabase db) {
    Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
    int insertTimeColumnIndex = cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME);
    while (cursor.moveToNext()) {
        long insertTime = cursor.getLong(insertTimeColumnIndex);
    }
    cursor.close();
}

ContentValues的容量調整

SQLiteDatabase提供了方便的ContentValues簡化了我們處理列名與值的映射,ContentValues內部採用了HashMap來存儲Key-Value數據,ContentValues的初始容量是8,如果當添加的數據超過8之前,則會進行雙倍擴容操作,因此建議對ContentValues填入的內容進行估量,設置合理的初始化容量,減少不必要的內部擴容操作。

及時關閉Cursor

使用數據庫,比較常見的就是忘記關閉Cursor。關於如何發現未關閉的Cursor,我們可以使用StrictMode,詳細請戳這裏Android性能調優利器StrictMode

耗時異步化

數據庫的操作,屬於本地IO,通常比較耗時,如果處理不好,很容易導致ANR,因此建議將這些耗時操作放入異步線程中處理,這裏推薦一個單線程 + 任務隊列形式處理的HandlerThread實現異步化。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章