Поэтому мне было поручено оценить производительность двух запросов и я получил удивительный результат. Мне заранее сказали, что HAVING медленнее, чем WHERE, потому что он фильтрует результаты только после доступа к строкам. Это кажется разумным, и этот вопрос о порядке выполнения предложения SQL усилил это.
Тем не менее, я оценил производительность следующих запросов с некоторыми предположениями, и похоже, что при использовании HAVING выполнение на самом деле происходит быстрее!
SELECT status, count(status)
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant'
SELECT status, count(status)
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status
Предположения были такими:
CUSTOMER 100 000 записей.Исходя из этого, мои оценки были такими:
First query:
Accessing all rows, FROM: 100 000 * 0.01ms = 1000ms
GROUP BY: 100 000 * 0.005ms = 500ms
HAVING (2 conditions, 3 groups): 2 * 3 * 0.005ms = 0.03ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 1.65003s
Second query:
Accessing all the rows, FROM: 1000ms
WHERE: 2 * 100 000 * 0.005ms = 1000ms
GROUP BY: 15 000 * 0.005ms = 75ms
SELECT and COUNT results: 15 000 * 0.01ms = 150ms
Total execution time: 2.225s
Результат возник из-за того, что GROUP BY создает только три группы, которые очень легко фильтровать, в то время как WHERE должен проходить и фильтровать записи одну за другой.
Поскольку я наивно полагаюсь на авторитет, я предполагаю, что либо где-то допустил ошибку, либо сделанные предположения неверны.
Итак, GROUP BY ведет себя так же с HAVING, что приводит к сокращению времени выполнения?
PLAN_TABLE_OUTPUT /* With HAVING */
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5 | 35 | 4 (25)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | HASH GROUP BY | | 5 | 35 | 4 (25)| 00:00:01 |
| 3 | TABLE ACCESS STORAGE FULL| CUSM | 5 | 35 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
PLAN_TABLE_OUTPUT /* With WHERE */
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 4 (25)| 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 7 | 4 (25)| 00:00:01 |
|* 2 | TABLE ACCESS STORAGE FULL| CUSM | 1 | 7 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - storage("STATUS"<>'Active' AND "STATUS"<>'Dormant')
filter("STATUS"<>'Active' AND "STATUS"<>'Dormant')
@Lamak Как я уже сказал, это было задание. Полагаю, это был бы лучший подход в «реальном мире».
Но нет никакого смысла оценивать время, как это делаете вы. И вы спрашиваете, почему результат противоречит здравому смыслу ... и ответ, скорее всего, заключается в том, что такой способ оценки времени выполнения неверен (или слишком упрощен, я не знаю)
@Lamak Позвольте мне перефразировать вопрос ..
Да, я не понимаю, в чем проблема - наличие фильтров, установленных после роли, а где фильтры до - почему это нелогично, что наличие должно быть быстрее? и какая разница, просто используйте то, что работает лучше для вас
Есть ли у вас индексы в столбце «статус»? Если да, добавьте их к вопросу.
@TheImpaler Нет, думаю, нет.
У вас будет такое же время, если вы используете coun (*) вместо count (status)?
@TheImpaler Мне кажется, я упоминал, что мы просто оценивали производительность запросов на основе предположений, а не сравнивали реальные базы данных. Хотя мне сказали, что использование COUNT со звездочкой медленнее, чем присвоение имени подсчитываемому столбцу.
Кстати, COUNT(*) не то же самое, что COUNT(column), они могут возвращать разные значения.
@Lamak Да, я в курсе, но всегда полезно уточнить.
Что ж, чтобы ответить на ваш вопрос, вам нужно взглянуть на внутренности Oracle. В противном случае мы просто спекулируем. Я думаю, вам нужно получить план выполнения для каждого запроса и сравнить их (я могу помочь их прочитать). Сделайте: ОБЪЯСНИТЬ ПЛАН <запрос>; затем выберите plan_table_output из таблицы (dbms_xplan.display ('plan_table', null, 'normal'))
@TheImpaler Вот они. У нас есть фиктивная версия базы данных с несколькими записями. Так что там показано.


