RussianLDP Рейтинг@Mail.ru
WebMoney: 
WMZ Z294115950220 
WMR R409981405661 
WME E134003968233 
Visa 
4274 3200 2453 6495 

Small. Fast. Reliable.
Choose any three.

1. Обзор

SQLite читает и пишет маленькие blob (например, изображения миниатюр) на 35% быстрее, чем те же самые объекты могут быть прочитаны или написаны с отдельными файлами на диске через fread() или fwrite().

Кроме того, единственная база данных SQLite, содержащая 10-килобайтные blob, использует приблизительно на 20% меньше дискового пространства, чем хранение blob в отдельных файлах.

Различие в производительности возникает (мы верим), потому что, работая от базы данных SQLite, системные вызовы open() и close() вызваны только однажды, тогда как open() и close() вызваны однажды для каждого blob, сохраненных в отдельных файлах. Кажется, что издержки запроса open() и close() больше, чем издержки использования базы данных. Сокращение размера является результатом того, что отдельные файлы увеличены к следующему кратному числу размера блока файловой системы, тогда как blob упакованы более плотно в базу данных SQLite.

Измерения в этой статье были сделаны в течение недели 2017-06-05 использования версий SQLite между 3.19.2 и 3.20.0. Можно ожидать, что будущие версии SQLite выступят еще лучше.

1.1. Протесты

Число 35% выше приблизительно. Фактическое время варьируется в зависимости от аппаратных средств, операционной системы и деталей эксперимента и из-за случайных исполнительных колебаний на реальных аппаратных средствах. См. текст ниже для большего количества деталей. Попробуйте эксперименты сами. Сообщите о значительных отклонениях на SQLite forum.

35% основано на запуске тестов на каждой машине, которую автор имеет под рукой. Некоторые рецензенты этой статьи сообщают, что у SQLite есть более высокое время ожидания, чем прямой I/O на их системах. Мы еще не понимаем различия. Мы также видим признаки, что SQLite не использует прямой I/O, когда экспериментами управляют, используя холодный кэш файловой системы.

Время ожидания чтения-записи для SQLite конкурентоспособно по отношению ко времени ожидания чтения-записи отдельных файлов на диске. Часто SQLite быстрее. Иногда SQLite почти как быстро. Так или иначе эта статья опровергает общее предположение, что реляционная база данных должна быть медленнее, чем I/O прямой файловой системы.

2022 study ( альтернативная ссылка на GitHub) нашло, что SQLite примерно вдвое более быстр при реальной рабочей нагрузке по сравнению с Btrfs и Ext4 в Linux.

Jim Gray и другие изучили производительность чтения BLOB против файлового ввода-вывода для Microsoft SQL Server и нашли, что чтение BLOB из базы данных было быстрее для размеров BLOB меньше, чем между 250 250 KB и 1 MB ( здесь). В том исследовании база данных все еще хранит имя файла содержания, даже если содержание проводится в отдельном файле. Таким образом, с базой данных консультируются для каждого BLOB, даже если это только, чтобы извлечь имя файла. В этой статье ключ для BLOB это имя файла, таким образом, никакой предварительный доступ к базе данных не требуется. Поскольку база данных никогда не используется вообще, читая содержание от отдельных файлов в этой статье, порог где прямой доступ I/O к файлу становится быстрее, меньше, чем в статье Грэя.

Статья Internal Versus External BLOBs на этом веб-сайте это более раннее исследование (приблизительно 2011), которое использует тот же самый подход, как в статье Джима Грэя, хранение имен файлов blob как записи в базе данных, но для SQLite вместо SQL Server.

2. Как эти измерения сделаны

Производительность I/O измерена, используя программу kvtest.c из исходного дерева SQLite. Чтобы собрать эту тестовую программу, сначала поместите исходный файл kvtest.c в каталог с исходными файлами объединения SQLite "sqlite3.c" и "sqlite3.h". Затем в unix выполните:

gcc -Os -I. -DSQLITE_DIRECT_OVERFLOW_READ \ kvtest.c sqlite3.c \
    -o kvtest -ldl -lpthread

Или в Windows с MSVC:

cl -I. -DSQLITE_DIRECT_OVERFLOW_READ kvtest.c sqlite3.c

Инструкции для сборки в Android даны ниже.

Используйте получающуюся программу "kvtest", чтобы произвести испытательную базу данных с 100000 случайных несжимаемых blob, каждый со случайным размером между 8000 и 12000 байт, используя команду:

