Уважаемые подписчики! Что-то не хватает у меня времени и терпения на изложение формального синтаксиса, так что, продолжим тему, поднятую в предыдущем выпуске. Дадим слово Тому Кайту еще раз...
Вы объяснили условия in и exists настолько хорошо (еще никто так этого не делал, - вы, должно быть, счастливый человек, - вы верите в то, что знаниями надо делиться)...
а теперь вы оставляете нам открытый вопрос...
"Помните - это ПРОСТОЕ ПРАВИЛО, а из простых правил есть бесконечно много исключений."
Не могли бы вы описать эти возможные исключения и факторы, которые могут их вызвать.
Таких факторов - бесконечно много, - ввод/вывод может повлиять на результат. Цель оптимизации тоже может повлиять (например, запрос с WHERE EXISTS, в общем случае, будет находить первую строку быстрее, чем IN, но IN будет давать ПОСЛЕДНЮЮ строку (все строки) быстрее, чем where exists). Если цель оптимизации - максимально быстрое получение первой строки, exists может по всем статьям давать преимущество над IN. Если же запрос выполняется пакетным процессом (и принципиально важно быстрее добраться до ПОСЛЕДНЕЙ строки)...
и так далее. Вывод: учитывайте различия этих условий, при настройке попробуйте оба варианта, разберитесь концептуально, что они делают, и тогда сможете маскимально эффективно их использовать.
Остаются ли ваши объяснения в силе для запросов с условиями NOT IN и NOT EXISTS?
Нет, отчасти потому, что NOT IN и NOT EXISTS не заменяют друг друга! Они имеют разную семантику и могут/будут давать разные результаты. См.
http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:442029737684например.
Блестящее обсуждение!
Я попытаюсь обобщить то, что вы говорите.
Условия in и exists - взаимозаменяемы, и мы должны использовать то или иное в зависимости от количества строк, возвращаемых внешним и внутренним запросом.
С другой стороны:
Условия not in и not exists - не взаимозаменяемы, и вообще никак не взаимосвязаны. Их использование зависит от того, что именно нужно.
Подводя окончательные итоги, не могли бы вы дать примеры, когда использование not in - оправдано, а not exists - нет.
Вы правы в первом утверждении (in/exists).
Второе утверждение - слишком сильное. not in и not exists не вполне взаимозаменяемы. NOT IN отличается от NOT EXISTS, но их результаты совпадают, если подзапрос, используемый в условии NOT IN, не содержит значений NULL.
И not in, и not exists могут быть очень эффективны, когда данные не содержат значения null (а условие not in ПРИ ИСПОЛЬЗОВАНИИ СТОИМОСТНОГО ОПТИМИЗАТОРА - особенно эффективно за счет использования "антисоединения". Подробнее об этом см. в руководстве по настройке производительности). При наличии значений NULL условие not in может быть крайне неэффективным, и многие заменяют его условием not exists (не понимая, что ОТВЕТ просто изменится!!!)
"И not in, и not exists могут быть очень эффективны, когда данные не содержат значения null (а условие not in ПРИ ИСПОЛЬЗОВАНИИ СТОИМОСТНОГО ОПТИМИЗАТОРА - особенно эффективно за счет использования "антисоединения". Подробнее об этом см. в руководстве по настройке производительности). При наличии значений NULL условие not in может быть крайне неэффективным, и многие заменяют его условием not exists (не понимая, что ОТВЕТ просто изменится!!!)"
Помоему, все это я понял правильно. Теперь:
1а) not in может выполняться быстрее, чем not exists (или медленнее). Это действительно разные условия. not exists во многом аналогично EXISTS. not in, когда подзапрос не возвращает значений NULL, может быть во многом аналогично IN (выполняется как антисоединение, а не как соединение). Так что, IN иногда эффективнее, чем EXISTS, и наоборот, а NOT IT иногда эффективнее, чем NOT EXISTS (а иногда - нет).
2а) Да, в этом случае это условие наиболее эффективно.
3а) Обычно, да...
1б) Ладно, время продемонстрировать NOT IN и NOT EXISTS (Я подозревал, что рано или поздно это придется сделать!) Поехали:
ops$tkyte@ORA817DEV.US.ORACLE.COM> create table big as select * from all_objects; Table created. ops$tkyte@ORA817DEV.US.ORACLE.COM> create table small as select * from all_objects where rownum < 10; Table created. ops$tkyte@ORA817DEV.US.ORACLE.COM> alter table big modify object_id null; Table altered. ops$tkyte@ORA817DEV.US.ORACLE.COM> alter table small modify object_id null; Table altered. ops$tkyte@ORA817DEV.US.ORACLE.COM> update small set object_id = null where rownum = 1; 1 row updated. ops$tkyte@ORA817DEV.US.ORACLE.COM> create unique index big_idx on big(object_id); Index created. ops$tkyte@ORA817DEV.US.ORACLE.COM> create unique index small_idx on small(object_id); Index created. ops$tkyte@ORA817DEV.US.ORACLE.COM> analyze table big compute statistics 2 for table 3 for all indexes 4 for all indexed columns; Table analyzed. ops$tkyte@ORA817DEV.US.ORACLE.COM> analyze table small compute statistics 2 for table 3 for all indexes 4 for all indexed columns; Table analyzed.
Итак, все готово - таблицы у нас есть. Таблица SMALL будет использоваться в подзапросе. Давайте посмотрим, что теперь будет при использовании условий NOT IN и NOT EXISTS:
ops$tkyte@ORA817DEV.US.ORACLE.COM> set autotrace traceonly
ops$tkyte@ORA817DEV.US.ORACLE.COM> select * from big
2 where NOT EXISTS (select null
3 from small
4 where small.object_id = big.object_id)
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=845 Bytes=81965)
1 0 FILTER
2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=845 Bytes=81965)
3 1 INDEX (UNIQUE SCAN) OF 'SMALL_IDX' (UNIQUE)
Statistics
----------------------------------------------------------
305 recursive calls
15 db block gets
18278 consistent gets
0 physical reads
0 redo size
1961046 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
16886 rows processed
Итак, для каждой строки в BIG был выполнен поиск по индексу в таблице SMALL, чтобы проверить, существует в ней данная строка или нет. Неплохо... 18 тысяч операций consistent reads (много), но не настолько много:
ops$tkyte@ORA817DEV.US.ORACLE.COM> select * from big
2 where object_id NOT IN ( select object_id
3 from small )
4 /
no rows selected
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=845 Bytes=81965)
1 0 FILTER
2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=845 Bytes=81965)
3 1 TABLE ACCESS (FULL) OF 'SMALL' (Cost=1 Card=1 Bytes=3)
Statistics
----------------------------------------------------------
0 recursive calls
202743 db block gets
84706 consistent gets
0 physical reads
0 redo size
862 bytes sent via SQL*Net to client
319 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
0 rows processed
Ого, что тут происходит? Итак, для каждой строки в BIG мы полностью просматривали таблицу SMALL. Да, для каждой строки, - мы полностью просмотрели таблицу small около 16000 раз (а если бы она была не такой маленькой...). Обратите внимание также, что запрос вернул 0 строк - данные не найдены. Предыдущий пернул более 16 тысяч - вот вам и влияние значений NULL в подзапросе на условие NOT IN.
Итак, давайте избавимся от этого безобразия (это не обязательно, можно просто использовать соответствующее условие, и мы сделаем это для таблицы BIG в целях демонстрации):
ops$tkyte@ORA817DEV.US.ORACLE.COM> update small set object_id = -1 where object_id is null;
1 row updated.
ops$tkyte@ORA817DEV.US.ORACLE.COM> commit;
Commit complete.
ops$tkyte@ORA817DEV.US.ORACLE.COM> alter table small modify object_id NOT NULL;
Table altered.
ops$tkyte@ORA817DEV.US.ORACLE.COM> analyze table small compute statistics
2 for table
3 for all indexes
4 for all indexed columns;
Table analyzed.
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big
2 where NOT EXISTS (select null
3 from small
4 where small.object_id = big.object_id)
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=845 Bytes=81965)
1 0 FILTER
2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=845 Bytes=81965)
3 1 INDEX (UNIQUE SCAN) OF 'SMALL_IDX' (UNIQUE)
Statistics
----------------------------------------------------------
71 recursive calls
15 db block gets
18270 consistent gets
0 physical reads
0 redo size
1961046 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
16886 rows processed
Итак, особой разницы нет... Но:
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big b0
2 where object_id IS NOT NULL
3 and object_id NOT IN ( select object_id
4 from small )
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=845 Bytes=81965)
1 0 FILTER
2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=845 Bytes=81965)
3 1 INDEX (UNIQUE SCAN) OF 'SMALL_IDX' (UNIQUE)
Statistics
----------------------------------------------------------
0 recursive calls
15 db block gets
18243 consistent gets
0 physical reads
0 redo size
1961046 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
16886 rows processed
Ага, уже неплохо. Теперь результаты аналогичны NOT EXISTS. Но подождите, - можно и лучше. Начнем использовать антисоединения...
ops$tkyte@ORA817DEV.US.ORACLE.COM> alter session set always_anti_join = MERGE;
Session altered.
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big b1
2 where object_id is not null
3 and object_id NOT IN ( select object_id
4 from small )
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=269 Card=16885 Bytes=1857350)
1 0 MERGE JOIN (ANTI) (Cost=269 Card=16885 Bytes=1857350)
2 1 SORT (JOIN) (Cost=267 Card=16894 Bytes=1638718)
3 2 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=16894 Bytes=1638718)
4 1 SORT (UNIQUE) (Cost=3 Card=9 Bytes=117)
5 4 VIEW OF 'VW_NSO_1' (Cost=1 Card=9 Bytes=117)
6 5 INDEX (FULL SCAN) OF 'SMALL_IDX' (UNIQUE) (Cost=1 Card=9 Bytes=36)
Statistics
----------------------------------------------------------
0 recursive calls
19 db block gets
237 consistent gets
236 physical reads
0 redo size
1863304 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
4 sorts (memory)
1 sorts (disk)
16886 rows processed
А вот теперь работы намного меньше - с 18 тысяч значение consistent gets уменьшилось до 237, - неплохо. Антисоединение хэшированием дает аналогичные результаты:
ops$tkyte@ORA817DEV.US.ORACLE.COM> alter session set always_anti_join = HASH;
Session altered.
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big b2
2 where object_id is not null
3 and object_id NOT IN ( select object_id
4 from small )
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=145 Card=16885 Bytes=1857350)
1 0 HASH JOIN (ANTI) (Cost=145 Card=16885 Bytes=1857350)
2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=16894 Bytes=1638718)
3 1 VIEW OF 'VW_NSO_1' (Cost=1 Card=9 Bytes=117)
4 3 INDEX (FULL SCAN) OF 'SMALL_IDX' (UNIQUE) (Cost=1 Card=9 Bytes=36)
Statistics
----------------------------------------------------------
0 recursive calls
15 db block gets
237 consistent gets
31 physical reads
0 redo size
2350779 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
3 sorts (memory)
0 sorts (disk)
16886 rows processed
2б) - см. 1б)
3б) - см. 1б), где NOT IN возвращает 0 строк, а NOT EXISTS - 16 тысяч. Они не одинаковы, если имеются значения NULL.
Оптимизация антисоединений (Anti-Joins) и полусоединений (Semi-Joins)
Антисоединение возвращает строки слева от предиката, для которых нет соответствующей строки справа от предиката. То есть, оно возвращает строки, не соответствующие (NOT IN) подзапросу справа. Например, антисоединение может выбрать список сотрудников, не работающих в определенной группе отделов:
SELECT * FROM emp
WHERE deptno NOT IN
(SELECT deptno FROM dept
WHERE loc = 'HEADQUARTERS');
Оптимизатор стандартно использует для подзапросов NOT IN алгоритм вложенных циклов, если только параметр инициализации ALWAYS_ANTI_JOIN не имеет значения MERGE или HASH, и не выполнен ряд обязательных условий, позволяющих преобразовать подзапрос NOT IN в антисоединение сортировкой слиянием или хешированием. Можно поместить подсказку MERGE_AJ или HASH_AJ в подзапрос NOT IN, чтобы указать, какой алгоритм должен использовать оптимизатор.
Полусоединение возвращает строки, соответствующие подзапросу EXISTS, не дублируя строки слева от предиката, если критериям подзапроса соответствует несколько строк справа. Например:
SELECT * FROM dept
WHERE EXISTS
(SELECT * FROM emp
WHERE dept.ename = emp.ename
AND emp.bonus > 5000);
В этом запросе из dept надо возвращать только одну строку, даже если несколько строк в emp соответствуют подзапросу. Если по столбцу bonus в таблице emp нет индекса, то полусоединение может использоваться для повышения производительности запроса.
Ну и что изменится, если по столбцу bonus есть индекс?
Полусоединение и так выполнялось в представленном выше запросе, правильно?
Как использование подсказок MERGE_SJ или HASH_SJ в подзапросе EXISTS повысит производительность?
О чем говорят оптимизатору эти подсказки?
И, наконец, не могли бы вы дать примеры использования конструкций ANY, ALL и SOME, а именно, когда и как их использовать. Мы эти конструкции никогда не использовали.
ANY/SOME во многом аналогично IN.
where x in (select y from t)
where x = ANY(select y from t)
SOME - это псевдоним ANY, ANY - то же самое, что SOME.
Условия any/some могут пригодиться в неравенствах:
scott@ORA817DEV.US.ORACLE.COM> select ename, sal, comm from emp where sal < some(select comm from emp); ENAME SAL COMM ---------- ---------- ---------- A 800 WARD 1250 500 MARTIN 1250 1400 ADAMS 1100 JAMES 950 MILLER 1300 6 rows selected.
Этот запрос находит сотрудников, таких, что у кого-то есть комиссионные, превышающие их зарплату...
ALL - классная штука. Я использую эту конструкцию для поиска наиболее часто встречающихся значений, например:
scott@ORA817DEV.US.ORACLE.COM> select to_char(hiredate,'mm') 2 from emp 3 group by to_char(hiredate,'mm') 4 having count(*) >= ALL (select count(*) from emp group by to_char(hiredate,'mm')); TO -- 12
Этот запрос показывает, в каком месяце чаще всего принимали на работу... Он говорит: выбрать месяц, количество записей для которого не меньше ВСЕХ количеств записей по месяцам...
Возвращаясь к твоему примеру:
ops$tkyte@ORA817DEV.US.ORACLE.COM> alter session set always_anti_join = MERGE
2 /
Session altered.
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big b1
2 where object_id is not null
3 and object_id NOT IN ( select object_id
4 from small )
5 /
16886 rows selected.
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=269 Card=16885 Bytes=1857350)
1 0 MERGE JOIN (ANTI) (Cost=269 Card=16885 Bytes=1857350)
2 1 SORT (JOIN) (Cost=267 Card=16894 Bytes=1638718)
3 2 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=16894 Bytes=1638718)
4 1 SORT (UNIQUE) (Cost=3 Card=9 Bytes=117)
5 4 VIEW OF 'VW_NSO_1' (Cost=1 Card=9 Bytes=117)
6 5 INDEX (FULL SCAN) OF 'SMALL_IDX' (UNIQUE) (Cost=1 Card=9 Bytes=36)
Statistics
----------------------------------------------------------
0 recursive calls
19 db block gets
237 consistent gets
236 physical reads
0 redo size
1863304 bytes sent via SQL*Net to client
125305 bytes received via SQL*Net from client
1127 SQL*Net roundtrips to/from client
4 sorts (memory)
1 sorts (disk)
16886 rows processed
Том,
Предположим, имеется конструкция where с десятью условиями, и каждое
условие получает значение из подзапроса. В каком порядке будут выполняться эти
подзапросы?
select a from b,c were b.x=c.x and b.t=(select t from some ttablle where something) and c.l=(select l from some xtable where something)
И еще, при обработке c.l=(select l from some xtable where something), будет ли
select a from b,c were b.x=c.x and b.t=(select t from some ttablle where something)
считаться внешним запросом, или только
select a from b,c were b.x=c.x
При использовании IN, внешний запрос - это результирующее множество, которое будет соединяться с результатом выполнения IN.
Если подзапрос b.t = (select t ... ) обрабатывается ПЕРВЫМ (до c.l = ( select l...)), он и будет "внешним запросом" по отношению к c.1 = ( select l...).
Однако нельзя полагаться на порядок проверки условий!!! Мы можем и будем изменять его при необходимости.
Том и команда,
Большое спасибо. Я люблю ваш сайт.
Я работаю с Oracle 8.1.7.3. Мне нужна помощь при написании запроса с консрукцией NOT IN. Вот как выглядят базовые таблицы:
CREATE TABLE SKU
(ITEM_ID NUMBER NOT NULL,
SKU_CODE VARCHAR2(10) NOT NULL,
SKU_DESC VARCHAR2(255) NOT NULL
,
CONSTRAINT PK_SKU
PRIMARY KEY (ITEM_ID, SKU_CODE)
USING INDEX)
/
CREATE TABLE SKU_SOLD_OUT
(ITEM_ID NUMBER NOT NULL,
SKU_CODE VARCHAR2(10) NOT NULL,
SOLD_OUT_DATE DATE NOT NULL,
DELETED CHAR(1) NOT NULL,
SOLD_OUT_TEXT VARCHAR2(1000) NOT NULL
,
CONSTRAINT PK_SKU_SOLD_OUT
PRIMARY KEY (ITEM_ID, SKU_CODE, SOLD_OUT_DATE)
USING INDEX)
/
Для пары item_id, sku_code будет о или много строк в таблице sku_sold_out, но в каждый момент времени активной будет только одна (deleted = 'N').
Мне нужно получить список item-ов, которые не полностью распроданы. Т.е. если для Item_id есть четыре sku_codes, и три уже продано, вернуть item_id. Если все четыре sku_codes проданы, не возвращать item_id.
Вот какой запрос я сейчас использую.
select distinct item_id
from sku
where item_id||'-'||sku_code not in (
select item_id||'-'||sku_code
from sku_sold_out
where deleted = 'N'
and sold_out_date <= sysdate)
В sku - около 10000 строк
В sku_sold_out - около 120000 строк.
Нет ли способа получше? Схема зафиксирована и реально используется, так что я не могу менять базу данных, разве что, создать представление и т.п.
Спасибо за помощь.
Ну, еще можно вот так:
select distinct item_id
from sku
where (item_id,sku_code) not in ( select item_id, sku_code
from sku_sold_out
where deleted = 'N'
and sold_out_date <= sysdate)
/
select distinct sku.item_id
from sku, sku_sold_out
where sku.item_id = sku_sold_out.item_id (+)
and sku.sku_code = sku_sold_out.sku_code (+)
and nvl(sku_sold_out.deleted,'Y') = 'Y'
and nvl(sku_sold_out.sold_out_date,sysdate) <= sysdate
/
Привет, Том!
В первоначальном объяснении ты написал, что:
"Подзапрос выполняется, выбираются неповторяющиеся значения, результат индексируется (или хешируется, или сортируется) и затем..."
FILTER
набор строк 1
набор строк 2
Я понял, что FILTER - это своего рода "фильтр", убирающий строки, не соответствующие определенному условию. В данном случае, это, очевидно, не так.
Не мог бы ты объянить (или дать ссылку на описание), как работает эта фильтрация двух наборов строк? Мне кажется, что этого рода FILTER работает аналогично вложенным циклам и не допускает распараллеливания, так ли это?
1) Они сохраняются отсортированными или хешированными. Сама структура является, своего рода, неявным "индексом". Пусть делается соединение сортировкой слиянием - вы увидите две сорировки (out to temp), а затем слияние. При соединении хешированием, хешируется один из наборов.
Что касается пуyкта 2, я предполагаю, вы имеете ввиду примерно следующее:
ops$tkyte@ORA817DEV.US.ORACLE.COM> Select * from big b0 2 where object_id IS NOT NULL 3 and object_id NOT IN ( select object_id 4 from small ) 5 / 16886 rows selected. Execution Plan ---------------------------------------------------------- 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=845 Bytes=81965) 1 0 FILTER 2 1 TABLE ACCESS (FULL) OF 'BIG' (Cost=7 Card=845 Bytes=81965) 3 1 INDEX (UNIQUE SCAN) OF 'SMALL_IDX' (UNIQUE)
FILTER в данном случае, это действительно аналого соединения вложенными циклами. Выполняется полный просмотр таблицы и проверяется условие "is not null". Затем каждая строка должна быть профильтрована через NOT IN, и это делается по индексу.
Это можно распараллелить, если удастся распараллелить полный просмотр.
Оригинал обсуждения, по-прежнему, можно найти здесь.
Copyright © 2002 Oracle Corporation
Ну, что, не устали? Я уже немного устал. Обсуждение продолжается (в ноябре посыпались новые вопросы, по теме), так что следите за ним на сайте AskTom. Возможно, мы к нему еще вернемся в будущем.
Думаю все же сделать еще один авторский выпуск, посвященный синтаксису. Выпуск выйдет на следующей неделе. Следите за новостями на сайте проекта Open Oracle и пишите ваши комментарии в гостевую книгу сайта OpenXS Initiative.
С наилучшими пожеланиями,
В.К.
|
|