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

eto*_*cky 17 python performance sql-insert

背景

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

环境

英特尔酷睿 i7-7700 @ 3.6GHz
16GB RAM
美光 1100 256GB 固态硬盘,Windows 10 x64
Python 3.6.5 Minconda
sqlite3.version 2.6.0

生成数据.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)
Run Code Online (Sandbox Code Playgroud)

输入.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
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

基本输入/输出

with open('input.ssv', 'r') as infile:
    infile.read()
Run Code Online (Sandbox Code Playgroud)

导入时间为 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
Run Code Online (Sandbox Code Playgroud)

导入时间为 0.26 秒或每秒 3.84 M

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

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

导入时间为 0.23 秒或每秒 4.32 M

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

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

1. 写文件

outfile.write(line)
Run Code Online (Sandbox Code Playgroud)

以 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
Run Code Online (Sandbox Code Playgroud)

导入时间为 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))
Run Code Online (Sandbox Code Playgroud)

当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))
Run Code Online (Sandbox Code Playgroud)

导入时间为 7.50 秒或每秒 133,296

这与写作BEGIN, BEGIN TRANSACTIONor BEGIN DEFERRED TRANSACTION, not BEGIN IMMEDIATEnor 相同BEGIN EXCLUSIVE

5.通过准备好的语句插入

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

c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(sym,bid,ask)])
Run Code Online (Sandbox Code Playgroud)

导入时间为 2.31 秒或每秒 432,124

6.关闭同步

当同步未设置为EXTRAFULL数据到达物理磁盘表面之前,电源故障会损坏数据库文件。当我们可以保证电源和OS是健康的时候,我们可以把同步转为同步,OFF这样数据交给OS层后就不会同步了。

conn = sqlite3.connect('example.db', isolation_level='DEFERRED')
c = conn.cursor()
c.execute('''PRAGMA synchronous = OFF''')
Run Code Online (Sandbox Code Playgroud)

导入时间为 2.25 秒或每秒 444,247

7.关闭日志,所以没有回滚和原子提交

在某些应用程序中,不需要数据库的回滚功能,例如时间序列数据插入。当我们可以保证电源和操作系统是健康的,我们可以把journal_modeoff使回滚日志被完全禁止和禁止原子提交和回滚功能。

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

在 2.22 秒或每秒 450,653 次导入

8. 使用内存数据库

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

conn = sqlite3.connect(":memory:")
Run Code Online (Sandbox Code Playgroud)

在 2.17 秒或每秒 460,405 次导入

9. 循环中更快的 Python 代码

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

9a. 避免赋值给变量

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

导入时间为 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:]))])
Run Code Online (Sandbox Code Playgroud)

导入时间为 1.94 秒或每秒 514,661

9c。避免 float() 到 ?

当我们使用executemany()?占位符,我们并不需要把字符串转换成浮动提前。

executemany("INSERT INTO stocks VALUES (?,?,?)", [(line[0:6], line[7:14], line[15:])])
Run Code Online (Sandbox Code Playgroud)

导入时间为 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()
Run Code Online (Sandbox Code Playgroud)

导入时间为 1.77 秒或每秒 564,611

有可能变得更快吗?

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

qwr*_*qwr 2

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

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