Оптимизации подзапросов в InterBase - Оптимизация скалярного подзапроса

ОГЛАВЛЕНИЕ

 

Оптимизация скалярного подзапроса

Точнее, доказательство её отсутствия.

select x from test1
where id = (select id from test2 where y = 130);

PLAN (TEST2 NATURAL)
PLAN (TEST1 INDEX (RDB$PRIMARY1))

По своей привычке InterBase выдаёт планы подзапросов первыми, до плана основного запроса.

Как можно видеть, условие в подзапросе вида id = id_извне никак на него не повлияло - он обрабатывается полным перебором. Попытка явно подсунуть ему план с индексом по test2(id) к успеху не приводит - возвращается ошибка. Зато внешний запрос индекс использует.

Теперь попробуем написать в точности то же самое, но через IN.

Аналогичный запрос, но через IN()

select x from test1
where id in (select id from test2 where y=130);

PLAN (TEST2 INDEX (RDB$PRIMARY2))
PLAN (TEST1 NATURAL)

Может показаться смешным, но замена = на IN перевернула весь план буквально с точностью до наоборот. Теперь внешний запрос начинает отрабатывать своё условие перебором, зато внутренний начинает чувствовать контекст. Условие из контекста аккуратно подходит под его индекс, что и используется.

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

Надо сказать, что оба запроса на самом деле дают результат, эквивалентный следующему соединению:

select test1.x from test1, test2
where test1.id=test2.id and test2.y=130;

Вариант со скалярным подзапросом даёт план, эквивалентный следующему:

PLAN JOIN (TEST2 NATURAL,TEST1 INDEX (RDB$PRIMARY1))

А вариант с множественным действует примерно так:

PLAN JOIN (TEST1 NATURAL,TEST2 INDEX (RDB$PRIMARY2))

В данном случае первый вариант эффективнее. Он делает один проход по test2, находит в ней всего одну запись, у которой y = 130, и с полученным значением выполняет внешний запрос. Вариант с соединением, однако, является более общим, так как скалярный подзапрос приведёт к ошибке, если записей с y = 130 окажется несколько.

Второй вариант, с IN это как раз стерпит, однако он менее эффективен, так как вызывает поиск по table2 на каждой итерации внешнего запроса. Правда, сам этот поиск делается по индексу.

И здесь ещё один существенный момент: при отработке подзапросов типа IN(...), =SOME(...), =ANY(...) перебор останавливается после первой же записи, выданной подзапросом. В то время как =ALL(...) будет работать либо до конца, либо до первой записи, не удовлетворяющей условию. То есть при удачном стечении обстоятельств, если ``подходящая'' запись всплывёт на первой же итерации подзапроса, всё может быть очень эффективно. А возможна и обратная ситуация.

Естественно, те же соображения применимы и при других видах сравнения. Операции <, <=, <> так же можно внести во внутренний запрос. Хотя пользы от этого, конечно, будет гораздо меньше, чем от равенства.

Кстати, в двух описанных примерах можно вместо y = 130 в подзапросе сделать x = 30 во внешнем запросе. На планы это не повлияет, поскольку и в том, и в другом случае условия налагаются на неиндексируемые поля. Однако оценки эффективности поменяются местами, и вариант с подзапросом через IN станет более эффективным. В прочем, ни один из вариантов с подзапросами никогда не будет эффективнее, чем оптимальный план в варианте с соединением. Потому невозможность автоматической раскрутки подзапросов в соединения является важным недостатком, который следует учитывать.