SQLite: COUNT медленный на больших таблицах - PullRequest
31 голосов
/ 24 января 2012

У меня проблема с производительностью в SQLite с SELECT COUNT (*) для больших таблиц.

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

У меня есть 2 таблицы:

CREATE TABLE Table1 (
Key INTEGER NOT NULL,
... several other fields ...,
Status CHAR(1) NOT NULL,
Selection VARCHAR NULL,
CONSTRAINT PK_Table1 PRIMARY KEY (Key ASC))

CREATE Table2 (
Key INTEGER NOT NULL,
Key2 INTEGER NOT NULL,
... a few other fields ...,
CONSTRAINT PK_Table2 PRIMARY KEY (Key ASC, Key2 ASC))

В Table1 содержится около 8 миллионов записей, в Table2 - около 51 миллиона записей, а размер файла базы данных превышает 5 ГБ.

Таблица1 имеет еще 2 индекса:

CREATE INDEX IDX_Table1_Status ON Table1 (Status ASC, Key ASC)
CREATE INDEX IDX_Table1_Selection ON Table1 (Selection ASC, Key ASC)

Поле «Состояние» является обязательным, но имеет только 6 различных значений, «Выбор» не требуется и имеет только около 1,5 миллиона значений, отличных от нуля, и только около 600 000 различных значений.

Я провел несколько тестов для обеих таблиц, вы можете увидеть сроки ниже, и я добавил «план запроса объяснения» для каждого запроса (QP). Я поместил файл базы данных на USB-карту памяти, чтобы можно было удалять его после каждого теста и получать надежные результаты без вмешательства в кэш диска. Некоторые запросы на USB выполняются быстрее (я полагаю, из-за отсутствия времени поиска), но некоторые - медленнее (сканирование таблицы).

SELECT COUNT(*) FROM Table1
    Time: 105 sec
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows)
SELECT COUNT(Key) FROM Table1
    Time: 153 sec
    QP: SCAN TABLE Table1 (~1000000 rows)
SELECT * FROM Table1 WHERE Key = 5123456
    Time: 5 ms
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1
    Time: 16 sec
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows)
SELECT * FROM Table1 WHERE Selection = 'SomeValue' AND Key > 5123456 LIMIT 1
    Time: 9 ms
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Selection (Selection=?) (~3 rows)

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

То же самое относится и к таблице 2:

SELECT COUNT(*) FROM Table2
    Time: 528 sec
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~1000000 rows)
SELECT COUNT(Key) FROM Table2
    Time: 249 sec
    QP: SCAN TABLE Table2 (~1000000 rows)
SELECT * FROM Table2 WHERE Key = 5123456 AND Key2 = 0
    Time: 7 ms
    QP: SEARCH TABLE Table2 USING INDEX sqlite_autoindex_Table2_1 (Key=? AND Key2=?) (~1 rows)

Почему SQLite не использует автоматически созданный индекс для первичного ключа в Table1? И почему, когда он использует автоиндекс на Table2, это все равно занимает много времени?

Я создал те же таблицы с тем же содержимым и индексами в SQL Server 2008 R2, и их число практически мгновенно.

В одном из комментариев ниже предлагается выполнить АНАЛИЗ в базе данных. Я сделал, и это заняло 11 минут. После этого я снова запустил некоторые тесты:

SELECT COUNT(*) FROM Table1
    Time: 104 sec
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~7848023 rows)
SELECT COUNT(Key) FROM Table1
    Time: 151 sec
    QP: SCAN TABLE Table1 (~7848023 rows)
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1
    Time: 5 ms
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid>?) (~196200 rows)
SELECT COUNT(*) FROM Table2
    Time: 529 sec
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~51152542 rows)
SELECT COUNT(Key) FROM Table2
    Time: 249 sec
    QP: SCAN TABLE Table2 (~51152542 rows)

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

Далее я создаю дополнительный индекс dan в поле Key таблицы Table1, который должен соответствовать автоиндексу. Я сделал это на исходной базе данных, без данных анализа. Создание этого индекса заняло более 23 минут (помните, это на USB-карте).

CREATE INDEX IDX_Table1_Key ON Table1 (Key ASC)

Затем я снова запустил тесты:

SELECT COUNT(*) FROM Table1
    Time: 4 sec
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Key(~1000000 rows)
SELECT COUNT(Key) FROM Table1
    Time: 167 sec
    QP: SCAN TABLE Table2 (~1000000 rows)
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1
    Time: 17 sec
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows)

Как видите, индекс помог с количеством (*), но не с количеством (ключом).

Наконец, я создал таблицу, используя ограничение столбца вместо ограничения таблицы:

CREATE TABLE Table1 (
Key INTEGER PRIMARY KEY ASC NOT NULL,
... several other fields ...,
Status CHAR(1) NOT NULL,
Selection VARCHAR NULL)

Затем я снова запустил тесты:

SELECT COUNT(*) FROM Table1
    Time: 6 sec
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows)
SELECT COUNT(Key) FROM Table1
    Time: 28 sec
    QP: SCAN TABLE Table1 (~1000000 rows)
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1
    Time: 10 sec
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows)

Хотя планы запросов одинаковы, времена намного лучше. Почему это?

Проблема в том, что ALTER TABLE не позволяет конвертировать существующую таблицу, и у меня есть много существующих баз данных, которые я не могу преобразовать в эту форму. Кроме того, использование ограничения столбца вместо ограничения таблицы не будет работать для Table2.

Кто-нибудь знает, что я делаю неправильно и как решить эту проблему?

