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

时间:2013-01-24 21:42:19

标签: 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

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

4 个答案:

答案 0 :(得分:15)

我添加了自己的答案,因为我不同意目前接受的答案。它声明该操作不是线程安全的,但这是完全错误的 - SQLite uses file locking适合其当前平台,以确保所有访问都符合ACID

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

正如@abernert在评论SQLite has had issues with threads中已经指出的那样,但这与在线程之间共享单个连接有关。正如他还提到的那样,这意味着如果您使用应用程序范围的池,如果第二个线程从池中提取循环连接,则会出现运行时错误。这些也是你在测试时可能没有注意到的那些烦人的错误(轻负载,也许只有一个线程在使用中),但后来很容易引起头痛。 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可能是一个糟糕的选择。由于their own website points out它不适合高并发性,因为必须通过单个全局锁挤压所有访问的性能损失随着并发线程数量的增加而开始咬合。如果您为了方便起见使用线程,那很好,但如果您真的期望高度并发,那么我就避免使用SQLite。

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

您可以看到差异大约3倍的因素,这并不重要。但是,绝对时间仍然是亚毫秒级,因此除非您对每个请求进行大量查询,否则可能还有其他地方需要先进行优化。如果你做了很多查询,合理的妥协可能是每个请求的新连接(但没有池的复杂性,只需每次重新连接)。

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

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

答案 1 :(得分:2)

如果你想知道某些事情是多么低效,那就写一个测试并亲自看看。

一旦我修复了错误,让你的示例首先工作,并编写代码来创建一个测试用例来运行它,弄清楚如何用timeit计算时间与通常一样微不足道是

请参阅http://pastebin.com/rd39vkVa

那么,当你运行它会发生什么?

$ python2.7 sqlite_test.py 10000
reopen: 2.02089715004
reuse:  0.278793811798
$ python3.3 sqlite_test.py 10000
reopen: 1.8329595914110541
reuse:  0.2124928394332528
$ pypy sqlite_test.py 10000
reopen: 3.87628388405
reuse:  0.760829925537

因此,打开数据库所需的时间大约是对几乎没有返回任何内容的近空表运行死简单查询的时间的4到8倍。这是你最糟糕的情况。

答案 2 :(得分:0)

启动非常低效,而且不是线程安全的。

使用合适的连接池库。 sqlalchemy提供了更多的游泳池,或者为sqlite找到了一个重量更轻的游泳池。

答案 3 :(得分:0)

为什么不每隔N秒重新连接一次。在我的ajax先行/数据库服务中,我每小时重新连接30-40行瓶子以获取更新, 如果你需要处理实时数据,有更好的数据库:

t0 = time.time()
con = None
connect_interval_in_sec = 3600

def myconnect(dbfile=<path to dbfile>):
    try:
        mycon = sqlite3.connect(dbfile)
        cur = mycon.cursor()
        cur.execute('SELECT SQLITE_VERSION()')
        data = cur.fetchone()
    except sqlite3.Error as e:
        print("Error:{}".format(e.args[0]))
        sys.exit(1)
    return mycon

在主循环中:

if con is None or time.time()-t0 > connect_interval_in_sec:
    con = myconnect()
    t0 = time.time()
<do your query stuff on con>