Вот в чем дело:
Согласно плану выполнения Oracle, оба запроса выполняют полное сканирование таблицы. То есть они читают ВСЕ РЯДЫ таблицы. Никакой разницы нет.
Запрос HAVING выполняет GROUP BY (хеширование), в результате чего получается 3 строки. Затем он применяет фильтр к этим трем строкам и возвращает результат.
Запрос WHERE применяет фильтр к каждой строке (100 000 в спецификации) после ее чтения, уменьшая их до 15 000. Наконец, он группирует их (хеширование) в 1 строку и возвращает одну строку.
Я думаю, что в случае, который вы описываете, запрос WHERE применяет фильтр ко всем 100000 строкам, в то время как запрос HAVING откладывает фильтр и применяет его только к 3 строкам. Это ускоряет выполнение запроса HAVING.
Не думайте, что этот результат будет применяться к каждому подобному запросу. Oracle очень умно использует статистику таблиц. В будущем план изменится в соответствии с реальными данными, которые вы добавляете в таблицу. План с 5 рядами ни в коем случае не соответствует плану на 100 000 строк.
Воспринимайте этот результат с недоверием. Сценарии реального мира намного сложнее.
Спасибо! Так и было, как я думал.
Да, починил.
Одно из ваших предположений неверно: HAVING медленнее, чем WHERE, потому что он фильтрует результаты только после доступа к строкам и хеширование.
Это та часть хеширования, которая делает условия HAVING более дорогими, чем условия WHERE. Хеширование требует записи данных, что может быть более затратным как физически, так и алгоритмически.
Хеширование требует как записи, так и чтения данных. В идеале хеширование данных будет выполняться за время O(n). Но на практике будут возникать хеш-коллизии, которые замедляют работу. И на практике не все данные умещаются в памяти.
Эти две проблемы могут иметь катастрофические последствия. В худшем случае при ограниченной памяти хеширование требует нескольких проходов, а сложность приближается к O(n^2). А запись на диск во временном табличном пространстве на порядки медленнее, чем запись в память.
Это те проблемы производительности, о которых вам нужно беспокоиться при работе с базами данных. Постоянное время выполнения простых условий и выражений обычно не имеет значения по сравнению со временем чтения, записи и соединения данных.
Это может быть особенно актуально в вашей среде. Операция TABLE ACCESS STORAGE FULL подразумевает, что вы используете Exadata. В зависимости от платформы вы можете использовать SQL в кремнии. Эти высокоуровневые условия могут идеально транслироваться в низкоуровневые инструкции, выполняемые на устройствах хранения. Это означает, что ваша оценка затрат на выполнение пункта может быть на несколько порядков завышенной.
Создайте образец таблицы со 100 000 строками:
create table customer(id number, status varchar2(100));
insert into customer
select
level,
case
when level <= 15000 then 'Deceased'
when level between 15001 and 50001 then 'Active'
else 'Dormant'
end
from dual
connect by level <= 100000;
begin
dbms_stats.gather_table_stats(user, 'customer');
end;
/
Выполнение кода в цикле показывает, что версия WHERE примерно в два раза быстрее, чем версия HAVING.
--Run times (in seconds): 0.765, 0.78, 0.765
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
GROUP BY status
HAVING status != 'Active' AND status != 'Dormant';
end loop;
end;
/
--Run times (in seconds): 0.39, 0.39, 0.39
declare
type string_nt is table of varchar2(100);
type number_nt is table of number;
v_status string_nt;
v_count number_nt;
begin
for i in 1 .. 100 loop
SELECT status, count(status)
bulk collect into v_status, v_count
FROM customer
WHERE status != 'Active' AND status != 'Dormant'
GROUP BY status;
end loop;
end;
/
почему вы оцениваете такое время вместо того, чтобы проверять, какое на самом деле быстрее?