Sqlite3 оптимизация запросов join vs subselect
Я пытаюсь выяснить самый лучший способ (возможно, в этом случае это не имеет значения), чтобы найти строки одной таблицы, основываясь на существовании флага и реляционного идентификатора в строке в другой таблице.
вот схемы:
CREATE TABLE files (
id INTEGER PRIMARY KEY,
dirty INTEGER NOT NULL);
CREATE TABLE resume_points (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
scan_file_id INTEGER NOT NULL );
Я использую SQLite3
там таблица файлов будет очень большой, 10k-5M строк, как правило. resume_points будет небольшим scan_file_id ' s
так что моя первая мысль было:
select distinct files.* from resume_points inner join files
on resume_points.scan_file_id=files.id where files.dirty = 1;
коллега предложил повернуть соединение вокруг:
select distinct files.* from files inner join resume_points
on files.id=resume_points.scan_file_id where files.dirty = 1;
тогда я подумал, так как мы знаем, что число различных scan_file_id
s будет настолько мал, что, возможно, подселект будет оптимальным (в этом редком случае):
select * from files where id in (select distinct scan_file_id from resume_points);
на explain
выходы были следующие строки: 42, 42, и 48 соответственно.
5 ответов
TL; DR: лучший запрос и индекс:
create index uniqueFiles on resume_points (scan_file_id);
select * from (select distinct scan_file_id from resume_points) d join files on d.scan_file_id = files.id and files.dirty = 1;
поскольку я обычно работаю с SQL Server, сначала я подумал, что оптимизатор запросов найдет оптимальный план выполнения для такого простого запроса независимо от того, каким образом вы пишете эти эквивалентные операторы SQL. Поэтому я загрузил SQLite и начал играть. К моему большому удивлению, разница в производительности была огромной.
вот код установки:
CREATE TABLE files (
id INTEGER PRIMARY KEY autoincrement,
dirty INTEGER NOT NULL);
CREATE TABLE resume_points (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,
scan_file_id INTEGER NOT NULL );
insert into files (dirty) values (0);
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into files (dirty) select (case when random() < 0 then 1 else 0 end) from files;
insert into resume_points (scan_file_id) select (select abs(random() % 8000000)) from files limit 5000;
insert into resume_points (scan_file_id) select (select abs(random() % 8000000)) from files limit 5000;
Я рассматривал два индексы:
create index dirtyFiles on files (dirty, id);
create index uniqueFiles on resume_points (scan_file_id);
create index fileLookup on files (id);
Ниже приведены запросы, которые я пробовал, и время выполнения на моем ноутбуке i5. Размер файла базы данных составляет всего около 200 МБ, так как у него нет других данных.
select distinct files.* from resume_points inner join files on resume_points.scan_file_id=files.id where files.dirty = 1;
4.3 - 4.5ms with and without index
select distinct files.* from files inner join resume_points on files.id=resume_points.scan_file_id where files.dirty = 1;
4.4 - 4.7ms with and without index
select * from (select distinct scan_file_id from resume_points) d join files on d.scan_file_id = files.id and files.dirty = 1;
2.0 - 2.5ms with uniqueFiles
2.6-2.9ms without uniqueFiles
select * from files where id in (select distinct scan_file_id from resume_points) and dirty = 1;
2.1 - 2.5ms with uniqueFiles
2.6-3ms without uniqueFiles
SELECT f.* FROM resume_points rp INNER JOIN files f on rp.scan_file_id = f.id
WHERE f.dirty = 1 GROUP BY f.id
4500 - 6190 ms with uniqueFiles
8.8-9.5 ms without uniqueFiles
14000 ms with uniqueFiles and fileLookup
select * from files where exists (
select * from resume_points where files.id = resume_points.scan_file_id) and dirty = 1;
8400 ms with uniqueFiles
7400 ms without uniqueFiles
похоже, оптимизатор запросов SQLite не очень продвинут. Лучшие запросы сначала уменьшают resume_points до небольшого количества строк (два в тестовом случае. ОП сказал, что это будет 1-2.), а затем посмотрите файл, чтобы увидеть, является ли он грязным или нет. dirtyFiles
индекс не сделал много из разница для любого из файлов. Я думаю, это может быть из-за того, как данные расположены в тестовых таблицах. Это может иметь значение в производственных таблицах. Однако разница не слишком велика, так как будет меньше, чем несколько поисков. uniqueFiles
имеет значение, так как он может уменьшить 10000 строк resume_points до 2 строк без сканирования через большинство из них. fileLookup
сделал некоторые запросы немного быстрее, но недостаточно, чтобы существенно изменить результаты. В частности, он сделал группа очень медленно. В заключение, уменьшите набор результатов раньше, чтобы сделать самые большие различия.
С files.id
является первичным ключом, попробуйте GROUP
ing BY
это поле, а не проверка DISTINCT files.*
SELECT f.*
FROM resume_points rp
INNER JOIN files f on rp.scan_file_id = f.id
WHERE f.dirty = 1
GROUP BY f.id
другой вариант для рассмотрения производительности-добавление индекса в resume_points.scan_file_id
.
CREATE INDEX index_resume_points_scan_file_id ON resume_points (scan_file_id)
вы могли бы попробовать exists
, который не будет производить дубликат files
:
select * from files
where exists (
select * from resume_points
where files.id = resume_points.scan_file_id
)
and dirty = 1;
конечно может помогите иметь правильные индексы:
files.dirty
resume_points.scan_file_id
полезен ли индекс будет зависеть от ваших данных.
Я думаю, что jtseng дал решение.
select * from (select distinct scan_file_id from resume_points) d
join files on d.scan_file_id = files.id and files.dirty = 1
В основном это то же самое, что вы опубликовали в качестве последнего варианта:
select * from files where id in (select distinct scan_file_id from resume_points) and dirty = 1;
это beacuse вы должны избежать полного сканирования таблицы / присоединиться.
поэтому сначала вам нужны ваши 1-2 разных идентификатора:
select distinct scan_file_id from resume_points
после этого только ваши 1-2 строки должны быть объединены на другой таблице вместо всех 10K, что дает оптимизацию производительности.
Если вам нужно это заявление несколько раз, я поместил бы его в вид. представление не изменит производительность, но выглядит чище / легче читать.
также проверьте документацию по оптимизации запросов:http://www.sqlite.org/optoverview.html
если таблица "resume_points" будет иметь только один или два отдельных идентификатора файла, ей, похоже, понадобится только одна или две строки и, похоже, нужен scan_file_id в качестве первичного ключа. Эта таблица имеет только два столбца, а id-номер бессмыслен.
и если это в случае, вам не нужен ни один из номеров ID.
pragma foreign_keys = on;
CREATE TABLE resume_points (
scan_file_id integer primary key
);
CREATE TABLE files (
scan_file_id integer not null references resume_points (scan_file_id),
dirty INTEGER NOT NULL,
primary key (scan_file_id, dirty)
);
и теперь вам тоже не нужно соединение. Просто запросите таблицу "файлы".