web-dev-qa-db-ja.com

SQLパフォーマンス:SELECT DISTINCTとGROUP BY

私は、少し緩慢に動作している既存のOracleデータベース駆動型アプリケーションのクエリ時間を改善しようとしています。アプリケーションは、以下のような大きなクエリをいくつか実行しますが、実行には1時間以上かかる場合があります。次のクエリのDISTINCTGROUP BY句に置き換えると、実行時間が100分から10秒に短縮されました。私の理解では、SELECT DISTINCTGROUP 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クエリプランです。

Oracle query plan for query using DISTINCT

以下は、GROUP BYを使用したクエリのOracleクエリプランです。

Oracle query plan for query using GROUP BY

23
woemler

パフォーマンスの違いは、おそらく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
18
Gordon Linoff

GROUP BYDISTINCTの実行プランはほぼ同じです。

ここで推測する必要があるため(説明プランがないため)ここでの違いは、インラインサブクエリが実行されるという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つのテーブルが使用されないことに注意してください。

16
Vincent Malgrat

最初に注意すべきことは、Distinctの使用がコードの匂い、別名アンチパターンを示すことです。一般的に、重複データを生成している欠落した結合または余分な結合があることを意味します。上記のクエリを見ると、group byは(クエリを見ることなく)高速です。つまり、group byは、返されるレコードの数を減らします。一方、distinctは結果セットを吹き飛ばし、行ごとの比較を行っています。

アプローチへの更新

申し訳ありませんが、もっと明確にすべきでした。ユーザーがシステムで特定のタスクを実行するとレコードが生成されるため、スケジュールはありません。ユーザーは、1日または1時間に数百の単一のレコードを生成できます。重要なことは、ユーザーが検索を実行するたびに、最新のレコードを返す必要があることです。これにより、特にデータを取り込むクエリの実行に時間がかかる場合、ここでマテリアライズドビューが機能するかどうか疑問に思います。

これが具体化されたビューを使用する正確な理由だと思います。したがって、プロセスはこのように機能します。ユーザーがシステムで任意のタスクを実行した後は「新しい」データのみを気にするため、マテリアライズドビューを構築する部分として長時間実行されるクエリを使用します。したいのは、この基本マテリアライズドビューに対してクエリを実行することです。これは、バックエンドで絶えず更新できます。関係する永続化戦略は、マテリアライズドビューを消さないようにする必要があります(一度に数百のレコードを保持しても何もクラッシュしません)。これにより、Oracleが読み取りロックを取得できるようになります(データを読み取るソースの数は気にせず、ライターのみに注意します)。最悪の場合、ユーザーはマイクロ秒の「古い」データを保持するため、これがウォール街の金融取引システムまたは原子炉のシステムでない限り、これらの「ブリップ」は最もイーグルアイのユーザーでさえ気付かれないはずです。

これを行う方法のコード例:

create materialized view dept_mv FOR UPDATE as select * from dept; 

ここで重要なのは、更新を呼び出さない限り、保持されているデータが失われないことです。マテリアライズドビューを再び「ベースライン」にするタイミングを決定するのはあなた次第です(おそらく深夜ですか?)

8
Woot4Moo