У меня возникли проблемы с тем, чтобы мой SQL работал эффективно с помощью оператора IN. Если я запускаю два оператора отдельно и вручную вставляю серию результатов (в данном случае 30 vendor_id), запрос vendor_master выполняется мгновенно, а запрос invoices выполняется примерно за 2 секунды.

select * FROM invoices where vendor_id IN

(

select vendor_id from vendor_master WHERE vendor_master_id = 12345

);

Так что же вызывает ОГРОМНОЕ замедление, более 60 секунд и часто превышающее время ожидания? Есть ли способ записать результаты в переменную через запятую? Или заставить внутренний оператор выполнить первый?

1
Atari2600 3 Сен 2014 в 06:01
1
Незначительное примечание. В подзапросе отсутствует ключевое слово WHERE.
 – 
Conrad Frix
3 Сен 2014 в 06:19

4 ответа

Лучший ответ

Так что же вызывает ОГРОМНОЕ замедление, более 60 секунд и часто превышающее время ожидания?

Предложение IN хорошо работает, когда набор данных внутри условия IN является "маленьким" и "детерминированным". Это потому, что условие оценивается один раз для каждой строки. Итак, если предположить, что запрос в предложении IN возвращает 100 строк, а таблица в предложении FROM имеет 1000 строк, серверу придется выполнять сравнения 100 * 1000 = 100,000, чтобы отфильтровать ваши данные. Слишком много усилий, чтобы отфильтровать слишком мало данных, вам не кажется? Конечно, если ваши наборы данных (как в предложениях from, так и in) больше, вы можете себе представить эффект.

Между прочим, когда вы используете подзапрос в качестве условия in, возникают дополнительные накладные расходы: подзапрос должен выполняться один раз для каждой строки. Итак, последовательность выглядит примерно так:

  • ряд 1
    • выполнить подзапрос
    • проверить, соответствует ли значение строки 1 значению результата подзапроса
    • если вышеприведенное верно, сохранить строку в результирующем наборе; в противном случае исключить его
  • 2 ряд
    • выполнить подзапрос
    • проверить, соответствует ли значение строки 2 значению результата подзапроса
    • если вышеприведенное верно, сохранить строку в результирующем наборе; в противном случае исключить его
  • ...

Слишком много работы, тебе не кажется?


Есть ли способ поместить результаты в переменную через запятую?

Да, есть способ ... но вы бы действительно захотели это сделать? Посмотрим:

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

set @valueList = (select group_concat(vendor_id separator ',')
                 from (select vendor_id from vendor_master where vendor_master_id = 12345) as a)

Затем создайте выражение SQL:

set @sql = concat('select * from invoices where vendor_id in (', @valueList, ')';

Наконец, создайте подготовленный оператор и выполните его:

prepare stmt from @sql;
execute stmt;
-- when you're done, don't forget to deallocate the statement:
-- deallocate prepare stmt;

Я еще раз спрашиваю вас: вы действительно хотите все это делать?


Или сначала выполнить внутренний оператор?

Все остальные ответы указывают вам в правильном направлении: вместо использования in используйте inner join:

select i.*
from invoices as i
     inner join (
         select distinct vendor_id 
         from vendor_master
         where vendor_master_id = 12345
     ) as vm on i.vendor_id = vm.vendor_id;

Если по какой-то причине это все еще слишком медленно, единственная альтернатива, которая приходит мне в голову, - это создать временную таблицу (своего рода «стратегия разделяй и властвуй»):

drop table if exists temp_vm;
create temporary table temp_vm
    select distinct vendor_id
    from vendor_master
    where vendor_master_id = 12345;
alter table temp_vm
    add index vi(vendor_id);
select i.*
from invoices as i inner join temp_vm as vm on i.vendor_id = vm.vendor_id;

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


В любом случае ваша производительность будет улучшена, если вы убедитесь, что ваши таблицы правильно проиндексированы; в частности, вам необходимо убедиться, что invoices.vendor_id и vendor_master.vendor_master_id` проиндексированы.

1
Barranka 24 Сен 2014 в 00:36
Есть ли причина, по которой я запускаю операторы отдельно и создаю свой собственный список, разделенный запятыми, который работает очень-очень быстро, и ему все еще нужно правильно оценивать каждое значение массива IN?
 – 
Atari2600
3 Сен 2014 в 06:56
Учтите следующее: для каждой строки в вашей таблице invoices условие in оценивается один раз, и это условие in является запросом, который необходимо оценить... слишком большая нагрузка! Когда вы передаете простой список значений, разделенных запятыми, нет необходимости оценивать запрос... и все же это слишком! Используйте inner join вместо in (чище, быстрее и дешевле)
 – 
Barranka
3 Сен 2014 в 07:03

До MySQL 5.6.6 in оптимизировался довольно неэффективно. Вместо этого используйте exists:

select *
FROM invoices i
where exists (select 1
              from vendor_master vm
              where i.vendor_id = vm.vendor_id and vm.vendor_master_id = 12345
             );

Для лучшей производительности вам нужен индекс по vendor_master(vendor_id, vendor_master_id).

3
Gordon Linoff 3 Сен 2014 в 06:03
1
Не говоря уже о проблеме, если vendor_id равен нулю
 – 
Conrad Frix
3 Сен 2014 в 06:18

Вы можете попробовать использовать INNER JOIN:

select i.* 
FROM invoices i
INNER JOIN vendor_master vm 
        ON i.vendor_id = vm.vendor_id AND vm.vendor_master_id = 12345
1
xdazz 3 Сен 2014 в 06:07

Вы можете использовать JOIN с DISTINCT вместо IN:

SELECT *
FROM invoices JOIN 
(
    SELECT DISTINCT vendor_id as vid
    FROM vendor_master
    WHERE vendor_master_id = 12345
) vmi
ON invoices.vendor_in = vmi.vid

Помните, что у вас должен быть DISTINCT, иначе, если есть две записи для внутреннего запроса, у вас будут повторяющиеся строки после JOIN, и результат будет отличаться от IN запрос.

1
Mohammad 3 Сен 2014 в 06:24