Я сократил реальную проблему до следующего контрольного примера:
DROP TABLE test_users;
CREATE TABLE test_users (
user_id INTEGER,
username VARCHAR2(32),
first_name VARCHAR2(40),
last_name VARCHAR2(40)
);
ALTER TABLE test_users ADD
(
CONSTRAINT test_users_pk
PRIMARY KEY (user_id)
USING INDEX
)
/
ALTER TABLE test_users ADD
(
CONSTRAINT test_users_uq
UNIQUE (username)
USING INDEX
)
/
INSERT INTO test_users VALUES (1, 'A', 'Sneezy', 'Timon');
INSERT INTO test_users VALUES (2, 'B', 'Dopey', 'Simba');
INSERT INTO test_users VALUES (3, 'C', 'Happy', 'Nala');
INSERT INTO test_users VALUES (4, 'D', 'Grumpy', 'Pumbaa');
COMMIT;
CREATE OR REPLACE FUNCTION test_function RETURN test_users.user_id%TYPE IS
identifier VARCHAR2(32);
user_id users.user_id%TYPE;
BEGIN
SELECT sys_context('userenv', 'client_identifier') INTO identifier FROM dual;
SELECT user_id INTO user_id FROM test_users WHERE upper(username) = upper(identifier);
dbms_output.put_line('TEST_FUNCTION called!');
RETURN user_id;
END test_function;
-- Testing with disabled result cache
ALTER TABLE test_users RESULT_CACHE (MODE DEFAULT);
DECLARE
f users.first_name%TYPE;
last_name users.last_name%TYPE;
identifier VARCHAR2(32);
l_user_id users.user_id%type;
BEGIN
dbms_output.put_line('setting the session identifier to A (Sneezy, Timon):');
dbms_session.set_identifier('A');
l_user_id := test_function();
dbms_output.put_line('function call in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = test_function();
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('variable use in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = l_user_id;
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('----');
dbms_output.put_line('setting the session identifier to B (Dopey Simba):');
dbms_session.set_identifier('B');
l_user_id := test_function();
dbms_output.put_line('function call in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = test_function();
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('variable use in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = l_user_id;
dbms_output.put_line(f || ' ' || last_name);
END;
/
-- Testing with enabled result cache
ALTER TABLE test_users RESULT_CACHE (MODE FORCE);
DECLARE
f users.first_name%TYPE;
last_name users.last_name%TYPE;
identifier VARCHAR2(32);
l_user_id users.user_id%type;
BEGIN
dbms_output.put_line('setting the session identifier to A (Sneezy, Timon):');
dbms_session.set_identifier('A');
l_user_id := test_function();
dbms_output.put_line('function call in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = test_function();
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('variable use in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = l_user_id;
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('----');
dbms_output.put_line('setting the session identifier to B (Dopey Simba):');
dbms_session.set_identifier('B');
l_user_id := test_function();
dbms_output.put_line('function call in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = test_function();
dbms_output.put_line(f || ' ' || last_name);
dbms_output.put_line('variable use in WHERE criteria:');
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = l_user_id;
dbms_output.put_line(f || ' ' || last_name);
END;
/
Индексы могут быть или не быть необходимыми. Идея в том, что имя текущего пользователя находится в идентификаторе сеанса. Функция теста превращает имя пользователя в идентификаторе сеанса в идентификатор пользователя. Имена пользователей могут (теоретически) меняться и использоваться как имена для входа. Идентификатор пользователя никогда не должен изменяться и, следовательно, является PK таблицы.
Что меня беспокоит, так это то, что при включении кэша результатов функция в критериях WHERE этого оператора не всегда вызывается:
SELECT first_name, last_name INTO f, last_name FROM test_users WHERE user_id = test_function();
Первый блок PL / SQL дает такой результат:
setting the session identifier to A (Sneezy, Timon):
TEST_FUNCTION called!
function call in WHERE criteria:
TEST_FUNCTION called!
Sneezy Timon
variable use in WHERE criteria:
Sneezy Timon
----
setting the session identifier to B (Dopey Simba):
TEST_FUNCTION called!
function call in WHERE criteria:
TEST_FUNCTION called!
Dopey Simba
variable use in WHERE criteria:
Dopey Simba
Второй блок производит это:
setting the session identifier to A (Sneezy, Timon):
TEST_FUNCTION called!
function call in WHERE criteria:
TEST_FUNCTION called!
Sneezy Timon
variable use in WHERE criteria:
Sneezy Timon
----
setting the session identifier to B (Dopey Simba):
TEST_FUNCTION called!
function call in WHERE criteria:
Sneezy Timon
variable use in WHERE criteria:
Dopey Simba
Как вы можете заметить, TEST_FUNCTION вызывает на один вызов меньше и имеет неправильный результат. Как я понял, кеширование результатов, таблица пользователей должна быть идеальным кандидатом. Много SELECT, очень мало DML. И все работает как надо, если я не поместил свой вызов функции в критерии WHERE. Если я вызываю функцию, сохраняю результат в переменной и использую его в критериях WHERE, все в порядке.
Почему это? Это ошибка или особенность? Является ли тот факт, что функция использует данные из идентификатора сеанса, основной проблемой? Или кеш результатов, как правило, не должен быть включен для всей таблицы?
Edit:
Прочитав некоторые ответы, я попытался явно объявить функцию как кэшированный результат, например:
CREATE OR REPLACE FUNCTION test_function(identifier VARCHAR2 DEFAULT sys_context('userenv', 'client_identifier'))
RETURN test_users.user_id%TYPE result_cache relies_on(test_users) IS
user_id test_users.user_id%TYPE;
BEGIN
SELECT user_id INTO user_id FROM test_users WHERE upper(username) = upper(identifier);
dbms_output.put_line('TEST_FUNCTION called!');
RETURN user_id;
END test_function;
Это очень похоже на пример из документации Oracle в комментарии ниже.
К сожалению, это не помогло. Вызов функции с паратезами или без них не имеет значения для меня (но см. Мой комментарий ниже). Единственный способ найти ожидаемые результаты - отключить кеш результатов для таблицы.