Условные функции

Условные функции - это такие функции, которые могут возвращать разные результаты в зависимости от выполнения тех или иных условий.

В качестве тестовых данным будем использовать таблицу из части про функции для работы с NULL.

DECODE

Функция DECODE в общем случае имеет следующий вид:

DECODE(что сравниваем,
       значение1, результат1,
       значение2, результат2,
       значение3, результат3,
       ....
       значениеN, результатN,
       значение по-умолчанию)               

Первым DECODE принимает параметр, значение которого будет сравниваться по очереди со списком значений, и в случае, когда он совпадет с одним из перечисленных, будет возвращен соответствующий результат. Если совпадений не найдено, будет возвращено значение по-умолчанию. Если значение по-умолчанию не указано, будет возвращен NULL.

Аргументы могут быть числового, строкового типа, или датой.

DECODE может сравнивать NULL значения:

select login,
       status,
       decode(status, -- <- Что сравниваем
              null, 'Статус не указан', -- <- пара Значение-Результат
              'Статус указан') has_status -- <- Значение по-умолчанию
from profiles
| LOGIN   | STATUS                      | HAS_STATUS       |
| johndoe | -                           | Статус не указан |
| admin   | Я админ. Все вопросы ко мне | Статус указан    |
| nelsol  | -                           | Статус не указан |

Перед сравнением Oracle автоматически приводит первый параметр и все значения к типу первого значения в списке параметров. Результат функции автоматически приводится к типу первого результата в списке параметров. Если первый результат в списке - NULL, результат функции DECODE будет приведен к строковому типу VARCHAR2.

Например, следующий запрос не выполнится из-за ошибки ORA-01722: invalid number:

select decode(login,
              'admin', 10,
              'Администратор') has_status
from profiles

Тип возвращаемого значения определяется первым результатом в списке параметров, в данном случае - числом "10". Но значение по-умолчанию имеет строковый тип, что и приводит к ошибке. Чтобы ошибки не было, нужно либо значение по-умолчанию заменить на число, либо заменить число 10 на любой строковый тип.

Любой из следующих запросов отработает без ошибок:

select decode(login,
              'admin', 'Администратор',
              'Не администратор') admin_login
from profiles;

select decode(login,
              'admin', 10,
              20) admin_login_flag
from profiles;

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

select login,
       length(login) login_length,
       decode(length(login),
              5, 'Пять',
              6, 'Шесть',
              'Не пять и не шесть') admin_login_length
from profiles
| LOGIN   | LOGIN_LENGTH | ADMIN_LOGIN_LENGTH |
| admin   |            5 | Пять               |
| johndoe |            7 | Не пять и не шесть |
| nelsol  |            6 | Шесть              |

Максимальное количество параметров в функции DECODE - 255.

Предыдущий пример, только с использованием вложенного DECODE:

select login,
       length(login) login_length,
        decode(length(login),
              5, 'Пять',
              decode(length(login),
                     6, 'Шесть',
                     'Не пять и не шесть')) admin_login_length
from profiles

Здесь в качестве значения по-умолчанию выступает еще один DECODE.

На практике вложенных decode следует избегать, ровно как и decode с большим количеством параметров.

Одна из распространенных ошибок - использовать DECODE для того, чтобы преобразовать какие-либо флаги в их строковые эквиваленты (при их большом количестве):

select a.*,
       decode(a.status,
              1, 'Закрыт',
              2, 'Отменен',
              3, 'Новый',
              4, 'В обработке'
       ) status_name
from some_table a

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

select a.*,
       st.status_name
from some_table a
join statuses st on st.status_code = a.status

CASE

Выражение CASE во многом похоже на DECODE, но обладает большими возможностями. Данное выражение позволяет реализовать полноценную условную логику в SQL запросе.

CASE может использоваться в двух вариантах - простом(англ. simple case expression) и поисковом(англ. searched case expression).

Простой CASE по принципу работы идентичен DECODE:

select login,
       case login -- <- что сравниваем
           when 'admin' then 'Администратор' -- Результат 1
           when 'johndoe' then 'Джон До' -- Результат 2
           else 'Другой пользователь' -- Значение по-умолчанию
       end user_flag    
from profiles
| LOGIN   | USER_FLAG           |
| admin   | Администратор       |
| johndoe | Джон До             |
| nelsol  | Другой пользователь |

user_flag здесь - псевдоним для столбца. Само выражение начинается с ключевого слова case и заканчивается ключевым словом end.

Как и в DECODE, для проверяемого значения начинают производиться сравнения со значениями в блоках WHEN. При первом же совпадении функция завершает работу и возвращает соответствующий результат (указанный после then). В случае, если ни одного совпадения не было найдено, возвращается значение, указанное в блоке ELSE. Если значение по-умолчанию не указано, будет возвращен NULL.

Searched case expression, в отличие от simple case expression, является куда более мощным инструментом. В отличие от последнего, в searched case expression в блоках when указываются условия, а не просто значения для сравнения:

select login,
       case
           when login = 'admin' then 'Администратор'
           else 'Не администратор'
       end is_admin
from profiles
| LOGIN   | IS_ADMIN         |
| admin   | Администратор    |
| johndoe | Не администратор |
| nelsol  | Не администратор |
select login,
       case
           when length(login) = 5 then 'Пять'
           when length(login) > 5 then 'Больше пяти'
           when length(login) between 0 and 4 then 'От 0 до 4'
       end login_length_stats
from profiles
| LOGIN   | LOGIN_LENGTH_STATS |
| admin   | Пять               |
| johndoe | Больше пяти        |
| nelsol  | Больше пяти        |

В общем и целом, лучше использовать DECODE для небольших, простых сравнений, и CASE для более сложных, т.к. он лучше читается.

Условные функции в WHERE части

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


-- Выведет профили пользователей, которые
-- не являются администраторами
select *
from profiles
where case
          when login = 'admin' then 1
          else 0
       end = 0
| LOGIN   | LAST_UPDATED | STATUS |
| johndoe | 01-JAN-09    | -      |
| nelsol  | -            | -      |

В примере выше выражение case вернет 0 в тех случаях, когда логин пользователя не будет логином администратора. Сразу после окончания выражения мы сравниваем его с нулем, тем самым получая только не-администраторов. Подобные способы, конечно, лучше не использовать, а вместо них прибегать к классическому варианту написания запроса, который будет более понятным:

select *
from profiles
where login <> 'admin'

При группировке условные функции, как и все другие, должны быть полностью продублированы в GROUP BY, использовать псевдоним колонки не получится:

-- Этот запрос не сработает
select case
           when length(login) > 5 then '> 5'
           when length(login) < 5 then '< 5'
           when length(login) = 5 then '= 5'
       end login_length,
       count(*) cnt
from profiles
group by login_length;

-- А вот этот отработает корректно
select case
           when length(login) > 5 then '> 5'
           when length(login) < 5 then '< 5'
           when length(login) = 5 then '= 5'
       end login_length,
       count(*) cnt
from profiles
group by case
           when length(login) > 5 then '> 5'
           when length(login) < 5 then '< 5'
           when length(login) = 5 then '= 5'
       end
| LOGIN_LENGTH | CNT |
| = 5          |   1 |
| > 5          |   2 |

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

Последний запрос можно переписать с использованием подзапроса, чтобы не дублировать CASE в GROUP BY:

select login_length,
       count(*)
from (
    select case
               when length(login) > 5 then '> 5'
               when length(login) < 5 then '< 5'
               when length(login) = 5 then '= 5'
           end login_length,
           login,
           status,
           last_updated
    from profiles
)
group by login_length

Комментарии