sqlite3常用技巧


數據庫是一種工具,在合理的條件下使用數據庫可以獲得許多益處。

  • 使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM