Эффективный способ обеспечить уникальные строки в SQLite3 - PullRequest
26 голосов
/ 03 марта 2011

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

Очевидным решением было использование составного первичного ключа с предложением конфликтаобрабатывать столкновения.Выше этого:

CREATE TABLE Event (Id INTEGER, Fld0 TEXT, Fld1 INTEGER, Fld2 TEXT, Fld3 TEXT, Fld4 TEXT, Fld5 TEXT, Fld6 TEXT);

стало так:

CREATE TABLE Event (Id INTEGER, Fld0 TEXT, Fld1 INTEGER, Fld2 TEXT, Fld3 TEXT, Fld4 TEXT, Fld5 TEXT, Fld6 TEXT, PRIMARY KEY (Fld0, Fld2, Fld3) ON CONFLICT REPLACE);

Это действительно обеспечивает ограничение уникальности, как мне это нужно.К сожалению, это изменение также влечет за собой снижение производительности, намного превышающее то, что я ожидал.Я провел несколько тестов, используя утилиту командной строки sqlite3, чтобы убедиться, что в остальной части моего кода нет ошибок.Тесты включают ввод 100 000 строк, либо в одной транзакции, либо в 100 транзакциях по 1000 строк в каждой.Я получил следующие результаты:

                                | 1 * 100,000   | 10 * 10,000   | 100 * 1,000   |
                                |---------------|---------------|---------------|
                                | Time  | CPU   | Time  | CPU   | Time  | CPU   |
                                | (sec) | (%)   | (sec) | (%)   | (sec) | (%)   |
--------------------------------|-------|-------|-------|-------|-------|-------|
No primary key                  | 2.33  | 80    | 3.73  | 50    | 15.1  | 15    |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld3               | 5.19  | 84    | 23.6  | 21    | 226.2 | 3     |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld2, Fld3         | 5.11  | 88    | 24.6  | 22    | 258.8 | 3     |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld0, Fld2, Fld3   | 5.38  | 87    | 23.8  | 23    | 232.3 | 3     |

В настоящее время мое приложение выполняет транзакции максимум в 1000 строк, и я был удивлен 15-кратным падением производительности.Я ожидал не более чем трехкратного снижения пропускной способности и увеличения загрузки ЦП, как это было в случае с транзакцией 100 тыс. Транзакций.Я предполагаю, что для индексации, связанной с поддержанием ограничений первичного ключа, требуется значительно большее количество синхронных операций с БД, поэтому в этом случае мои жесткие диски становятся узким местом.эффект - повышение производительности примерно на 15%.К сожалению, этого недостаточно.PRAGMA synchronous = NORMAL, похоже, не имеет никакого эффекта.

I может быть в состоянии восстановить некоторую производительность за счет увеличения размера транзакции, но я бы предпочел не делатьэто связано с увеличением использования памяти и опасениями по поводу скорости отклика и надежности.

Текстовые поля в каждой строке имеют переменную длину в среднем около 250 байт.Производительность запросов не имеет большого значения, но производительность вставки очень важна.Код моего приложения написан на C и (должен быть) переносим по крайней мере для Linux и Windows.

Есть ли способ улучшить производительность вставки без увеличения размера транзакции?Либо какая-то настройка в SQLite (что угодно, но не принудительно заставляет БД работать в асинхронном режиме) или программно в коде моего приложения?Например, есть ли способ обеспечить уникальность строки без использования индекса?

BOUNTY:

Используя метод хеширования / индексации, описанный в моем собственном ответе, яудалось несколько смягчить падение производительности до точки, где это, вероятно, приемлемо для моего приложения.Однако, похоже, что с увеличением числа строк в таблице наличие индекса делает вставки все медленнее и медленнее.

Меня интересует любой метод или тонкая настройка, которая повысит производительность в этомособый вариант использования, если он не требует взлома кода SQLite3 и не приводит к тому, что проект становится неприемлемым.

Ответы [ 5 ]

15 голосов
/ 22 марта 2011

Я использовал sqlite для вставки миллионов строк во время выполнения, и это то, что я использовал для увеличения производительности:

  • Используйте как можно меньше транзакций.
  • Используйте параметризованные командыдля вставки данных (подготовьте команду один раз и просто измените значения параметров в цикле)
  • Установите PRAGMA синхронно ВЫКЛ (не знаете, как это работает с WAL)
  • Увеличьтеразмер страницы базы данных.
  • Увеличение размера кэша. Это важный параметр, поскольку он приведет к тому, что sqlite будет фактически записывать данные на диск меньшее количество раз и будет выполнять больше операций в процессе создания памяти.весь процесс быстрее.
  • Если вам нужен индекс, добавьте его после вставки строк, выполнив необходимую команду sqlite.В этом случае вам нужно будет убедиться в уникальности самих себя, как вы это делаете сейчас.

Если вы попробуете это, пожалуйста, опубликуйте результаты вашего теста.Я верю, что это будет интересно всем.

