LISTAGG оценивается и терпит неудачу в недостижимом операторе case

Я столкнулся с очень необычным поведением функции LISTAGG в Oracle.

Я знаю, что LISTAGG не работает, если он имеет дело с более чем 4000 символов.

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

CREATE TABLE EMP (
  ID  VARCHAR2(401),
  DEP VARCHAR2(10)
);

INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 401), 'FOO'); -- Run exactly 9 times
INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 5), 'BAR');   -- Run 3 times

Для простоты давайте проигнорируем мой особый случай для количества > 100 и просто скажем, что FOO следует исключить, а BAR следует включить.

SELECT DEP,
  CASE
    WHEN DEP = 'BAR' THEN
      LISTAGG(ID, ',')
        WITHIN GROUP (ORDER BY NULL)
        OVER (PARTITION BY DEP)
    ELSE
      'Too many to count'
  END AS ID_LIST
FROM EMP;

Это дает результаты, которые должны выглядеть так (но с другими случайными символами):

Успех

Однако, добавив всего одну дополнительную строку, доведя общее количество в отделе FOO до 10...

INSERT INTO EMP VALUES (DBMS_RANDOM.string('A', 401), 'FOO'); -- Same as before

Заставляет нас столкнуться с исключением при повторном запуске того же выбора:

ORA-01489: result of string concatenation is too long  
01489. 00000 -  "result of string concatenation is too long"  
*Cause:    String concatenation result is more than the maximum size.  
*Action:   Make sure that the result is less than the maximum size.  

Как ни странно, это происходит, даже если условие в операторе case изменено на 1=2.

Я не уверен, что здесь происходит. Кажется, что SQL решает оценить оператор независимо от того, намерен ли он его использовать, поэтому терпит неудачу, когда встречается с LISTAGG из 4000+ символов.

У меня есть несколько решений моей проблемы, но я действительно хотел бы узнать больше о том, почему SQL решил (очевидно) запустить LISTAGG, даже если он никогда не должен быть достигнут.


person Addison    schedule 25.08.2016    source источник


Ответы (1)


Окончательная оценка столбцов/выражений списка выбора, включая сокращенное выражение case, происходит после извлечения данных. Любая группировка и т. д. к этому моменту уже выполнена.

Этот эффект происходит не только с listagg(), его можно увидеть с любым вызовом агрегатной или аналитической функции в возвращаемом выражении, хотя его трудно заметить, если нет побочных эффектов.

В качестве демонстрации я создал простой пакет с функцией, которую я могу вызвать из запроса:

create package p as
  n number := 0;
  function f return number;
end;
/

create package body p as
  function f return number as
  begin
    n := n + 1;
    return n;
  end;
end;
/

По сути, это имитация последовательности, специфичной для сеанса; последовательности также демонстрируют такое поведение, но, по-видимому, по другой причине, поэтому я не хотел использовать их для этого.

Вызов этой функции внутри выражения case делает то, что вы ожидаете; он вызывается только при выполнении условия:

select dep,
  case
    when dep = 'BAR' then
      p.f
    else
      -1
  end as id_list
from emp;

DEP        ID_LIST
---------- -------
FOO             -1
...
BAR              1
BAR              2
BAR              3
FOO             -1

select p.f from dual;

         F
----------
         4

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

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |    13 |    91 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| EMP  |    13 |    91 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

Вместо этого с помощью совокупного вызова:

select dep,
  case
    when dep = 'BAR' then
      count(p.f)
    else
      -1
  end as id_list
from emp
group by dep;

DEP        ID_LIST
---------- -------
FOO             -1
BAR              3

select p.f from dual;

         F
----------
        18

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    13 |    91 |     4  (25)| 00:00:01 |
|   1 |  HASH GROUP BY     |      |    13 |    91 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    13 |    91 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------

... функция вызывалась 13 раз вместо 3; план показывает хеш-группу пошагово, что должно произойти во всех извлеченных строках, прежде чем случай будет оценен.

Аналогично для аналитической версии:

select dep,
  case
    when dep = 'BAR' then
      count(p.f) over (partition by dep)
    else
      -1
  end as id_list
from emp;

DEP        ID_LIST
---------- -------
BAR              3
BAR              3
BAR              3
FOO             -1
...

select p.f from dual;

         F
----------
        32

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    13 |    91 |     4  (25)| 00:00:01 |
|   1 |  WINDOW SORT       |      |    13 |    91 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    13 |    91 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------

... снова функция вызывалась 13 раз, поскольку сортировка окна (и, следовательно, аналитический расчет) была выполнена до того, как можно было оценить выражение case.

Таким образом, проблема на самом деле не в том, что выражение возврата (listagg() в вашем случае) оценивается в выражении case, когда этого не должно быть; он оценивается и выдает исключение еще до того, как будут рассмотрены условия выражения case.

person Alex Poole    schedule 25.08.2016
comment
Все еще странно, что оптимизатор не работает лучше, когда первое условие when в операторе case равно 1=0. Оптимизатор должен переписать case when 1=0 then <whatever> else 'Too many to count' end as id_list в 'Too many to count' as id_list еще до того, как все это пойдет на компиляцию. Оптимизатор довольно умен в других частях разбора запросов, но, по-видимому, не так в этом случае. - person mathguy; 25.08.2016
comment
В примечании MOS предлагается выполнить оптимизацию для сравнения чисел (поэтому 1 = 0), но не для сравнения строк ('T' = 'F'). Но даже в этом случае это должно быть слишком поздно в процессе - это будет сделано в версии без агрегации, но масштаб этого переписывания запроса может быть слишком большим, когда ему придется беспокоиться об агрегации и т. д. Может быть, это слишком ниша для них, чтобы посмотрели... - person Alex Poole; 25.08.2016
comment
Спасибо Алекс, это было очень подробное объяснение. Я должен был подумать, чтобы сделать план объяснения по этому поводу самостоятельно. - person Addison; 26.08.2016