Почему вставки замедляются так сильно, как растет БД? - PullRequest
3 голосов
/ 23 октября 2019

Я делаю личный проект, который генерирует много данных, и я подумал, что его хранение в локальной БД имеет смысл. Тем не менее, я вижу безумное замедление по мере роста БД, что делает его невозможным для работы.

Я сделал простой тест, показывающий, что я делаю. Я делаю словарь, где я делаю кучу локальной обработки (примерно 1 миллион записей), затем пакетно вставляю это в базу данных SQLite, затем зацикливаю и делаю все это снова. Вот код:

from collections import defaultdict
import sqlite3
import datetime
import random

def log(s):
    now = datetime.datetime.now()
    print(str(now) + ": " + str(s))

def create_table():
    conn = create_connection()
    with conn:
        cursor = conn.cursor()

        sql = """
            CREATE TABLE IF NOT EXISTS testing (
                test text PRIMARY KEY,
                number integer
            );"""
        cursor.execute(sql)
    conn.close()

def insert_many(local_db):
    sql = """INSERT INTO testing(test, number) VALUES(?, ?) ON CONFLICT(test) DO UPDATE SET number=number+?;"""

    inserts = []
    for key, value in local_db.items():
        inserts.append((key, value, value))

    conn = create_connection()
    with conn:
        cursor = conn.cursor()
        cursor.executemany(sql, inserts)
    conn.close()

def main():
    i = 0
    log("Starting to process records")
    for i in range(1, 21):
        local_db = defaultdict(int)
        for j in range(0, 1000000):
            s = "Testing insertion " + str(random.randrange(100000000))
            local_db[s] += 1
        log("Created local DB for " + str(1000000 * i) + " records")
        insert_many(local_db)
        log("Finished inserting " + str(1000000 * i) + " records")

def create_connection():
    conn = None
    try:
        conn = sqlite3.connect('/home/testing.db')
    except Error as e:
        print(e)

    return conn

if __name__ == '__main__':
    create_table()
    main()

Это отлично работает в течение секунды, а затем замедляется как сумасшедший. Вот результат, который я только что получил:

2019-10-23 15:28:59.211036: Starting to process records
2019-10-23 15:29:01.308668: Created local DB for 1000000 records
2019-10-23 15:29:10.147762: Finished inserting 1000000 records
2019-10-23 15:29:12.258012: Created local DB for 2000000 records
2019-10-23 15:29:28.752352: Finished inserting 2000000 records
2019-10-23 15:29:30.853128: Created local DB for 3000000 records
2019-10-23 15:39:12.826357: Finished inserting 3000000 records
2019-10-23 15:39:14.932100: Created local DB for 4000000 records
2019-10-23 17:21:37.257651: Finished inserting 4000000 records
...

Как видите, первый миллион вставок занимает 9 секунд, затем следующий миллион занимает 16, затем он увеличивается до 10 минут, затем час и 40 минут (!). Есть ли что-то странное, что я делаю, которое вызывает это сумасшедшее замедление, или это ограничение sqlite?

Ответы [ 2 ]

2 голосов
/ 23 октября 2019

(больше расширенного комментария, чем ответа)

SQLite поддерживает только индексы BTree. Для строк, которые могут иметь различную длину, дерево хранит идентификаторы строк. Сложность чтения дерева равна O (log (n)), где n - длина таблицы, однако она будет умножена на сложность чтения и сравнения строкового значения из таблицы. Поэтому, если для этого нет веских оснований, лучше иметь целочисленное поле в качестве первичного ключа.

Что в этом случае усугубляет ситуацию, вставляемые строки имеют довольно длинный общий префикс(«Тестирование вставки»), поэтому поиск первого несоответствия занимает больше времени.

Предложения по ускорению в порядке ожидаемого размера эффекта:

  • Хэш поддержки реальных баз данных (MariaDB, Postgres)индексы, которые решат эту проблему.
  • отключить автокоммит (пропустить ненужные записи на диск; очень дорого)
  • перевернуть текстовую строку (число перед фиксированным текстом) или даже оставить только часть числа
  • использовать массовые вставки (несколько записей в одном выражении)

Ответ @ peak позволяет избежать всей проблемы, не используя индекс. Если индекс вообще не требуется, это определенно способ.

2 голосов
/ 23 октября 2019

Используя вашу программу с одной (незначительной?) Модификацией, я получил очень разумные временные характеристики, показанные непосредственно ниже. Модификация заключалась в использовании sqlite3.connect вместо pysqlite.connect.

Синхронизация с использованием sqlite3.connect

Комментарии # являются приблизительными.

2019-10-23 13:00:37.843759: Starting to process records
2019-10-23 13:00:40.253049: Created local DB for 1000000 records
2019-10-23 13:00:50.052383: Finished inserting 1000000 records          # 12s
2019-10-23 13:00:52.065007: Created local DB for 2000000 records
2019-10-23 13:01:08.069532: Finished inserting 2000000 records          # 18s
2019-10-23 13:01:10.073701: Created local DB for 3000000 records
2019-10-23 13:01:28.233935: Finished inserting 3000000 records          # 20s
2019-10-23 13:01:30.237968: Created local DB for 4000000 records
2019-10-23 13:01:51.052647: Finished inserting 4000000 records          # 23s
2019-10-23 13:01:53.079311: Created local DB for 5000000 records
2019-10-23 13:02:15.087708: Finished inserting 5000000 records          # 24s
2019-10-23 13:02:17.075652: Created local DB for 6000000 records
2019-10-23 13:02:41.710617: Finished inserting 6000000 records          # 26s
2019-10-23 13:02:43.712996: Created local DB for 7000000 records
2019-10-23 13:03:18.420790: Finished inserting 7000000 records          # 37s
2019-10-23 13:03:20.420485: Created local DB for 8000000 records
2019-10-23 13:04:03.287034: Finished inserting 8000000 records          # 45s
2019-10-23 13:04:05.593073: Created local DB for 9000000 records
2019-10-23 13:04:57.871396: Finished inserting 9000000 records          # 54s
2019-10-23 13:04:59.860289: Created local DB for 10000000 records       
2019-10-23 13:05:54.527094: Finished inserting 10000000 records # 57s
...

Стоимость TEXTПЕРВИЧНЫЙ КЛЮЧ

Я считаю, что основной причиной замедления является определение test как ПЕРВИЧНОГО КЛЮЧА ТЕКСТА. Это влечет за собой огромные затраты на индексирование, как предлагает этот фрагмент из прогона, в котором удаляются объявления «PRIMARY KEY» и «ON CONFLICT»:

2019-10-23 13:26:22.627898: Created local DB for 10000000 records
2019-10-23 13:26:24.010171: Finished inserting 10000000 records
...
2019-10-23 13:26:58.350150: Created local DB for 20000000 records
2019-10-23 13:26:59.832137: Finished inserting 20000000 records

Это меньше 1,4 с на 10-миллионномотметка рекордов, и не намного больше отметки в 20 миллионов записей.

...