Многопоточное чтение SQLite: оптимизация SELECT для 1 млн записей
При работе с SQLite в высоконагруженных сценариях, когда требуется выполнять тысячи параллельных SELECT-запросов к read-only базе данных, стандартные подходы могут давать низкую производительность. Особенно это заметно при попытке заменить LINQ-операции (WHERE, ORDER BY, SKIP, TAKE) для таблицы с 1 млн записей и 20 полями. Рассмотрим эффективные методы ускорения.
Проблема Mode=Memory и Cache=Shared
Использование Data Source=filename;Mode=Memory;Cache=Shared создаёт общую базу в памяти, но совместный доступ через Cache=Shared снижает пропускную способность. Включение PRAGMA read_uncommitted = true не решает проблему, так как SQLite всё равно блокирует записи при конкурентном чтении. Для read-only сценариев лучше отказаться от Memory-режима.
Оптимальная конфигурация для read-only
Для максимальной скорости многопоточных SELECT-запросов используйте:
- Файловую базу данных с параметром
Mode=ReadOnly- это снимает блокировки на запись. - WAL-режим (
PRAGMA journal_mode=WAL) - позволяет параллельно читать и писать без взаимных блокировок. - Увеличение кэша (
PRAGMA cache_size = -20000- 20 МБ) для хранения страниц в памяти. - Отключение синхронизации (
PRAGMA synchronous = OFFилиNORMAL) для read-only.
Использование VFS для многопоточности
Стандартный VFS (Virtual File System) SQLite не оптимизирован для тысяч параллельных чтений. Кросс-платформенные альтернативы:
- unix-dotfile - стандартный VFS для Unix, но он использует файловые блокировки, что снижает производительность.
- NFS VFS - не подходит для локальных баз.
- Собственная реализация VFS - в Microsoft.Data.Sqlite можно создать кастомный VFS, который использует
pread/pwriteбез блокировок, но это требует глубоких знаний.
На практике для 500-5000 параллельных SELECT-запросов лучше не использовать VFS, а применить пул соединений с WAL-режимом.
Пул соединений и многопоточность
Создайте фиксированный пул из 10-20 соединений (по числу ядер процессора). Каждое соединение открывается с Mode=ReadOnly и Journal Mode=WAL. Запросы распределяются через очередь (например, BlockingCollection). Это даёт:
- Отсутствие блокировок между потоками.
- Использование кэша страниц на уровне соединения.
- Возможность выполнять 500-5000 запросов с задержкой менее 1 мс на SELECT (при условии индексации).
Индексация и оптимизация запросов
Для таблицы с 1 млн записей и 20 полями (10 строковых) обязательно создайте индексы на поля, используемые в WHERE, ORDER BY и JOIN. Используйте составные индексы для покрытия запросов. Например:
CREATE INDEX idx_filter ON table (field1, field2) WHERE condition;Также избегайте SELECT * - выбирайте только нужные столбцы. Для пагинации (SKIP, TAKE) используйте ROWID или LIMIT/OFFSET с индексами.
Альтернативы SQLite для высоких нагрузок
Если производительность остаётся низкой, рассмотрите:
- DuckDB - аналитическая СУБД с отличной поддержкой параллельных чтений.
- SQLite с расширением
mpt(multi-process) - экспериментальная VFS для многоядерных систем. - In-memory базы данных (например, Redis) для кэширования часто запрашиваемых данных.
Выбор зависит от требований к кросс-платформенности и объёму данных.