前言
這篇是接著上篇ios學習筆記之SQLite初學者手冊(上)的,對如何優化在執行大量插入語句之后的性能,如何回滾數據以及如何查詢做一簡單的說明。
一 性能優化
1 DML語句 通過綁定參數實現插入語句
準備語句(prepared statement)對象
準備語句(prepared statement)對象一個代表一個簡單SQL語句對象的實例,這個對象通常被稱為“準備語句”或者編譯好的SQL語句
操作歷程:
- 使用sqlite3_prepare_v2或相關的函數創建這個對象 如果執行成功,則返回SQLITE_OK,否則返回一個錯誤碼
2.使用sqlite3_bind_*()給宿主參數(host parameters)綁定值
3.通過調用sqlite3_step() 一次或多次來執行這個sql
4.使用sqlite3_reset()重置這個語句,然后回到第2步,這個過程做0次或多次
5.使用sqlite3_finalize()銷毀這個對象,防止內存泄露
假設我們要插入很多條數據,則第2、3、4步重復執行,第1、5步就不需要重復執行了
示例:
(創建數據庫接上篇)
ios學習筆記之SQLite初學者手冊(上)
為Student類添加方法
// 通過綁定的方式,來插入語句
func bindInsert() -> () {
let sql = "insert into t_stu(name,age,score) values(?,?,?)"
// 1 根據sql語句,創建,準備語句,問號代表占位
// 參數一:一個已經打開的數據庫
// 參數二:sql字符串 "lsfjfflkf"
// 參數三:即將取出字符串(參數二這個字符串)的長度,例如:2,就是從前面開始,取2個字符
// -1代表自動計算
// 參數四:預處理語句
// 參數五:根據參數三的長度,取出參數二的值以后,剩余的數字,例如取出了參數二中的ls
// 兩個字符,那么參數五就是fjfflkf
let db = SQLiteTool.shareInstance.db
var stmt:OpaquePointer? = nil
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
print("預處理失敗")
return
}
// 2 綁定參數
// 參數一:準備語句
// 參數二:綁定值的索引,索引從1開始
sqlite3_bind_int(stmt, 2, 20) // 綁定第二個參數,賦值20
sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
// 綁定文本
// 參數一:準備語句
// 參數二:綁定值的索引,1
// 參數三:綁定的值
// 參數四:值取出多少長度,-1,取出所有
// 參數五:值的處理方式
// 下面兩個是宏定義,swift里面沒有宏定義
// (按住command點擊sqlite3_bind_text方法,索索SQLITE_STATIC及SQLITE_TRANSIENT,然后會發現:
// typedef void (*sqlite3_destructor_type)(void*);
// #define SQLITE_STATIC ((sqlite3_destructor_type)0)
// #define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1)
// sqlite3_destructor_type是指向函數的指針
// )
// SQLITE_STATIC:認為參數是一個常量,不會被釋放,處理方案:不做任何引用
// SQLITE_TRANSIENT:會對參數進行一個引用
//
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
// 此時按住option鍵點擊SQLITE_TRANSIENT會看到:let SQLITE_TRANSIENT: sqlite3_destructor_type,就是說此時SQLITE_TRANSIENT的類型是sqlite3_destructor_type
sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
// 3 執行sql語句,準備語句
if sqlite3_step(stmt) == SQLITE_DONE{
print("執行成功")
}
// 4 重置語句
sqlite3_reset(stmt)
// 5 釋放準備語句
sqlite3_finalize(stmt)
}
func insertStudent() -> () {
let sql = "insert into t_stu(name,age,score) values('\(name)',\(age),\(score))"
if SQLiteTool.shareInstance.execute(sql: sql) {
print("插入行成功")
}else{
print("插入行失敗")
}
}
在控制器中測試
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let stu = Student(name:"wangwu",age:39,score:99)
stu.bindInsert()
}
刷新Navicat Premium,發現插入成功
相比較上篇里講的給我一條語句sql,然后去執行sqlite3_exec(db, sql, nil, nil, nil)語句,我們的綁定參數確實是復雜了很多,但是sqlite3_exec其實就是對綁定參數方法的一個封裝,當我們插入很多條語句時,分別查看兩種方法的性能,那么肯定是綁定參數這種方法性能更好。
下面在控制器中對兩種方法進行執行時間測試:
用sqlite3_exec方法:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let stu = Student(name:"wangwu",age:39,score:99)
let beginTime = CFAbsoluteTimeGetCurrent()
for i in 0..<1000{
stu.insertStudent()
}
let endTime = CFAbsoluteTimeGetCurrent()
print(endTime - beginTime)
}
測試三次,點擊三次屏幕,(將所有的print("插入行成功")等都注釋掉,因為這個很耗時)結果:
執行成功
0.5521399974823
0.540975987911224
0.548547983169556
用綁定參數方法:
對bindInsert()方法進行修改,將第2、3、4步放入循環
// 通過綁定的方式,來插入語句
func bindInsert() -> () {
let sql = "insert into t_stu(name,age,score) values(?,?,?)"
let db = SQLiteTool.shareInstance.db
var stmt:OpaquePointer? = nil
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
print("預處理失敗")
return
}
for i in 0..<1000{
// 2 綁定參數
let newI = Int32(i) // 將i轉換成Int32類型
sqlite3_bind_int(stmt, 2, newI) // 綁定第二個參數,賦值i
sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
// 3 執行sql語句,準備語句
if sqlite3_step(stmt) == SQLITE_DONE{
// print("執行成功")
}
// 4 重置語句
sqlite3_reset(stmt)
}
// 5 釋放準備語句
sqlite3_finalize(stmt)
}
func insertStudent() -> () {
let sql = "insert into t_stu(name,age,score) values('\(name)',\(age),\(score))"
if SQLiteTool.shareInstance.execute(sql: sql) {
// print("插入行成功")
// }else{
// print("插入行失敗")
}
}
控制器
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let stu = Student(name:"wangwu",age:39,score:99)
let beginTime = CFAbsoluteTimeGetCurrent()
stu.bindInsert()
let endTime = CFAbsoluteTimeGetCurrent()
print(endTime - beginTime)
}
結果:
// 執行成功
// 0.494072020053864
// 0.520428955554962
// 0.516857028007507
// 0.521933019161224
多次測試,發現綁定參數方法略微快點,這還不是我們想要的,如果繼續優化,該怎么辦呢???其實,如果使用sqlite3_exec或者,sqlite3_step()來執行sql語句,會自動開啟一個”事務“,然后,自動提交”事務“,那么針對這個問題,我們可以這樣解決:只需要手動開啟事務,手動提交任務,這時候,函數內部,就不會自動開啟和提交事務,這樣可以大大提高執行效率。
2 手動開啟事務
為SQLiteTool類增加方法
// 手動開啟事務
func beginTransaction() -> () {
let sql = "begin transaction"
execute(sql: sql)
}
// 手動關閉事務
func commitTransation() -> () {
let sql = "commit transaction"
execute(sql: sql)
}
將其插入到Student的綁定代碼中
// 通過綁定的方式,來插入語句
func bindInsert() -> () {
let sql = "insert into t_stu(name,age,score) values(?,?,?)"
let db = SQLiteTool.shareInstance.db
var stmt:OpaquePointer? = nil
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
print("預處理失敗")
return
}
// 手動開啟事務
SQLiteTool.shareInstance.beginTransaction()
for i in 0..<1000{
// 2 綁定參數
let newI = Int32(i)// 將i轉換成Int32類型
sqlite3_bind_int(stmt, 2, newI) // 綁定第二個參數,賦值i
sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
// 3 執行sql語句,準備語句
if sqlite3_step(stmt) == SQLITE_DONE{
// print("執行成功")
}
// 4 重置語句
sqlite3_reset(stmt)
}
// 提交事務
SQLiteTool.shareInstance.commitTransation()
// 5 釋放準備語句
sqlite3_finalize(stmt)
}
在控制器里面執行
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let stu = Student(name:"wangwu",age:39,score:99)
let beginTime = CFAbsoluteTimeGetCurrent()
stu.bindInsert()
let endTime = CFAbsoluteTimeGetCurrent()
print(endTime - beginTime)
}
結果:
執行成功
0.00568002462387085
可以看出效率大大提高了。
二 回滾數據
當我們做多條修改時,假如有的語句修改成功,而有的修改失敗,那么最終我們是否要提交呢?在數據庫里,我們只有當全部數據修改成功時再整體提交,如果部分執行失敗,就不提交,保持修改之前的數據。
示例:
我們先在表t_stu中增加字段money,然后手動輸入兩條數據
在Student類里添加更新數據的類方法
class func update(sql:String) -> (Bool) {
return SQLiteTool.shareInstance.execute(sql: sql)
}
在SQLiteTool類添加回滾的語句
func rollbackTransaction() -> () {
let sql = "rollback transaction"
execute(sql: sql)
}
控制器:故意將語句“let result2 = Student.update(sql: "update t_stu set money1 = money + 10 where name = 'lisi'")” 中的money錯寫成money1
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// zhangsan money -10
SQLiteTool.shareInstance.beginTransaction()
let result1 = Student.update(sql: "update t_stu set money = money - 10 where name = 'zhangsan'") // 將張三的錢減10
// lisi money +10
let result2 = Student.update(sql: "update t_stu set money1 = money + 10 where name = 'lisi'") // 將李四的錢加10
if result1 && result2 {
SQLiteTool.shareInstance.commitTransation() // 當兩條修改語句全部正確執行,才會提交
print("修改數據成功,提交")
}else{
SQLiteTool.shareInstance.rollbackTransaction() // 只要有一條操作失敗,就回滾,滾到修改之前的數據
print("修改數據失敗,回滾")
}
}
刷新數據庫,發現數據沒變。但是如果我們將錯寫的money1改回money,則提交成功。
三 DQL查詢語句
1 代碼
為Student方法添加兩個新的類方法,queryAllStmt方法是通過綁定來進行查詢,queryAll是通過sqlite3_exec進行查詢
class func queryAllStmt() -> () {
// 準備語句 歷程
let sql = "select * from t_stu "
let db = SQLiteTool.shareInstance.db
// 一.創建準備語句
// 參數一:一個已經打開的數據庫
// 參數二:sql語句
// 參數三:字符串,取的長度,-1代表全選
// 參數四:準備語句的指針
// 參數五:剩余待取的字符串
var stmt:OpaquePointer? = nil
if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK {
print("準備預處理語句失敗")
return
}
// 二.綁定參數(這一步可以省略)
// 三.執行準備語句
// sqlite3_step 的作用 執行DQL語句時,會把執行得到的結果放到準備語句(stmt)中
while sqlite3_step(stmt) == SQLITE_ROW {
// 先判斷,如果還有數據(整行數據),則繼續執行,while循環是一行行取
// 讀取數據
// 1. 從準備語句里讀取
let count = sqlite3_column_count(stmt) // sqlite3_column_count(stmt)是用來計算預處理語句里,一共有多少列的
for i in 0..<count{
// for循環是一列列取
// 2. 取出列的名稱
let columnName = sqlite3_column_name(stmt, i)
let columnNameStr = String(cString:columnName!,encoding:String.Encoding.utf8)
print(columnNameStr as Any)
// 3.取出列的值
// 不同的類型,是通過不同的函數獲取
// 3.1 獲取這一列的類型
let type = sqlite3_column_type(stmt, i)
// 3.2 根據不同的類型,使用不同的函數
if type == SQLITE_INTEGER {
let value = sqlite3_column_int(stmt, i)
print(value)
}
if type == SQLITE_FLOAT {
let value = sqlite3_column_double(stmt, i)
print(value)
}
if type == SQLITE_TEXT {
// value:UnsafePointer<UInt8>?
let value = sqlite3_column_text(stmt, i)
let valueStr = String(cString: value!)
print(valueStr)
}
}
}
// 四.重置準備語句(這一步可以省略)
// 五.釋放準備語句
sqlite3_finalize(stmt)
}
class func queryAll() -> () {
let sql = "select * from t_stu"
let db = SQLiteTool.shareInstance.db
// 參數一:一個打開的數據庫
// 參數二:sql語句
// 參數三:回調代碼塊
// 參數1:傳遞過來的值
// 參數2:列的個數
// 參數3:值的數組
// 參數4:列名稱的數組
// 返回值:0 繼續查詢 1:終止查詢
// 參數四:傳遞到參數三里面的第一個參數
// 參數五:錯誤信息
sqlite3_exec(db, sql, { (firstValue, columnCount, values, columnNames) -> Int32 in
let count = Int(columnCount)
for i in 0..<count{
// 列的名稱
let columnName = columnNames?[i]
let columnNameStr = String(cString:columnName!,encoding:String.Encoding.utf8)
// 值
let value = values?[i]
let valueStr = String(cString:value!,encoding:String.Encoding.utf8)
// let valueStr = String.Encoding.utf8
print(columnNameStr as Any,valueStr as Any)
}
return 0
}, nil, nil)
}
在控制器分別調用兩個方法
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Student.queryAll()
Student.queryAllStmt()
}
結果:
Optional("id")
1
Optional("name")
zhangsan
Optional("age")
10
Optional("score")
99.0
Optional("money")
100.0
Optional("id")
2
Optional("name")
lisi
Optional("age")
9
Optional("score")
88.0
Optional("money")
100.0
2 將綁定方法與sqlite3_exec方法進行對比
①.使用后者,解析出來的都是字符串,而使用綁定的方法基本類型都可以解析
②.使用后者,無法插入二進制,因為插入二進制時,是當做字符串進行解析的,而使用綁定的方法,可以在綁定參數時,綁定二進制(sqlite3_bind_blob)