Как улучшить производительность вставки SQLite в Python 3.6? - PullRequest
0 голосов
/ 03 сентября 2018

Фон

Я бы хотел вставить 1 миллион записей в SQLite с использованием Python. Я попробовал несколько способов улучшить это, но это все еще не так удовлетворено. Файл базы данных загружается в память, используя 0,23 секунды (поиск pass ниже), но SQLite 1,77 секунды для загрузки и вставки в файл.

Окружающая среда

Intel Core i7-7700 @ 3,6 ГГц
16 ГБ ОЗУ
Микрон 1100 256 ГБ SSD, Windows 10 x64
Python 3.6.5 Minconda
sqlite3.version 2.6.0

GenerateData.py

Я генерирую 1 миллион тестовых входных данных в том же формате, что и мои реальные данные.

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()

Basic I / O

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

Импортируется за 0,13 секунды или 7,6 М в секунду

Тестирует скорость чтения.

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

Импортируется за 0,26 секунды или 3,84 М в секунду

Тестирует скорость чтения и записи без разбора чего-либо

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

Импортируется за 0,23 секунды или 4,32 М в секунду

Когда я анализирую данные построчно, получается очень высокий вывод.

Это дает нам представление о том, насколько быстро выполняются операции ввода-вывода и обработки строк на моем тестовом компьютере.

1. Записать файл

outfile.write(line)

Импортируется за 0,52 секунды или 1,93 М в секунду

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 К / с

3. Вставить оператор с автокоммитом

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

Когда уровень изоляции = Нет (автокоммит), программе требуется много часов для завершения (я не мог ждать такие долгие часы)

Обратите внимание, что размер выходного файла базы данных составляет 32 325 632 байта, что составляет 32 МБ. Это больше, чем размер входного файла ssv 23 МБ на 10 МБ.

4. Вставить оператор с НАЧАЛО (ОТЛОЖЕНО)

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 TRANSACTION или BEGIN DEFERRED TRANSACTION, а не BEGIN IMMEDIATE или BEGIN EXCLUSIVE.

5. Вставить подготовленным заявлением

Использование приведенной выше транзакции дает удовлетворительные результаты, но следует отметить, что использование строковых операций в Python нежелательно, поскольку он подвергается SQL-инъекции. Более того, использование строки медленнее по сравнению с подстановкой параметров.

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

Импортируется за 2,31 секунды или 432 124 в секунду

6. Выключить синхронный

Сбой питания приводит к повреждению файла базы данных, если для параметра синхронизации не установлено значение EXTRA или FULL до того, как данные достигнут поверхности физического диска. Когда мы сможем убедиться, что питание и ОС исправны, мы можем включить синхронный режим 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. Использование базы данных в памяти

В некоторых приложениях запись данных на диски не требуется, например, приложения, предоставляющие запрашиваемые данные веб-приложениям.

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

Импортируется за 2,17 секунды или 460,405 в секунду

9. Более быстрый код Python в цикле

Мы должны рассмотреть возможность сохранения каждого бита вычислений в интенсивном цикле, например, избегая присвоения переменным и строковым операциям.

9а. Избегайте присвоения переменной

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() с ? заполнителем, нам не нужно заранее превращать строку в float.

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 в секунду

Можно быстрее?

У меня есть файл размером 23 МБ с 1 миллионом записей, составляющих фрагмент текста в качестве имени символа и 2 числа с плавающей запятой в качестве bid и ask. При поиске pass выше, результат теста показывает 4,32 млн вставок в секунду в обычный файл. Когда я вставляю в надежную базу данных SQLite, она падает до 0,564 млн вставок в секунду. Что еще вы можете подумать, чтобы сделать это еще быстрее в SQLite? Что если не SQLite, а другая система баз данных?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...