如何提高 Python 3.6 中的 SQLite 插入性能?

Posted

技术标签:

【中文标题】如何提高 Python 3.6 中的 SQLite 插入性能?【英文标题】:How to improve SQLite insert performance in Python 3.6? 【发布时间】:2019-02-08 02:11:37 【问题描述】:

背景

我想使用 Python 向 SQLite 插入 100 万条记录。我尝试了很多方法来改进它,但仍然不是很满意。数据库使用 0.23 秒将文件加载到内存(在下面搜索 pass)但 SQLite 加载和插入文件需要 1.77 秒。

环境

英特尔酷睿 i7-7700 @ 3.6GHz 16GB 内存 美光 1100 256GB SSD,Windows 10 x64 Python 3.6.5 明康达 sqlite3.version 2.6.0

GenerateData.py

我生成了与我的真实数据格式相同的 100 万个测试输入数据。

import time
start_time = time.time()
with open('input.ssv', 'w') as out:
    symbols = ['AUDUSD','EURUSD','GBPUSD','NZDUSD','USDCAD','USDCHF','USDJPY','USDCNY','USDHKD']
    lines = []
    for i in range(0,1*1000*1000):
        q1, r1, q2, r2 = i//100000, i%100000, (i+1)//100000, (i+1)%100000
        line = ' .:05d .:05d'.format(symbols[i%len(symbols)], q1, r1, q2, r2)
        lines.append(line)
    out.write('\n'.join(lines))
print(time.time()-start_time, i)

input.ssv

测试数据如下所示。

AUDUSD 0.00000 0.00001
EURUSD 0.00001 0.00002
GBPUSD 0.00002 0.00003
NZDUSD 0.00003 0.00004
USDCAD 0.00004 0.00005
...
USDCHF 9.99995 9.99996
USDJPY 9.99996 9.99997
USDCNY 9.99997 9.99998
USDHKD 9.99998 9.99999
AUDUSD 9.99999 10.00000
// total 1 million of lines, taken 1.38 second for Python code to generate to disk

Windows 正确显示 23,999,999 字节的文件大小。

基线代码 InsertData.py

import time
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, *args):
        elapsed = time.time()-self.start
        print('Imported in :.2f seconds or :.0f per second'.format(elapsed, 1*1000*1000/elapsed)) 

with Timer() as t:
    with open('input.ssv', 'r') as infile:
        infile.read()

基本输入输出

with open('input.ssv', 'r') as infile:
    infile.read()

0.13 秒或 7.6 M/秒导入

它测试读取速度。

with open('input.ssv', 'r') as infile:
    with open('output.ssv', 'w') as outfile:
        outfile.write(infile.read()) // insert here

0.26 秒或每秒 3.84 M 导入

它在不解析任何东西的情况下测试读写速度

with open('input.ssv', 'r') as infile:
    lines = infile.read().splitlines()
    for line in lines:
        pass # do insert here

0.23 秒或每秒 4.32 M 导入

当我逐行解析数据时,它实现了非常高的输出。

这让我们了解我的测试机器上的 IO 和字符串处理操作有多快。

1。写入文件

outfile.write(line)

0.52 秒或每秒 1.93 M 导入

2。拆分为浮点数到字符串

tokens = line.split()
sym, bid, ask = tokens[0], float(tokens[1]), float(tokens[2])
outfile.write(' :.5f %.5f\n'.format(sym, bid, ask)) // real insert here

在 2.25 秒或 445 K/秒内导入

3。使用自动提交插入语句

conn = sqlite3.connect('example.db', isolation_level=None)
c.execute("INSERT INTO stocks VALUES ('',:.5f,:.5f)".format(sym,bid,ask))

当isolation_level = None(自动提交)时,程序需要好几个小时才能完成(我等不了这么长时间)

请注意,输出数据库文件大小为 32,325,632 字节,即 32MB。它比输入文件 ssv 文件大小 23MB 大 10MB。

4。使用 BEGIN (DEFERRED) 插入语句

conn = sqlite3.connect('example.db', isolation_level=’DEFERRED’) # default
c.execute("INSERT INTO stocks VALUES ('',:.5f,:.5f)".format(sym,bid,ask))

在 7.50 秒内导入或每秒 133,296 个

这与写BEGINBEGIN TRANSACTIONBEGIN DEFERRED TRANSACTION 相同,而不是BEGIN IMMEDIATEBEGIN EXCLUSIVE

5。按准备好的语句插入

使用上面的事务可以得到令人满意的结果,但需要注意的是,使用 Python 的字符串操作是不受欢迎的,因为它会受到 SQL 注入。此外,与参数替换相比,使用字符串速度较慢。

c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(sym,bid,ask)])

在 2.31 秒内导入或每秒 432,124 个

6。关闭同步

如果在数据到达物理磁盘表面之前未将同步设置为EXTRAFULL,电源故障会损坏数据库文件。在保证电源和操作系统健康的情况下,我们可以将同步到OFF,这样在数据交给操作系统层后就不同步了。

conn = sqlite3.connect('example.db', isolation_level='DEFERRED')
c = conn.cursor()
c.execute('''PRAGMA synchronous = OFF''')