Я использовал System.Data.SQLite версии 1.0.74.0 для создания таблиц и запуска тестов. Я использовал SQLiteSpy 1.9.1.

Спасибо

Марк

Ответы [ 8 ]

25 голосов
/ 14 июня 2013

Если у вас нет DELETE d записей, выполните:

SELECT MAX(_ROWID_) FROM "table" LIMIT 1;

Избегает сканирования полной таблицы. Обратите внимание, что _ROWID_ является идентификатором SQLite .

24 голосов
/ 18 февраля 2012

С http://old.nabble.com/count(*)-slow-td869876.html

SQLite всегда выполняет полное сканирование таблицы для подсчета (*).
не хранит метаинформацию в таблицах, чтобы ускорить этот
процесс.

Не хранение метаданных - это продуманное решение
.Если бы каждая таблица хранила счет (или, что лучше, каждый
узел btree хранил счет), то при каждом ВСТАВКЕ или УДАЛЕНИИ должно происходить гораздо большее обновление
.Это
будет замедлять INSERT и DELETE, даже в общем случае
, где скорость счета (*) не важна.

Если вам действительно нужен быстрый COUNT, то вы можете создать
триггер на INSERT и DELETE, который обновляет текущий счетчик
в отдельной таблице, а затем запросить эту отдельную таблицу
, чтобы найтипоследний счет.

Конечно, не стоит вести счетчик ПОЛНЫХ строк, если вам
нужны СЧЕТЫ, зависящие от предложений WHERE (т. Е. WHERE field1> 0 и field2 <1000000000).</p>

2 голосов
/ 04 декабря 2013

Не считайте звезды, считайте рекорды! Или на другом языке, никогда не выпускайте

ВЫБЕРИТЕ СЧЕТЧИК (*) ОТ имени таблицы;

использовать

ВЫБЕРИТЕ COUNT (ROWID) ИЗ ИМЕНИ таблицы;

Позвоните в EXPLAIN QUERY PLAN, чтобы увидеть разницу. Убедитесь, что у вас есть индекс, содержащий все столбцы, упомянутые в предложении WHERE.

1 голос
/ 25 января 2012

Это может не сильно помочь, но вы можете запустить команду ANALYZE , чтобы перестроить статистику о вашей базе данных.Попробуйте запустить "ANALYZE;", чтобы перестроить статистику по всей базе данных, затем снова запустите ваш запрос и посмотрите, будет ли он быстрее.

0 голосов
/ 31 октября 2012

У меня была такая же проблема, в моей ситуации помогла команда VACUUM. После его выполнения в базе данных COUNT (*) скорость увеличилась почти в 100 раз. Однако самой команде требуется несколько минут в моей базе данных (20 миллионов записей). Я решил эту проблему, запустив VACUUM, когда мое программное обеспечение завершает работу после разрушения главного окна, поэтому задержка не создает проблем для пользователя.

0 голосов
/ 17 февраля 2012

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

Предполагая, что у вас есть столбец date_created (или вы можете добавить его), выполняйте запрос в фоновом режиме каждый день в полночь (скажем,в 00:05) и сохраните значение где-то вместе с датой last_updated, которую оно вычислило (я вернусь к этому чуть позже).

Затем, запустив ваш столбец date_created (с индексом),Вы можете избежать полного сканирования таблицы, выполнив запрос, подобный SELECT COUNT (*) FROM TABLE WHERE date_updated> "[TODAY] 00:00:05".

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

Единственный улов заключается в том, что с 12:05 до 12:07 (продолжительность, в течение которой выполняется ваш запрос общего количества), у вас есть условие гонки, в котором вы можете проверить значение last_updated вашего полного сканирования таблицы ().Если ему> 24 часа, то ваш запрос инкрементного счета должен получить полный счет дня плюс время, прошедшее сегодня.Если ему <24 часа, то ваш запрос инкрементного счета должен получить частичный счет дня (только время, прошедшее сегодня). </p>

0 голосов
/ 17 февраля 2012

Вывод для быстрых запросов начинается с текста «QP: SEARCH».В то время как те для медленных запросов начинаются с текста «QP: SCAN», который предполагает, что sqlite выполняет сканирование всей таблицы, чтобы сгенерировать счет.1003 * следующий , который предполагает, что использование полного сканирования таблицы для получения счетчика - это именно то, как работает sqlite, и поэтому, вероятно, его нельзя избежать.я хотел бы знать, можно ли быстро получить счет с помощью запроса, подобного следующему?

выберите 1, где статус = 1 объединение, выберите 1, где статус = 2 ...

, а затем подсчитайте строкив результате.Это явно некрасиво, но может сработать, если убедит sqlite выполнить запрос как поиск, а не как сканирование.Идея возврата «1» каждый раз состоит в том, чтобы избежать затрат на возврат реальных данных.

0 голосов
/ 08 февраля 2012

Что касается ограничения столбцов, SQLite сопоставляет столбцы, которые объявлены как INTEGER PRIMARY KEY, с внутренним идентификатором строки (что, в свою очередь, допускает ряд внутренних оптимизаций). Теоретически, он мог бы сделать то же самое для отдельно объявленного ограничения первичного ключа, но, по-видимому, он не делает этого на практике, по крайней мере, с используемой версией SQLite. (System.Data.SQLite 1.0.74.0 соответствует ядру SQLite 3.7.7.1. Возможно, вы захотите повторить проверку своих цифр с помощью 1.0.79.0; вам не нужно менять базу данных для этого, только библиотеку.)

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