每次查询后重新打开sqlite数据库的效率

mat*_*tts 10 python sqlite

我目前正在龙卷风中使用Web服务器,但是我遇到了尝试同时访问数据库的不同代码的问题.

我通过简单地使用查询函数简化了这一点,该函数基本上是这样做的(但略高一些):

def query(command, arguments = []):
    db = sqlite3.open("models/data.db")
    cursor = db.cursor()
    cursor.execute(command, arguments)
    result = cursor.findall()
    db.close()
    return result
Run Code Online (Sandbox Code Playgroud)

我只是想知道在每次查询后重新打开数据库是多么有效(我猜它是一个非常大的常量时间操作,还是会缓存一些东西?),以及是否有更好的方法来执行此操作.

Car*_*roo 15

我正在添加自己的答案,因为我不同意目前接受的答案.它声明该操作不是线程安全的,但这是完全错误的 - SQLite使用适合其当前平台的文件锁定来确保所有访问都符合ACID.

在Unix系统上,这将是fcntl()flock()锁定,这是一个每文件句柄锁.因此,每次发布一个新连接的代码将始终分配一个新的文件句柄,因此SQLite自己的锁定将防止数据库损坏.这样做的结果是在NFS共享或类似产品上使用SQLite通常是一个坏主意,因为这些通常不会提供特别可靠的锁定(但它确实取决于您的NFS实现).

正如@abernert在评论中已经指出的那样,SQLite遇到了线程问题,但这与线程之间共享单个连接有关.正如他所提到的,这意味着如果您使用应用程序范围的池,如果第二个线程从池中提取循环连接,则会出现运行时错误.这些也是你在测试时可能没有注意到的那些烦人的错误(轻负载,也许只有一个线程在使用中),但后来很容易引起头痛.Martijn Pieters后来建议使用线程本地池应该可以正常工作.

正如SQLite FAQ从版本3.3.1中所概述的那样,只要它们没有任何锁定,它们在线程之间传递连接实际上是安全的 - 这是SQLite的作者添加的一个让步,尽管它批评了线程的使用一般.任何明智的连接池的实现将始终确保一切都已经被提交或更换池中的连接回滚之前,所以实际上是一个应用程序全局池可能是安全的,如果它不是针对共享Python的检查,这即使使用更新版本的SQLite,我相信仍然存在.当然,我的Python 2.7.3系统有一个sqlite3带有sqlite_version_info报告3.7.9的模块,但RuntimeError如果你从多个线程访问它,它仍会抛出一个.

在任何情况下,当检查存在时,即使基础SQLite库支持连接,也无法有效地共享连接.

至于你原来的问题,每次创建一个新连接肯定比保持一个连接池效率低,但是已经提到过这需要一个线程本地池,这实现起来有点痛苦.创建与数据库的新连接的开销基本上是打开文件并读取标头以确保它是有效的SQLite文件.实际执行语句的开销较高,因为它需要取出外观并执行相当多的文件I/O,因此大部分工作实际上是延迟到语句执行和/或提交之前.

然而有趣的是,至少在Linux系统上我看过执行语句的代码重复了读取文件头的步骤 - 因此,打开一个新的连接并不是那么糟糕,因为初始读取打开连接时,会将标头拉入系统的文件系统缓存中.因此,它归结为打开单个文件句柄的开销.

我还应该补充一点,如果你期望你的代码扩展到高并发性,那么SQLite可能是一个糟糕的选择.正如他们自己的网站所指出的那样,它并不适合高并发性,因为随着并发线程数量的增加,不得不通过单个全局锁来挤压所有访问的性能开始下降.如果你使用线程是为了方便,那很好,但如果你真的期望高度的并发性,那么我就避免使用SQLite.

简而言之,我不认为你每次打开的方式实际上都是那么糟糕.线程本地池可以提高性能吗?可能是.这种性能的提升是否会引人注目?在我看来,除非你看到很高的连接速率,否则你将拥有很多线程,所以你可能想要离开SQLite,因为它不能很好地处理并发性.如果您决定使用它,请确保在将连接返回池之前清除连接 - SQLAlchemy具有一些连接池功能,即使您不希望所有ORM层位于顶部,您也可能会发现这些功能很有用.

编辑

非常合理地指出我应该附上真实的时间.这些来自相当低功耗的VPS:

>>> timeit.timeit("cur = conn.cursor(); cur.execute('UPDATE foo SET name=\"x\"
    WHERE id=3'); conn.commit()", setup="import sqlite3;
    conn = sqlite3.connect('./testdb')", number=100000)
5.733098030090332
>>> timeit.timeit("conn = sqlite3.connect('./testdb'); cur = conn.cursor();
    cur.execute('UPDATE foo SET name=\"x\" WHERE id=3'); conn.commit()",
    setup="import sqlite3", number=100000)
16.518677949905396
Run Code Online (Sandbox Code Playgroud)

您可以看到差异大约3倍的因素,这并非无关紧要.但是,绝对时间仍然是亚毫秒级,因此除非您根据请求进行大量查询,否则可能还有其他优先考虑的地方.如果您执行大量查询,则合理的折衷可能是每个请求的新连接(但没有池的复杂性,只需每次重新连接).

对于读取(即SELECT),每次连接的相对开销将更高,但挂钟时间的绝对开销应该是一致的.

正如在这个问题的其他地方已经讨论过的那样,你应该用真实的查询进行测试,我只是想记录一下我做出的结论.