在 2.25 秒内导入或每秒 444,247 个

7。关闭日志,因此没有回滚或原子提交

在某些应用程序中,不需要数据库的回滚功能,例如时间序列数据插入。当我们可以确保电源和操作系统健康时,我们可以将journal_mode 转为off 以便完全禁用回滚日志并禁用原子提交和回滚功能。

conn = sqlite3.connect('example.db', isolation_level='DEFERRED')
c = conn.cursor()
c.execute('''PRAGMA synchronous = OFF''')
c.execute('''PRAGMA journal_mode = OFF''')

在 2.22 秒内导入或每秒 450,653 个

8。使用内存数据库

在某些应用程序中不需要将数据写回磁盘,例如向 Web 应用程序提供查询数据的应用程序。

conn = sqlite3.connect(":memory:")

在 2.17 秒内导入或每秒 460,405 个

9。循环中更快的 Python 代码

我们应该考虑将每一点计算都保存在一个密集的循环中,例如避免分配给变量和字符串操作。

9a。避免分配给变量

tokens = line.split()
c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(tokens[0], float(tokens[1]), float(tokens[2]))])

在 2.10 秒内导入或每秒 475,964 个

9b。避免 string.split()

当我们可以将空格分隔的数据视为固定宽度格式时,我们可以直接表示每个数据到数据头部的距离。 这意味着line.split()[1] 变为line[7:14]

c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(line[0:6], float(line[7:14]), float(line[15:]))])

在 1.94 秒内导入或每秒 514,661 个

9c。避免 float() 到 ?

当我们使用executemany()? 占位符时,我们不需要事先将字符串转为浮点数。

executemany("INSERT INTO stocks VALUES (?,?,?)", [(line[0:6], line[7:14], line[15:])])

在 1.59 秒内导入或每秒 630,520 个

10.迄今为止最快的全功能和健壮的代码

import time
class Timer:    
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, *args):
        elapsed = time.time()-self.start
        print('Imported in :.2f seconds or :.0f per second'.format(elapsed, 1*1000*1000/elapsed))
import sqlite3
conn = sqlite3.connect('example.db')
c = conn.cursor()
c.execute('''DROP TABLE IF EXISTS stocks''')
c.execute('''CREATE TABLE IF NOT EXISTS stocks
             (sym text, bid real, ask real)''')
c.execute('''PRAGMA synchronous = EXTRA''')
c.execute('''PRAGMA journal_mode = WAL''')
with Timer() as t:
    with open('input.ssv', 'r') as infile:
        lines = infile.read().splitlines()
        for line in lines:
            c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(line[0:6], line[7:14], line[15:])])
        conn.commit()
        conn.close()

在 1.77 秒内导入或每秒 564,611 个

有可能变得更快吗?

我有一个 23MB 的文件,其中包含 100 万条记录,由一段文本作为符号名称和 2 个浮点数作为买价和卖价。当您在上面搜索pass 时,测试结果显示每秒向纯文件插入 4.32 M。当我插入到一个健壮的 SQLite 数据库时,它下降到每秒 0.564 万次插入。你可能会想到什么让它在 SQLite 中更快?如果不是 SQLite 而是其他数据库系统呢?

【问题讨论】:

我的问题是否正确:每秒 50 万次插入 SQLite 对您来说太慢了? @KlausD。数据库使用 0.23 秒(在上面搜索 pass)将文件加载到内存,但 SQLite 加载和插入文件需要 1.77 秒。不太慢,但我想让它更快。看看你是否可以判断它是否可能非常接近软件瓶颈或任何优化它的方法。 不幸的是,性能优化不是 SO 的主题。您可能会在 Code Review 或与数据库相关的兄弟网站上找到帮助。 伟大的研究! 6 & 7 为我做了诀窍。我之前使用的是内存数据库,但禁用保护措施让我在使用REPLACE INTO 的 SSD 上达到相似的速度。 【参考方案1】:

如果python的解释器实际上是计时(第9节)与SQLite性能的重要因素,您可能会发现PyPy可以显着提高性能(Python的sqlite3接口是用纯python实现的。)这里用纯python做的不多,但是如果您正在执行 cPython 尚未使用 C 实现优化的某些操作,例如通用整数操作,那么从 cPython 切换可能是值得的(优化的黄金法则:配置文件!)

显然,如果SQLite 之外的性能 真的很重要,您可以尝试使用更快的语言(如 C/C++)编写。多线程可能有帮助,也可能没有帮助,具体取决于数据库锁的实现方式。

【讨论】:

以上是关于如何提高 Python 3.6 中的 SQLite 插入性能?的主要内容,如果未能解决你的问题,请参考以下文章

如何解决 Python 3.6 中的 UnicodeDecodeError?

如何使用scipy 1.0.0计算python 3.6中的VIF?

如何阻止不和谐机器人响应自身/所有其他机器人 [Python 3.6 中的不和谐机器人]

如何提高效率SQLite大数据量操作效率

如何从python中的SQLite查询中获取单个结果?

如何使用循环打印 sqlite3 中的表以及 python 中的列名以及如何准确获取列名?