web-dev-qa-db-ja.com

OracleはWHERE句から遅い関数呼び出しを抽出します

特定の日付からの分数として日付が格納されている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
);

「MINUTE」と呼ばれるものとそれらが表す実際の日付/時刻との間で変換する2つの関数を作成しました。彼らはこのように見えます:

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;

現在、特定の時間枠と重複するすべての行を返すクエリを作成しようとしています。おそらく関数呼び出しが原因で、実行にかなり長い時間がかかります。私はそれのいくつかの異なるバージョンを試しました:

-- 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;

最初の2つのクエリでの比較では、適切な比較を行うためにすべての行で関数呼び出しが必要であることは問題だと確信しています。直接比較なので、最終的なクエリが非常に高速になるのはこのためです。

上記の2つのパーティと同じ速さで実行される単一のクエリが欲しいです。クエリで関数呼び出しを1回だけ実行し、各行に対して呼び出す必要なく、後続のすべての比較で戻り値を使用するように強制する方法はありますか?次のようなことを試しましたが、期待したように機能しないようです。

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;
5
Venture Free

マークを関数に deterministic と指定すると、クエリの2番目の形式が機能します。入力値、それは常に同じ結果を返します。
そのセットを使用すると、Oracleは、すべての行ではなく、where句の各パラメーターに対して1回だけ変換を実行します。

これとともに:

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;
/

大量のダミー行(増加する整数)で満たされたテーブルで、次のタイミングが一貫して得られます。

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

対確定的注釈付き関数:

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

値を直接プラグインすることで、自分が持っているものに非常に近づく必要があり、それらの列のインデックスは、リテラルをプラグインしたかのように使用できます。

(正確に一致するものに関数ベースのインデックスがない限り、start|stop_minute列に変換関数を配置することは、一般的にはお勧めできません。)

4
Mat