./kvtest init test1.db --count 100k --size 10k --variance 2k

При желании можно проверить новую базу данных, управляя этой командой:

./kvtest stat test1.db

Затем, превратите копии всех blob в отдельные файлы в каталоге, используя команду:

./kvtest export test1.db test1.dir

В этом пункте можно измерить сумму дискового пространства, используемого базой данных test1.db и каталогом test1.dir со всем его содержанием. На стандартном Ubuntu Linux файл базы данных составил 1,024,512,000 байт, каталог test1.dir 1,228,800,000 байт (согласно "du -k"), приблизительно на 20% больше, чем база данных.

Каталог "test1.dir", созданный выше, помещает все blob в единственную папку. Это было предугадано, что некоторые операционные системы выступят плохо, когда единственный каталог будет содержать 100000 объектов. Чтобы проверить это, программа kvtest может также сохранить blob в иерархии папок не больше, чем по 100 файлов и/или подкаталогов на папку. Альтернативное представление на диске blob может быть создано, используя параметр командной строки --tree для команды "export":

./kvtest export test1.db test1.tree --tree

Каталог test1.dir будет содержать 100000 файлов с именами "000000", "000001", "000002" и т.д., но test1.tree будет содержать те же самые файлы в подкаталогах "00/00/00", "00/00/01" и т.д. Каталог test1.dir и test1.test занимают приблизительно ту же самую сумму места, хотя test1.test очень немного больше из-за дополнительных записей каталога.

Все эксперименты, которые следуют, управляют тем же самым "test1.dir" или "test1.tree". Очень мало различия в производительности измерено в любом случае, независимо от операционной системы.

Измерьте производительность для чтения blob от базы данных и от отдельных файлов, используя эти команды:

./kvtest run test1.db --count 100k --blob-api
./kvtest run test1.dir --count 100k --blob-api
./kvtest run test1.tree --count 100k --blob-api

В зависимости от вашей аппаратной и операционной системы необходимо видеть, что это читает из файла базы данных test1.db приблизительно на 35% быстрее, чем читает от отдельных файлов в test1.dir или test1.tree. Результаты могут измениться значительно от одного пробега до следующего из-за кэширования. Таким образом, желательно запустить тесты многократно и взять среднее число или худший случай или лучший случай, в зависимости от ваших требований.

Опция --blob-api на тесте чтения базы данных заставляет kvtest использовать sqlite3_blob_read() в SQLite, чтобы загрузить содержание blob вместо того, чтобы управлять чистыми SQL-операторами. Это помогает SQLite работать немного быстрее на тестах чтения. Можно опустить тот выбор, чтобы сравнить исполнение SQLite, управляющего SQL-операторами. В этом случае SQLite все еще выигрывает прямое чтение, хотя не так, как используя sqlite3_blob_read(). Опция --blob-api проигнорирована для тестов, которые читают файлы с диска.

Измерьте производительность записи, добавив опцию --update. Это вызывает перезапись blob на месте другим случайным blob точно того же самого размера.

./kvtest run test1.db --count 100k --update
./kvtest run test1.dir --count 100k --update
./kvtest run test1.tree --count 100k --update

Тест на запись выше не абсолютно справедлив, так как SQLite делает безопасные по питанию транзакции, тогда как запись прямо на диск этого не умеет. Чтобы поместить тесты в более равные условия, добавьте опцию --nosync, чтобы отключить запрос fsync() или FlushFileBuffers(), чтобы вынудить сброс содержания к диску или используйте опцию --fsync для тестов прямой записи на диск, чтобы вынудить их вызвать fsync() или FlushFileBuffers(), обновляя дисковые файлы.

По умолчанию kvtest управляет измерениями I/O базы данных в одной транзакции. Используйте опцию --multitrans, чтобы каждое чтение или запись выполнялась в отдельной транзакции. Опция --multitrans делает SQLite намного медленнее и неконкурентоспособным по отношению к прямому дисковому I/O. Этот выбор доказывает, что чтобы получить большую производительность SQLite, необходимо сгруппировать как можно больше взаимодействия базы данных в единственной транзакции.

Есть много других вариантов тестирования, которые могут быть найдены, управляя командой:

./kvtest help

2.1. Измерения производительности чтения