8 голосов
/ 07 марта 2011

Предложение ON CONFLICT REPLACE заставит SQLite удалить существующие строки, а затем вставить новые строки.Это означает, что SQLite, вероятно, собирается потратить некоторое время

  • , удаляя существующие строки
  • , обновляя индексы
  • , вставляя новые строки
  • обновляяиндексы

Это мое мнение, основанное на документации SQLite и прочтении о других системах управления базами данных.Я не смотрел исходный код.

SQLite имеет два способа выражения ограничений уникальности: PRIMARY KEY и UNIQUE.Они оба создают индекс, хотя.

Теперь действительно важные вещи.,.

Здорово, что вы сделали тесты.Большинство разработчиков не делают этого.Но я думаю, что результаты вашего теста сильно вводят в заблуждение.

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

Если она не должна давать правильных ответов, я могу сделать это действительно, очень быстро.

Чтобы получить значимое время для вставки в таблицу без ключа, вам нужно либо

  • выполнить код до вставки новых данных, чтобы убедиться, что вы не нарушаете необъявленноеограничение первичного ключа, и чтобы убедиться, что вы обновляете существующие строки с правильными значениями (вместо вставки), или
  • код выполнения после вставки в эту таблицу для очистки дубликатов (Fld0,Fld2, Fld3) и для урегулирования конфликтов

И, конечно же, нужно учитывать и время, которое занимают эти процессы.

FWIW, я проверилвыполнение 100K SQL-операторов вставки в вашу схему в транзакциях по 1000 операторов, и это заняло всего 30 секунд.Одна транзакция из 1000 операторов вставки, которая, кажется, соответствует ожиданиям в рабочей среде, заняла 149 мсек.

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

4 голосов
/ 07 марта 2011

(Обычно я не отвечаю на свои вопросы, но я бы хотел документировать несколько идей / частичных решений для этого.)

Основная проблема с составным первичным ключом - это способ обработки индексов. Составные ключи подразумевают индекс для составного значения, что в моем случае означает индексирование строк . Хотя сравнение строковых значений не такое медленное, индексирование значения длиной, скажем, 500 байтов означает, что узлы B-дерева в индексе могут вмещать гораздо меньше указателей на строки / узлы, чем B-дерево, которое индексирует 64- бит целочисленное значение. Это означает загрузку гораздо большего количества страниц БД для каждого поиска по индексу, так как высота B-дерева увеличивается.

Для решения этой проблемы я изменил свой код так:

  • Используется Режим WAL . Увеличение производительности, безусловно, стоило такого небольшого изменения, поскольку у меня нет проблем с тем, что файл БД не является автономным.

  • Я использовал хеш-функцию MurmurHash3 - после перезаписи ее в C и адаптации - для получения одного 32-битного хеш-значения из значений полей, которые будут формировать ключ , Я хранил этот хэш в новом индексированном столбце. Поскольку это целочисленное значение, индекс довольно быстрый. Это единственный индекс для этой таблицы. Поскольку в таблице будет не более 10 000 000 строк, коллизии хеш-функции не будут влиять на производительность - хотя я не могу реально считать значение хеш-функции UNIQUE, индекс будет возвращать только одну строку в общем случае.

На данный момент есть две альтернативы, которые я кодировал и в настоящее время испытываю:

  • DELETE FROM Event WHERE Hash=? AND Fld0=? AND Fld2=? AND Fld3=?, за которым следует INSERT.

  • UPDATE Event SET Fld1=?,... WHERE Hash=? AND Fld0=? AND Fld2=? AND Fld3=?, за которым следует INSERT, если строки не обновлены.

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

EDIT:

На данный момент я согласился с использованием второго варианта, который действительно немного быстрее. Однако кажется, что любой вид индекса значительно замедляет SQLite3 по мере увеличения индексированной таблицы. Увеличение размера страницы БД до 8192 байт, кажется, немного помогает, но не так радикально, как хотелось бы.

3 голосов
/ 14 октября 2013

В дополнение ко всем остальным отличным ответам, одна вещь, которую вы можете сделать, это разделить данные на несколько таблиц.

Вставки SQLite становятся все медленнее и медленнее с увеличением числа строк, но если вы можете разделитьтаблица на несколько, эффект уменьшается (например: «names» -> «names_a», «names_b», ... для имен, начинающихся с буквы x).Позже вы можете сделать CREATE VIEW "names" AS SELECT * FROM "names_a" UNION SELECT * FROM "names_b" UNION ....

3 голосов
/ 03 марта 2011
Case When Exists((Select ID From Table Where Fld0 = value0 and Fld2 = value1 and Fld3 = value 2)) Then
    --Insert Statement
End

Я не на 100% уверен, что вставка работает так в SQLite, но я думаю, что так и должно быть. Это при правильной индексации полей Where должно быть достаточно быстрым. Однако это две транзакции, которые нужно учитывать.

...