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_ids будет настолько мал, что, возможно, подселект будет оптимальным (в этом редком случае):

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 является первичным ключом, попробуйте GROUPing 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)
);

и теперь вам тоже не нужно соединение. Просто запросите таблицу "файлы".