Диаграмма ниже показывает данные, собранные, используя kvtest.c на пяти различных системах:

  • Win7: A circa-2009 Dell Inspiron laptop, Pentium dual-core at 2.30GHz, 4GiB RAM, Windows7.
  • Win10: A 2016 Lenovo YOGA 910, Intel i7-7500 at 2.70GHz, 16GiB RAM, Windows10.
  • Mac: A 2015 MacBook Pro, 3.1GHz intel Core i7, 16GiB RAM, MacOS 10.12.5
  • Ubuntu: Desktop built from Intel i7-4770K at 3.50GHz, 32GiB RAM, Ubuntu 16.04.2 LTS
  • Android: Galaxy S3, ARMv7, 2GiB RAM

Все машины используют SSD кроме Win7, у которой есть жесткий диск. Испытательная база данных это 100K blob с размерами, однородно распределенными между 8K и 12K для в общей сложности приблизительно 1 гигабайта содержания. Размер страницы базы данных составляет 4 KB. Выбор времени компиляции -DSQLITE_DIRECT_OVERFLOW_READ использовался для всех этих тестов. Тесты были запущены многократно. Первый показ использовался, чтобы нагреть кэш, и от его времени отказались.

Диаграмма ниже показывает среднее время, чтобы прочитать blob непосредственно от файловой системы против времени, чтобы прочитать тот же самый blob из БД SQLite. Фактическое время варьируется значительно от одной системы до другой (Ubuntu desktop намного быстрее, чем телефон Galaxy S3, например). Эта диаграмма показывает отношение времени, чтобы прочитать данные из файла, разделенное на время, чтобы прочитать от базы данных. Крайний левый столбец в диаграмме это нормализованное время, чтобы читать от базы данных для справки.

В этой диаграмме, SQL-оператор ("SELECT v FROM kv WHERE k=?1") подготовлен однажды. Затем для каждого blob, значение ключа blob связано с параметром ?1, и запросы оценены, чтобы извлечь содержание blob.

Диаграмма показывает, что на Windows10, содержание может быть прочитано из базы данных SQLite приблизительно в 5 раз быстрее, чем это может быть прочитано непосредственно с диска. На Android SQLite приблизительно только на 35% быстрее, чем чтение с диска.


Диаграмма 1: Чтение SQLite, время ожидания относительно прямой файловой системы.
100K blob, avg 10KB каждый, произвольный порядок, используя SQL.

Работа может быть улучшена немного, обойдя слой SQL и читая содержание непосредственно, используя sqlite3_blob_read() , примерно вот так:


Диаграмма 2: Чтение SQLite, время ожидания относительно прямой файловой системы.
100K blob, avg 10KB, произвольный порядок через sqlite3_blob_read().

Дальнейшая работа улучшается при помощи особенности memory-mapped I/O SQLite. В следующей диаграмме весь файл базы данных 1GB с отображенной памятью, и blob прочитаны (в произвольном порядке) с использованием sqlite3_blob_read(). С этой оптимизацией SQLite вдвое быстрей Android или MacOS X и более чем в 10 раз быстрее, чем Windows.


Диаграмма 3: Чтение SQLite, время ожидания относительно прямой файловой системы.
100K blob, avg 10KB, произвольный порядок
использование sqlite3_blob_read() из memory-mapped БД.

Третья диаграмма показывает, что чтение содержания из SQLite может быть вдвое быстрей, чем читать от отдельных файлов на диске для Mac и Android и в десять раз быстрее для Windows.

2.2. Измерения производительности записи

Пишет медленнее. На всех системах, используя прямой I/O и SQLite, производительность записи от 5 до 15 раз медленнее, чем чтение.

Измерения производительности записи были сделаны, заменив (переписав) каждый blob другим. Все blob случайны и несжимаемы. Поскольку запись медленнее, только 10000 из этих 100000 blob в базе данных заменяются. blob, которые будут заменены, отобраны наугад и без определенного порядка.

Прямые записи на диск достигаются, используя fopen()/fwrite()/fclose(). По умолчанию, и во всех результатах, показанных ниже, буфера файловой системы OS никогда не сбрасываются к постоянному хранению, используя fsync() или FlushFileBuffers(). Другими словами, нет никакой попытки сделать прямую запись на диск транзакционной или безопасной по питанию. Мы нашли, что вызывая fsync() или FlushFileBuffers() на каждом файле запись будет приблизительно в 10 или более раз медленнее, чем пишет SQLite.

