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  
我使用与我的真实数据相同的格式生成了 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)
测试数据看起来像这样。
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 字节的文件大小。
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 和字符串处理操作有多快。
outfile.write(line)
以 0.52 秒或每秒 1.93 M 导入
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
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。
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
这与写作BEGIN, BEGIN TRANSACTIONor BEGIN DEFERRED TRANSACTION, not BEGIN IMMEDIATEnor 相同BEGIN EXCLUSIVE。
使用上面的事务得到了令人满意的结果,但需要注意的是,使用 Python 的字符串操作是不可取的,因为它会受到 SQL 注入的影响。此外,与参数替换相比,使用字符串速度较慢。
c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(sym,bid,ask)])
导入时间为 2.31 秒或每秒 432,124
当同步未设置为EXTRA或FULL数据到达物理磁盘表面之前,电源故障会损坏数据库文件。当我们可以保证电源和OS是健康的时候,我们可以把同步转为同步,OFF这样数据交给OS层后就不会同步了。
conn = sqlite3.connect('example.db', isolation_level='DEFERRED')
c = conn.cursor()
c.execute('''PRAGMA synchronous = OFF''')
导入时间为 2.25 秒或每秒 444,247
在某些应用程序中,不需要数据库的回滚功能,例如时间序列数据插入。当我们可以保证电源和操作系统是健康的,我们可以把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 次导入
在某些应用程序中,不需要将数据写回磁盘,例如向 Web 应用程序提供查询数据的应用程序。
conn = sqlite3.connect(":memory:")
在 2.17 秒或每秒 460,405 次导入
我们应该考虑将计算的每一位都保存在一个密集循环中,例如避免分配给变量和字符串操作。
tokens = line.split()
c.executemany("INSERT INTO stocks VALUES (?,?,?)", [(tokens[0], float(tokens[1]), float(tokens[2]))])
导入时间为 2.10 秒或每秒 475,964
当我们可以将空格分隔的数据作为固定宽度格式处理时,我们可以直接表示每个数据到数据头部的距离。这意味着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
当我们使用executemany()与?占位符,我们并不需要把字符串转换成浮动提前。
executemany("INSERT INTO stocks VALUES (?,?,?)", [(line[0:6], line[7:14], line[15:])])
导入时间为 1.59 秒或每秒 630,520
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 M 次插入。在 SQLite 中你还能想到什么让它更快?如果不是 SQLite 而是其他数据库系统怎么办?