私は、少し緩慢に動作している既存のOracleデータベース駆動型アプリケーションのクエリ時間を改善しようとしています。アプリケーションは、以下のような大きなクエリをいくつか実行しますが、実行には1時間以上かかる場合があります。次のクエリのDISTINCT
をGROUP BY
句に置き換えると、実行時間が100分から10秒に短縮されました。私の理解では、SELECT DISTINCT
とGROUP BY
はほぼ同じ方法で動作します。なぜ実行時間に大きな差があるのでしょうか?バックエンドでのクエリの実行方法の違いは何ですか? SELECT DISTINCT
がより速く実行される状況はありますか?
注:次のクエリでは、WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
は、結果をフィルター処理できるいくつかの方法の1つにすぎません。この例は、SELECT
に列が含まれていないすべてのテーブルを結合する理由を示すために提供されており、使用可能なデータの約10分の1になります。
DISTINCT
を使用するSQL:
SELECT DISTINCT
ITEMS.ITEM_ID,
ITEMS.ITEM_CODE,
ITEMS.ITEMTYPE,
ITEM_TRANSACTIONS.STATUS,
(SELECT COUNT(PKID)
FROM ITEM_PARENTS
WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
) AS CHILD_COUNT
FROM
ITEMS
INNER JOIN ITEM_TRANSACTIONS
ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID
AND ITEM_TRANSACTIONS.FLAG = 1
LEFT OUTER JOIN ITEM_METADATA
ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
LEFT OUTER JOIN JOB_INVENTORY
ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID
LEFT OUTER JOIN JOB_TASK_INVENTORY
ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
LEFT OUTER JOIN JOB_TASKS
ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID
LEFT OUTER JOIN JOBS
ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
LEFT OUTER JOIN TASK_INVENTORY_STEP
ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID
LEFT OUTER JOIN TASK_STEP_INFORMATION
ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE
TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
ORDER BY
ITEMS.ITEM_CODE
GROUP BY
を使用するSQL:
SELECT
ITEMS.ITEM_ID,
ITEMS.ITEM_CODE,
ITEMS.ITEMTYPE,
ITEM_TRANSACTIONS.STATUS,
(SELECT COUNT(PKID)
FROM ITEM_PARENTS
WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
) AS CHILD_COUNT
FROM
ITEMS
INNER JOIN ITEM_TRANSACTIONS
ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID
AND ITEM_TRANSACTIONS.FLAG = 1
LEFT OUTER JOIN ITEM_METADATA
ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
LEFT OUTER JOIN JOB_INVENTORY
ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID
LEFT OUTER JOIN JOB_TASK_INVENTORY
ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
LEFT OUTER JOIN JOB_TASKS
ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID
LEFT OUTER JOIN JOBS
ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
LEFT OUTER JOIN TASK_INVENTORY_STEP
ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID
LEFT OUTER JOIN TASK_STEP_INFORMATION
ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE
TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
GROUP BY
ITEMS.ITEM_ID,
ITEMS.ITEM_CODE,
ITEMS.ITEMTYPE,
ITEM_TRANSACTIONS.STATUS
ORDER BY
ITEMS.ITEM_CODE
以下は、DISTINCT
を使用したクエリのOracleクエリプランです。
以下は、GROUP BY
を使用したクエリのOracleクエリプランです。
パフォーマンスの違いは、おそらくSELECT
句でのサブクエリの実行によるものです。すべての行に対してこのクエリを再実行していると推測していますbefore the distinct。のために group by
、1回実行されますafter group by。
代わりに、結合に置き換えてみてください:
select . . .,
parentcnt
from . . . left outer join
(SELECT PARENT_ITEM_ID, COUNT(PKID) as parentcnt
FROM ITEM_PARENTS
) p
on items.item_id = p.parent_item_id
GROUP BY
とDISTINCT
の実行プランはほぼ同じです。
ここで推測する必要があるため(説明プランがないため)ここでの違いは、インラインサブクエリが実行されるというIMOです[〜#〜] after [〜#〜] the GROUP BY
しかし[〜#〜] before [〜#〜]DISTINCT
。
したがって、クエリが1M行を返し、1K行に集約される場合:
GROUP BY
クエリはサブクエリを1000回実行しますが、DISTINCT
クエリはサブクエリを1000000回実行します。Tkprofの説明計画は、この仮説を実証するのに役立ちます。
これについて議論している間、クエリの記述方法が読者とオプティマイザの両方にとって誤解を招くことに注意することが重要だと思います。明らかに、TASK_INVENTORY_STEP.STEP_TYPE
の値は「TYPE A」です。
IMOを使用すると、クエリのプランが改善され、次のように記述すれば読みやすくなります。
SELECT ITEMS.ITEM_ID,
ITEMS.ITEM_CODE,
ITEMS.ITEMTYPE,
ITEM_TRANSACTIONS.STATUS,
(SELECT COUNT(PKID)
FROM ITEM_PARENTS
WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID) AS CHILD_COUNT
FROM ITEMS
JOIN ITEM_TRANSACTIONS
ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID
AND ITEM_TRANSACTIONS.FLAG = 1
WHERE EXISTS (SELECT NULL
FROM JOB_INVENTORY
JOIN TASK_INVENTORY_STEP
ON JOB_INVENTORY.JOB_ITEM_ID=TASK_INVENTORY_STEP.JOB_ITEM_ID
WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
AND ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID)
多くの場合、DISTINCTは、クエリが適切に記述されていないことを示す兆候です(適切なクエリでは重複が返されないため)。
また、元の選択では4つのテーブルが使用されないことに注意してください。
最初に注意すべきことは、Distinct
の使用がコードの匂い、別名アンチパターンを示すことです。一般的に、重複データを生成している欠落した結合または余分な結合があることを意味します。上記のクエリを見ると、group by
は(クエリを見ることなく)高速です。つまり、group by
は、返されるレコードの数を減らします。一方、distinct
は結果セットを吹き飛ばし、行ごとの比較を行っています。
アプローチへの更新
申し訳ありませんが、もっと明確にすべきでした。ユーザーがシステムで特定のタスクを実行するとレコードが生成されるため、スケジュールはありません。ユーザーは、1日または1時間に数百の単一のレコードを生成できます。重要なことは、ユーザーが検索を実行するたびに、最新のレコードを返す必要があることです。これにより、特にデータを取り込むクエリの実行に時間がかかる場合、ここでマテリアライズドビューが機能するかどうか疑問に思います。
これが具体化されたビューを使用する正確な理由だと思います。したがって、プロセスはこのように機能します。ユーザーがシステムで任意のタスクを実行した後は「新しい」データのみを気にするため、マテリアライズドビューを構築する部分として長時間実行されるクエリを使用します。したいのは、この基本マテリアライズドビューに対してクエリを実行することです。これは、バックエンドで絶えず更新できます。関係する永続化戦略は、マテリアライズドビューを消さないようにする必要があります(一度に数百のレコードを保持しても何もクラッシュしません)。これにより、Oracleが読み取りロックを取得できるようになります(データを読み取るソースの数は気にせず、ライターのみに注意します)。最悪の場合、ユーザーはマイクロ秒の「古い」データを保持するため、これがウォール街の金融取引システムまたは原子炉のシステムでない限り、これらの「ブリップ」は最もイーグルアイのユーザーでさえ気付かれないはずです。
これを行う方法のコード例:
create materialized view dept_mv FOR UPDATE as select * from dept;
ここで重要なのは、更新を呼び出さない限り、保持されているデータが失われないことです。マテリアライズドビューを再び「ベースライン」にするタイミングを決定するのはあなた次第です(おそらく深夜ですか?)