WHERE vs. HAVING с GROUP BY

Поэтому мне было поручено оценить производительность двух запросов и я получил удивительный результат. Мне заранее сказали, что 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 записей.
  • Стоимость доступа к строке составляет 0,01 мс (SELECT + COUNT)
  • Стоимость выполнения клаузулы 0.005мс.
  • Существует три типа статусов клиентов: два выше и «Умерший».
  • 15 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 10.04.2018 18:17

@Lamak Как я уже сказал, это было задание. Полагаю, это был бы лучший подход в «реальном мире».

Felix 10.04.2018 18:19

Но нет никакого смысла оценивать время, как это делаете вы. И вы спрашиваете, почему результат противоречит здравому смыслу ... и ответ, скорее всего, заключается в том, что такой способ оценки времени выполнения неверен (или слишком упрощен, я не знаю)

Lamak 10.04.2018 18:21

@Lamak Позвольте мне перефразировать вопрос ..

Felix 10.04.2018 18:21

Да, я не понимаю, в чем проблема - наличие фильтров, установленных после роли, а где фильтры до - почему это нелогично, что наличие должно быть быстрее? и какая разница, просто используйте то, что работает лучше для вас

Daniel Marcus 10.04.2018 18:22

Есть ли у вас индексы в столбце «статус»? Если да, добавьте их к вопросу.

The Impaler 10.04.2018 18:24

@TheImpaler Нет, думаю, нет.

Felix 10.04.2018 18:25

У вас будет такое же время, если вы используете coun (*) вместо count (status)?

The Impaler 10.04.2018 18:28

@TheImpaler Мне кажется, я упоминал, что мы просто оценивали производительность запросов на основе предположений, а не сравнивали реальные базы данных. Хотя мне сказали, что использование COUNT со звездочкой медленнее, чем присвоение имени подсчитываемому столбцу.

Felix 10.04.2018 18:30

Кстати, COUNT(*) не то же самое, что COUNT(column), они могут возвращать разные значения.

Lamak 10.04.2018 18:33

@Lamak Да, я в курсе, но всегда полезно уточнить.

Felix 10.04.2018 18:34

Что ж, чтобы ответить на ваш вопрос, вам нужно взглянуть на внутренности Oracle. В противном случае мы просто спекулируем. Я думаю, вам нужно получить план выполнения для каждого запроса и сравнить их (я могу помочь их прочитать). Сделайте: ОБЪЯСНИТЬ ПЛАН <запрос>; затем выберите plan_table_output из таблицы (dbms_xplan.display ('plan_table', null, 'normal'))

The Impaler 10.04.2018 18:36
ОБЪЯСНИТЕ ПЛАН ДЛЯ <запрос> - у меня плохо.
The Impaler 10.04.2018 18:45

@TheImpaler Вот они. У нас есть фиктивная версия базы данных с несколькими записями. Так что там показано.

Felix 10.04.2018 18:52
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
6
14
1 504
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вот в чем дело:

  1. Согласно плану выполнения Oracle, оба запроса выполняют полное сканирование таблицы. То есть они читают ВСЕ РЯДЫ таблицы. Никакой разницы нет.

  2. Запрос HAVING выполняет GROUP BY (хеширование), в результате чего получается 3 строки. Затем он применяет фильтр к этим трем строкам и возвращает результат.

  3. Запрос WHERE применяет фильтр к каждой строке (100 000 в спецификации) после ее чтения, уменьшая их до 15 000. Наконец, он группирует их (хеширование) в 1 строку и возвращает одну строку.

Я думаю, что в случае, который вы описываете, запрос WHERE применяет фильтр ко всем 100000 строкам, в то время как запрос HAVING откладывает фильтр и применяет его только к 3 строкам. Это ускоряет выполнение запроса HAVING.

Не думайте, что этот результат будет применяться к каждому подобному запросу. Oracle очень умно использует статистику таблиц. В будущем план изменится в соответствии с реальными данными, которые вы добавляете в таблицу. План с 5 рядами ни в коем случае не соответствует плану на 100 000 строк.

Воспринимайте этот результат с недоверием. Сценарии реального мира намного сложнее.

Спасибо! Так и было, как я думал.

Felix 10.04.2018 19:08

Да, починил.

The Impaler 10.04.2018 19:10
Ответ принят как подходящий

Одно из ваших предположений неверно: 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;
/

Другие вопросы по теме