воскресенье, 10 августа 2008 г.

ORA-01427: подзапрос одиночной строки возвращает более одной строки

Приходилось сталкиваться с такой ошибкой?
Читаем дальше.

Зачем же использовать такие подзапросы, коль возможны ошибки?
Но ведь удобно же!

Пример.
Ест у нас запрос, который скажем выводит некий список расходных транзакций модуля Inventory


SELECT ...
FROM mtl_material_transactions mmt
WHERE ...

Нам здесь не важно, что выводит этот список и по какому критерию, но нужно отметить, что за многоточиями может скрываться не один десяток, а то и не одна сотня, строк кода.

Но вот возникла необходимость добавить в запрос еще одну колонку - счет ГК с которого списали ТМЦ. Мы знаем, что в таблице mtl_transaction_accounts по коду складской транзакции можно найти две полупроводки, одна с положительной суммой (дебет), другая с отрицательной (кредит). Ну вот значит счет кредитовой полупроводки нас и интересует. Самым простым способом "вклиниться" в существующий запрос будет что-то такое:

SELECT ...
,(SELECT mta.reference_account
FROM mtl_transaction_accounts mta
WHERE mta.transaction_id = mmt.transaction_id
AND mta.base_transaction_value < 0
) AS "Счет учета ТМЦ"
FROM mtl_material_transactions mmt
WHERE ...

Запускаем - беда!
ORA-01427: подзапрос одиночной строки возвращает более одной строки

Почему возникла ошибка?
Для некоторых складских транзакций наши предположения оказались неверны.
И это плохо.
А плохо потому, что проблемными могут оказаться всего лишь пара записей из нескольких тысяч, а мы не увидим ничего, т.к. весь запрос возвращает ошибку.

Прежде чем начинать исправить ситуацию, нужно понять а какой собственно результат запроса был бы приемлемым, учитывая наличие складских транзакций с неожиданными распределениями(проводками)?

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

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

Одна запись из нескольких может получиться при использовании групповых функций.
Ну что же:

SELECT ...
,(SELECT MAX(mta.reference_account)
FROM mtl_transaction_accounts mta
WHERE mta.transaction_id = mmt.transaction_id
AND mta.base_transaction_value < 0
) AS "Счет учета ТМЦ"
FROM mtl_material_transactions mmt
WHERE ...

Однако. Как оказалось, вернуть что-нибудь - не проблема, проблема потом понять что получили. Применив групповую функцию MAX мы гарантируем, что ошибки ORA-01427 больше не будет. Какой-нибудь счет да вернется. Но при таком подходе, мы никогда и не узнаем, что у нас есть записи с некорректно
определенным счетом.

Тем не менее, главный шаг к правильному решению уже сделан, осталось чуть-чуть. Ведь во всех случаях, где подзапрос возвращает одну запись, использование MAX не является ошибкой - максисум от одного значения равен самому значению. Значит нам нужно в тех случаях, где подзапрос возвращает одну запись - использовать
MAX (ну хотите MIN). А там где больше чем одну - возвращать значение, указывающее на ошибку.

Так ведь это же совсем не сложно сделать!
Количество записей подзапроса - это COUNT(*), условную логику можно реализовать через CASE или, по старинке, через DECODE. Не забудем и про то, что подзапрос может совсем не вернуть записей:
  
SELECT ...
,(SELECT DECODE(COUNT(*), 1,MAX(mta.reference_account), 0,NULL, -999)
FROM mtl_transaction_accounts mta
WHERE mta.transaction_id = mmt.transaction_id
AND mta.base_transaction_value < 0
) AS "Счет учета ТМЦ"
FROM mtl_material_transactions mmt
WHERE ...

Всё.
Теперь не только ошибка ORA-01427 больше не появится, но и можно легко найти те записи, где наша логика определения счета учета ТМЦ дала сбой.

Дополнительно отметим, что так как mta.reference_account имеет числовой тип данных, то и ошибочное значение должно быть числовым (-999). Для строковых типов данных можно было бы использовать - 'Ошибка' или 'ORA-01427'. Для дат - что-то из далекого прошлого или будущего. Важно лишь, чтобы такого значения гарантированно не было в реальных данных.

Подводим итоги.
При использовании подзапросов вместо

SELECT
(SELECT t2.column
FROM table2
WHERE ...)
FROM table1 t1
WHERE ...

лучше использовать

SELECT
(SELECT DECODE(COUNT(*), 0,NULL, 1,MAX(t2.column), 'ORA-01427')
FROM table2
WHERE ...)
FROM table1 t1
WHERE ...

И не забыть разобраться почему появились записи с 'ORA-01427'

среда, 6 августа 2008 г.

Forms. Особенности режима ввода запроса

Режим ввода запроса это то, что делает Forms продуктом с "интуитивно непонятным пользовательским интерфейсом" с одной стороны, и замечательная, очень функциональная особенность с другой стороны.

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

В чем же состоит эта реализация.

Для того чтобы выполнить поиск пользователю нужно запомнить и уметь нажимать на две "магические" клавиши [Ввести запрос] и [Выполнить запрос]. В зависимости от выбранной раскладки клавиатуры ими часто бывают F11/Ctrl-F11 или F7/F8 (задаются/меняются на сервере приложений в текстовом файле frmweb.res)

