數據庫是一種工具,在合理的條件下使用數據庫可以獲得許多益處。
- 使用SQL語句可以完成復雜的統計,可以少寫許多復雜邏輯
- 使用數據庫無需擔心內存溢出問題
- 原來可能需要許多文件來保存,現在只需要一個sqlite db文件就足夠了
一、使用conn.executemany批量執行
executemany的速度是execute的2倍。
import os
import sqlite3
import timeit
db_name = "test.db"
insert_size = 1000000
def clear(conn):
conn.execute("DROP TABLE IF EXISTS user")
conn.execute("CREATE TABLE user (name VARCHAR(10),age INT)")
def execute():
conn = sqlite3.connect(db_name)
clear(conn)
for i in range(insert_size):
conn.execute("INSERT INTO user (name,age) VALUES(?,?)", ("user%s" % i, i))
conn.close()
def execute_many():
def data_iter():
for i in range(insert_size):
yield "user%s" % i, i
conn = sqlite3.connect(db_name)
clear(conn)
conn.executemany("INSERT INTO user(name,age)VALUES(?,?)", data_iter())
conn.close()
print(timeit.timeit(execute, number=1)) #6.94724779695861
print(timeit.timeit(execute_many, number=1)) #3.6068240203748756
os.remove(db_name)
二、直接用conn即可,不需要游標
使用游標大概是JDBC倡導的東西,實際上在很多數據庫連接庫中直接使用連接即可。
在線示例和文檔中通常如下:
connection = sqlite3.connect(':memory:')
cursor = connection.cursor()
# Do something with cursor
但大多數情況下,你根本不需要光標,你可以直接使用連接對象(本文末尾會提到)。像execute和executemany類似的操作可以直接在連接上調用。以下是一個證明此事的示例:
import sqlite3
connection = sqlite3(':memory:')
# Create a table
connection.execute('CREATE TABLE events(ts, msg)')
# Insert values
connection.executemany(
'INSERT INTO events VALUES (?,?)',
[
(1, 'foo'),
(2, 'bar'),
(3, 'baz')
]
)
# Print inserted rows
for row in connnection.execute('SELECT * FROM events'):
print(row)
三、光標可用於迭代
你可能經常會看到使用fetchone或fetchall來處理SELECT查詢結果的示例。但是我發現處理這些結果的最自然的方式是直接在光標上迭代:
for row in connection.execute('SELECT * FROM events'):
print(row)
四、使用Pragmas
在你的程序中有幾個 pragma 可用於調整 sqlite3 的行為。特別地,其中一個可以改善性能的是synchronous
connection.execute('PRAGMA synchronous = OFF')
此命令讓sqlite停止了一些安全性檢測,這可能是危險的。如果應用程序在事務中間意外崩潰,數據庫可能會處於不一致的狀態。所以請小心使用! 但是如果要更快地插入很多行並且自己確信不需要sqlite幫忙做一些無用的檢測,那么這是一個選擇。
五、推遲索引創建
假設你需要在數據庫上創建幾個索引,而你需要在插入很多行的同時創建索引。把索引的創建推遲到所有行的插入之后可以導致實質性的性能改善。
六、使用占位符不要拼串
使用 Python 字符串操作將值包含到查詢中是很方便的。但是這樣做非常不安全,而 sqlite3 給你提供了更好的方法來做到這一點:
# Do not do this!
my_timestamp = 1
c.execute("SELECT * FROM events WHERE ts = '%s'" % my_timestamp)
# Do this instead
my_timestamp = (1,)
c.execute('SELECT * FROM events WHERE ts = ?', my_timestamp)
此外,使用Python%s(或格式或格式的字符串常量)的字符串插值對於executemany來說並不是總是可行,這更凸顯了占位符比拼串有優勢。
七、使用反射
查詢執行完畢之后得到的結果是一個元組,元組的每列表示什么含義可以通過cursor.description來獲得。cursor.description可以看做一個二維字符串。
def to_json(cursor: sqlite3.Cursor, row):
# 將一行數據和cursor轉化為一個dict
a = {}
for col in cursor.description:
a[col[0]] = row
return a
八、存儲二進制數據
存儲二進制數據就是存儲bytes類型的對象。
import os
import sqlite3
import numpy as np
db_name = "test.db"
conn = sqlite3.connect(db_name)
conn.execute("DROP TABLE IF EXISTS test")
conn.execute("CREATE TABLE test(data BLOB)")
a = np.random.random((3, 4)).astype(np.float32)
print(a)
row = a[0].tobytes().hex()
conn.executemany("INSERT INTO test(data)VALUES (?)", map(lambda x: (sqlite3.Binary(x.tobytes()),), a))
b = conn.execute("SELECT * FROM test").fetchall()
for i in b:
print(i, len(i[0]))
i = np.fromstring(i[0], dtype=np.float32)
print(i)
print(conn.execute("SELECT length(data) FROM test").fetchall()) # 輸出16,16,16
conn.close()
os.remove(db_name)
存儲二進制容易犯的一個錯誤就是把blob字段當成字符串來用,這樣會導致一個字節使用兩個16進制字符來表示,空間平白無故地多用一倍。
import os
import sqlite3
import numpy as np
db_name = "test.db"
conn = sqlite3.connect(db_name)
conn.execute("DROP TABLE IF EXISTS test")
conn.execute("CREATE TABLE test(data BLOB)")
a = np.random.random((3, 4)).astype(np.float32)
print(a)
row = a[0].tobytes().hex()
conn.executemany("INSERT INTO test(data)VALUES (?)", map(lambda x: (x.tobytes().hex(),), a))
b = conn.execute("SELECT * FROM test").fetchall()
for i in b:
print(i, len(i[0]))
i = np.fromstring(bytes.fromhex(i[0]), dtype=np.float32)
print(i)
print(conn.execute("SELECT length(data) FROM test").fetchall()) # 輸出32,32,32
conn.close()
os.remove(db_name)
九、sqlite中的鎖
sqlite3的鎖及事務類型
sqlite3總共有五種鎖,按鎖的級別依次是:UNLOCKED /SHARED /RESERVERD /PENDING /EXCLUSIVE。
當執行select即讀操作時,需要獲取到SHARED鎖(共享鎖),當執行insert/update/delete操作(即內存寫操作時),需要進一步獲取到RESERVERD鎖(保留鎖),當進行commit操作(即磁盤寫操作時),需要進一步獲取到EXCLUSIVE鎖(排它鎖)。
對於RESERVERD鎖,sqlite3保證同一時間只有一個連接可以獲取到保留鎖,也就是同一時間只有一個連接可以寫數據庫(內存),但是其它連接仍然可以獲取SHARED鎖,也就是其它連接仍然可以進行讀操作(這里可以認為寫操作只是對磁盤數據的一份內存拷貝進行修改,並不影響讀操作)。
對於EXCLUSIVE鎖,是比保留鎖更為嚴格的一種鎖,在需要把修改寫入磁盤即commit時需要在保留鎖/未決鎖的基礎上進一步獲取到排他鎖,顧名思義,排他鎖排斥任何其它類型的鎖,即使是SHARED鎖也不行,所以,在一個連接進行commit時,其它連接是不能做任何操作的(包括讀)。
PENDING鎖(即未決鎖),則是比較特殊的一種鎖,它可以允許已獲取到SHARED鎖的事務繼續進行,但不允許其它連接再獲取SHARED鎖,當已存在的SHARED鎖都被釋放后(事務執行完成),持有未決鎖的事務就可以獲得commit的機會了。sqlite3使用這種鎖來防止writer starvation(寫餓死)。
死鎖的情況
死鎖的情況
當兩個連接使用begin transaction開始事務時,第一個連接執行了一次select操作(已經獲取到SHARED鎖),第二個連接執行了一次insert操作(已經獲取到了RESERVERD鎖),此時第一個連接需要進行一次insert/update/delete(需要獲取到RESERVERD鎖),第二個連接則希望執行commit(需要獲取到EXCLUSIVE鎖),由於第二個連接已經獲取到了RESERVERD鎖,根據RESERVERD鎖同一時間只有一個連接可以獲取的特性,第一個連接獲取RESERVERD鎖的操作必定失敗,而由於第一個連接已經獲取到SHARED鎖,第二個連接希望進一步獲取到EXCLUSIVE鎖的操作也必定失敗。就導致了事務死鎖。
事務類型的使用原則
在用”begin transaction”顯式開啟一個事務時,默認的事務類型為DEFERRED,鎖的狀態為UNLOCKED,即不獲取任何鎖,如果在使用的數據庫沒有其它的連接,用begin就可以了。如果有多個連接都需要對數據庫進行寫操作,那就得使用BEGIN IMMEDIATE/EXCLUSIVE開始事務了。
使用事務的好處是:1.一個事務的所有操作相當於一次原子操作,如果其中某一步失敗,可以通過回滾來撤銷之前所有的操作,只有當所有操作都成功時,才進行commit,保證了操作的原子特性;2.對於多次的數據庫操作,如果我們希望提高數據查詢或更新的速度,可以在開始操作前顯式開啟一個事務,在執行完所有操作后,再通過一次commit來提交所有的修改或結束事務。
對SQLITE_BUSY的處理
當有多個連接同時對數據庫進行寫操作時,根據事務類型的使用原則,我們在每個連接中用BEGIN IMMEDIATE開始事務,即多個連接都嘗試取得保留鎖的情況,根據保留鎖同一時間只有一個連接可以獲取到的特性,其它連接都將獲取失敗,即事務開始失敗,這種情況下,sqlite3將返回一個SQLITE_BUSY的錯誤,如果我們不希望操作就此失敗而返回,就必須處理SQLITE_BUSY的情況,sqlite3提供了sqlite3_busy_handler或sqlite3_busy_timeout來處理SQLITE_BUSY,對於sqlite3_busy_handler,我們可以指定一個busy_handler來處理,並可以指定失敗重試的次數。而sqlite3_busy_timeout則是由sqlite3自動進行sleep並重試,當sleep的累積時間超過指定的超時時間時,最終返回SQLITE_BUSY。需要注意的是,這兩個函數同時只能使用一個,后面的調用會覆蓋掉前次調用。從使用上來說,sqlite3_busy_timeout更易用一些,只需要指定一個總的超時時間,然后sqlite自己會決定多久進行重試以及重試的次數,直到達到總的超時時間最終返回SQLITE_BUSY。並且,這兩個函數一經調用,對其后的所有數據庫操作都有效,非常方便。
參考資料
https://www.cnblogs.com/nice107/p/8067165.html
https://zhuanlan.zhihu.com/p/26576194