Oracle 从 WHERE 子句中提取慢速函数调用

Ven*_*ree 5 oracle functions

我有一个 oracle 表,其中日期存储为自特定日期以来的分钟数。它看起来像这样:

CREATE TABLE MY_TABLE
(
  SOMETHING     NUMBER(15,1) NOT NULL,
  START_MINUTE  NUMBER(15,1) NOT NULL,
  STOP_MINUTE   NUMBER(15,1) NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

我编写了两个函数来在所谓的“分钟”和它们代表的实际日期/时间之间进行转换。它们看起来像这样:

CREATE OR REPLACE FUNCTION FROM_MINUTE (MINUTE_IN IN NUMBER)
RETURN DATE AS
BEGIN
  /* Minute 0 = 12/30/1899 12:00am */
  RETURN (TO_DATE('1899-12-30', 'YYYY-MM-DD') + (MINUTE_IN / 1440));
END FROM_MINUTE;

CREATE OR REPLACE FUNCTION TO_MINUTE (DATE_IN IN DATE)
RETURN NUMBER AS
BEGIN
  /* Minute 0 = 12/30/1899 12:00am */
  RETURN
    (TRUNC(DATE_IN, 'DD') - TO_DATE('12/30/1899', 'MM/DD/YYYY')) * 1440 +
    TO_NUMBER(TO_CHAR(DATE_IN, 'HH24'))  * 60 +
    TO_NUMBER(TO_CHAR(DATE_IN, 'MI'));
END TO_MINUTE;
Run Code Online (Sandbox Code Playgroud)

现在我正在尝试编写一个查询,该查询返回与特定时间范围重叠的所有行。运行需要很长时间,大概是因为函数调用。我已经尝试了它的几个不同版本:

-- This compares dates by converting the minutes to dates first
SELECT * FROM MY_TABLE
WHERE FROM_MINUTE(START_MINUTE) < TO_DATE('2013-01-31', 'YYYY-MM-DD')
AND FROM_MINUTE(STOP_MINUTE) > TO_DATE('2013-01-01', 'YYYY-MM-DD');

-- This compares minutes by converting the dates to minutes first
SELECT * FROM MY_TABLE
WHERE START_MINUTE < TO_MINUTE(TO_DATE('2013-01-31', 'YYYY-MM-DD'))
AND STOP_MINUTE > TO_MINUTE(TO_DATE('2013-01-01', 'YYYY-MM-DD'));

-- Finally I just got the minute values in a separate query...
SELECT 
  TO_MINUTE(TO_DATE('2013-01-01', 'YYYY-MM-DD')) AS EARLIEST, -- Returns 59436000
  TO_MINUTE(TO_DATE('2013-01-31', 'YYYY-MM-DD')) AS LATEST    -- Returns 59479200
FROM DUAL;
-- and I plugged them directly into the query
SELECT * FROM MY_TABLE
WHERE START_MINUTE < 59479200
AND STOP_MINUTE > 59436000;
Run Code Online (Sandbox Code Playgroud)

我很确定问题在于前两个查询中的比较需要对每一行进行函数调用才能进行适当的比较。这就是为什么最终查询要快得多的原因,因为它只是一个直接比较。

我希望有一个与上面两个部分一样快的查询。有没有办法强制查询只运行一次函数调用,并在每次后续比较中使用返回值,而不必为每一行调用?我尝试了以下类似的方法,但似乎并没有像我希望的那样工作:

SELECT * FROM MY_TABLE,
  (SELECT 
    TO_MINUTE(TO_DATE('2013-01-01', 'YYYY-MM-DD')) AS EARLIEST, -- Returns 59436000
    TO_MINUTE(TO_DATE('2013-01-31', 'YYYY-MM-DD')) AS LATEST    -- Returns 59479200
  FROM DUAL) MY_MINUTES
WHERE START_MINUTE < MY_MINUES.LATEST
AND STOP_MINUTE > MY_MINUTES.EARLIEST;
Run Code Online (Sandbox Code Playgroud)

Mat*_*Mat 4

如果您将函数标记为确定性,则查询的第二种形式将起作用,这意味着对于给定的一组输入值,它将始终返回相同的结果。
通过该设置,Oracle 将仅对子句中的每个参数运行一次转换,where而不是对每一行运行转换。

有了这个:

CREATE OR REPLACE FUNCTION TO_MINUTE_D (DATE_IN IN DATE)
RETURN NUMBER DETERMINISTIC AS
BEGIN
  /* Minute 0 = 12/30/1899 12:00am */
  RETURN
    (TRUNC(DATE_IN, 'DD') - TO_DATE('12/30/1899', 'MM/DD/YYYY')) * 1440 +
    TO_NUMBER(TO_CHAR(DATE_IN, 'HH24'))  * 60 +
    TO_NUMBER(TO_CHAR(DATE_IN, 'MI'));
END TO_MINUTE_D;
/
Run Code Online (Sandbox Code Playgroud)

在充满大量虚拟行(不断增加的整数)的表上,我一致得到以下计时:

SQL> SELECT * FROM MY_TABLE
WHERE START_MINUTE < TO_MINUTE(TO_DATE('2013-01-31', 'YYYY-MM-DD'))
AND STOP_MINUTE > TO_MINUTE(TO_DATE('2013-01-01', 'YYYY-MM-DD'));

no rows selected

Elapsed: 00:00:12.69
Run Code Online (Sandbox Code Playgroud)

与确定性注释函数相比:

SQL> SELECT * FROM MY_TABLE
     WHERE START_MINUTE < TO_MINUTE_D(TO_DATE('2013-01-31', 'YYYY-MM-DD'))
     AND STOP_MINUTE > TO_MINUTE_D(TO_DATE('2013-01-01', 'YYYY-MM-DD')); 

no rows selected

Elapsed: 00:00:00.07
Run Code Online (Sandbox Code Playgroud)

您应该非常接近直接插入值所拥有的内容,并且可以像插入文字一样使用这些列上的索引。

(正如您所发现的,将转换函数放在start|stop_minute列上通常不是一个好主意,除非您在完全匹配的索引上有基于函数的索引。)