IMHO, именно необходимость запоминания и делает Forms интуитивно непонятным, ибо интуитивно понятным выглядит движение мыши в сторону кнопки с надписью "Найти".

Так какие же горизонты открываются перед пользователем, после того как клавиша [Ввести запрос] нажата либо им, либо заботливой рукой рядом стоящего консультанта.

Для начала отметим, что действия пользователя в режиме ввода запроса сводятся к тому, чтобы вводить шаблоны значений в поля формы. При выполнении запроса, итоговая фраза WHERE будет содержать отдельное условие для каждого поля, в которое вводился шаблон. Отдельные условия будут соединены через логическое AND.
Здесь рассказывается о том, как узнать какой же в итоге запрос выполнялся.

Ниже перечислены возможности режима ввода запроса, начиная с широко известных и продолжая менее именитыми:

1. Шаблон поля не содержит символов % и _ при этом не начинается с < > = != # :
Условие строится по правилу:
WHERE поле = 'шаблон'

Используется когда нужно найти все записи по точному значению поля.

Пример


Поле Тип Шаблон поиска Условие
---------- ------ ------------- --------------------
USER_NAME Строка SYSADMIN WHERE USER_NAME = 'SYSADMIN'
ENTERED_DB Число 234.12 WHERE ENTERED_DB = 234.12
START_DATE Дата 01.07.08 WHERE START_DATE = TO_DATE('01.07.08','DD.MM.RRRR')*

*Формат даты для функции TO_DATE задается параметром FORMS60_USER_DATE_FORMAT и обычно выставляется в 'DD.MM.RRRR'.

2. Шаблон поля содержит символы % и/или _ при этом не начинается с < > = != # :
Условие строится по правилу:
WHERE поле LIKE 'шаблон'

Используется когда нужно найти все записи по заданному шаблону,
где % заменяет любое количество символов, а _ заменяет любой один символ.

Чаще всего используется для поиска по известной подстроке.
Для дат можно хитрым образом задавать диапазоны, в примере ниже показан поиск всех дат за месяц.

Пример

Поле Тип Шаблон поиска Условие
---------- ------ ------------- --------------------
USER_NAME Строка SYS% WHERE USER_NAME LIKE 'SYS%'
ENTERED_DB Число 234.% WHERE ENTERED_DB LIKE '234.%'
START_DATE Дата %.07.2008 WHERE TO_CHAR(START_DATE, 'DD.MM.RRRR' LIKE '%.07.2008')


3. Для чисел и дат работают операторы сравнения
Для этого шаблон должен начинаться с >, >=, <, <=, <>
или для поиска по диапазону значений
#BETWEEN min AND max

Пример

Поле Тип Шаблон поиска Условие
---------- ------ ------------- --------------------
ENTERED_DB Число >234 WHERE ENTERED_DB > 234
ENTERED_DB Число <=0 WHERE ENTERED_DB <= 0
ENTERED_DB Число #BETWEEN 0 AND 10 WHERE ENTERED_DB BETWEEN 0 AND 10
START_DATE Дата <>01.07.2008 WHERE START_DATE <> TO_DATE('01.07.2008','DD.MM.RRRR')

4. Для любых типов данных работает проверка на неопределенные(пустые) значения
Для этого используется один из шаблонов #IS NULL или #IS NOT NULL

Пример

Поле Тип Шаблон поиска Условие
---------- ------ ------------- ----------------------------
START_DATE Дата #IS NULL WHERE START_DATE IS NULL
DESCRIPTION Строка #IS NOT NULL WHERE DESCRIPTION IS NOT NULL

5. Шаблоны, начинающиеся с # в общем случае.
Вообще то, после начального # можно использовать не только BETWEEN или IS [NOT] NULL, но и любой другой текст, подстановка которого сразу после имени поля приведет к правильно построенному условию.

Пример

Поле Шаблон Условие
---------- ------ --------------------
CREATION_DATE #>TRUNC(SYSDATE) WHERE CREATION_DATE > TRUNC(SYSDATE)
CODE #<>RTRIM(CODE) WHERE CODE <> RTRIM(CODE)

Отметим использование функций (TRUNC, RTRIM,...).
Причем функции могут быть не только системными, но и пользовательскими.

6. Венцом возможностей по построению запроса является использование диалога Query/Where.
По своей сути это возможность напрямую написать текст команды SELECT после фразы WHERE.

Для человека понимающего в синтаксисе команды SELECT это дает огромные возможности по формированию запроса и не только.

Но вот из-за этого "и не только" в ОЕБС начиная с версии 11.5.9 эта возможность обрезана, также как и пункт 5 (Использование # в общем случае). Дело в том, что и 5-й, и 6-й пункты позволяют пользователю включать в текст запроса пользовательские функции. А пользовательские функции, начиная с версии СУБД 8i, могут не только читать данные, но и ,окаймленные автономной транзакцией, изменять их.
А это серьезная дыра в безопасности. Ведь технически подкованный пользователь, сговорившись со специалистом службы поддержки, может творить в системе всё что угодно.
Второй будет как бы случайно писать функции, модифицирующие данные, а первый, как бы случайно их вызывать, выполняя невинные запросы в экранной форме.

Пресекается это переменной FORMS60_RESTRICT_ENTER_QUERY, выставляемой в TRUE на сервере приложений.

Подробнее об этом можно почитать в ноте
257788.1 - Enter Query + :a Does Not Open Query/Where Window ORA-1403

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