Следующая диаграмма сравнивает обновления базы данных SQLite в режиме WAL против сырой прямой дисковой перезаписи отдельных файлов на диске. PRAGMA synchronous = NORMAL. Вся запись базы данных в единственной транзакции. Таймер для записи базы данных остановлен после того, как транзакция коммитится, но прежде чем запущена контрольная точка . Обратите внимание на то, что SQLite пишет, в отличие от прямой записи на диск транзакционно и безопасно, хотя потому что синхронное урегулирование NORMAL вместо FULL, транзакции не длительны.


Диаграмма 4: Запись SQLite, время ожидания относительно прямой записи файловой системы.
10K blob, в среднем размер 10 КБ, произвольный порядок, режим WAL с синхронным NORMAL,
исключено время контрольной точки.

Показатели производительности android для опыта с записью опущены, потому что тесты производительности на Galaxy S3 случайны. Два последовательных пробега того же самого эксперимента дали бы дико различные времена. И честно говоря, исполнение SQLite на андроиде немного медленнее, чем запись непосредственно на диск.

Следующая диаграмма показывает исполнение SQLite против прямой записи на диск, когда транзакции отключены ( PRAGMA journal_mode=OFF) и PRAGMA synchronous = OFF. Эти параметры настройки помещают SQLite в равные условия с прямой записью на диск. Это делает данные подверженными повреждениям из-за системных катастроф и перебоев в питании.


Диаграмма 5: Запись SQLite, время ожидания относительно прямой записи файловой системы.
10K blob, в среднем размер 10 КБ, произвольный порядок,
журналирование отключено, synchronous OFF.

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

2.3. Изменения

Выбор времени компиляции -DSQLITE_DIRECT_OVERFLOW_READ заставляет SQLite обходить свой кэш страницы, читая содержание со страниц переполнения. Это помогает базе данных читать 10K blob немного быстрее, но не все это намного быстрее. SQLite все еще держит преимущество по скорости перед прямым чтением файловой системы без выбора времени компиляции SQLITE_DIRECT_OVERFLOW_READ.

Другие варианты времени компиляции, такие как использование -O3 вместо -Os или применение -DSQLITE_THREADSAFE=0 и/или некоторые из других рекомендуемых вариантов времени компиляции могли бы помочь SQLite работать еще быстрее относительно прямой файловой системы.

Размер blob в данных тестирования затрагивает производительность. Файловая система обычно будет быстрее для больших blob, так как издержки open() и close() амортизируется по большему количеству байтов I/O, тогда как база данных будет более эффективной в скорости, когда средний размер blob меньше.

3. Общие результаты

  1. SQLite конкурентоспособен и обычно быстрее, чем blob, сохраненные в отдельных файлах на диске, для чтения и записи.

  2. SQLite намного быстрее, чем прямая запись на диск в Windows, когда антивирусная защита включена. Так как антивирусное программное обеспечение должно быть включено по умолчанию в Windows, это означает, что SQLite обычно намного быстрее, чем прямые записи на диск в Windows.

  3. Чтение на порядок быстрее, чем запись для всех систем, как для SQLite, так и для I/O прямо на диск.

  4. Производительность I/O значительно различается в зависимости от операционной системы и аппаратных средств. Сделайте свои собственные измерения прежде, чем делать выводы.

  5. Некоторые другие СУБД SQL советуют разработчикам хранить blob в отдельных файлах и затем хранить имя файла в базе данных. В этом случае нужно сначала консультироваться с базой данных, чтобы найти имя файла прежде, чем открыть и прочитать файл, простое хранение blob в базе данных дает намного более быстрое чтение и производительность записи с SQLite. См. статью здесь для подробностей.

4. Дополнительные примечания

4.1. Компилирование и тестирование на Android

Программа kvtest собрана и выполнена на Android следующим образом. Сначала установите Android SDK и NDK. Затем подготовьте скрипт "android-gcc":

#!/bin/sh
#
NDK=/home/drh/Android/Sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-16/arch-arm
ABIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
GCC=$ABIN/arm-linux-androideabi-gcc
$GCC --sysroot=$SYSROOT -fPIC -pie $*

Сделайте его исполняемым и поместите его в $PATH. Теперь соберите программу kvtest следующим образом:

android-gcc -Os -I. kvtest.c sqlite3.c -o kvtest-android

Затем переместите получающийся исполняемый файл kvtest-android в устройство на базе Android:

adb push kvtest-android /data/local/tmp

Теперь примените "adb shell", чтобы получить приглашение оболочки на устройстве на базе Android, перейдите в /data/local/tmp и начните запускать тесты как с любым другим unix.