すぐに実践できる! SQLパフォーマンスチューニング5選

こんにちは。
技術リーダーの因藤と、技術スペシャリストの深町です。
このたび、2人でタッグを組んで記事執筆を行うことになりました。
3年もの長い付き合いの2人です!

この3年はずっと同じチームで働いており、いくつもの案件(ECサイト)を一緒に取り組んできました。
このチームの扱うサイトは「大規模」のものが多く、イベント時の高アクセスへの対応や、大量のファイル連携など、常にサイトの「パフォーマンス」を意識する必要がありました。

「高アクセスでDBサーバーのCPU使用率が100%に・・」
「CSV取込に5hかかっている・・」
ECモールを立ち上げた際は、こんな経験をたくさんしました・・笑

これらの経験から、サイトのパフォーマンス(主にSQLについて)を落とさないための工夫を記事にまとめました。
数ある中から、効果が高く、実践で簡単につかえる5つピックアップしましたので、ぜひ一読いただければ幸いです。

1.SELECT * を利用しない

まずはシンプルなところから。

SELECT * って便利ですよね。
新たにテーブルへカラムを追加しても、*を記載しておけば、勝手に取得できます。
しかし、テーブルのカラムが増えていくにつれ、パフォーマンスが著しく低下します。
テーブルの項目数が多い場合、それだけで検索する際のコストがかかります。
※w2では247項目もの項目が存在するViewも存在

必要な項目が限られるのであれば、必要な分にしぼりこむことで、
無駄なデータ検索が少なくなり、パフォーマンスが向上します。

また、よりパフォーマンス向上を図るには、
インデックスのカラムだけをSELECTすると最速で検索できます。 ※インデックスのカラムだけ利用したい場合のみ

理由としては、SELECT句にインデックス以外のカラムが指定された場合、
Index Seekによる二分岐探索が行われた後、足りないデータを探しにキー参照が行われます。
そのため、インデックスのカラムのみに絞ると、「足りないデータ検索」の分を短縮できます。

▼参考記事

2.WHERE句で関数や計算を利用しない

WHERE句で、関数を利用したり、計算式を記載するような場面があるかと思います。
例えば、税抜金額のカラムを税込に計算して値の比較をしたい場合などです。

通常クエリ実行時は、DBがテーブルのデータ分布(統計情報)をある程度理解できていると、効率よく検索をしてくれますが、
WHERE句でインデックスが貼られているカラムに関数や計算式を利用すると、関数を利用しているカラムに対してオプティマイザがインデックスを判断できない(統計情報が利用できない)ため、全レコードに対して計算を実施してしまいます。(↓参照

クエリ① インデックス利用されない

SELECT  *
  FROM  user
 WHERE  UPPER(name) = 'TARO'

クエリ② インデックス利用される

SELECT  *
  FROM  user
 WHERE  name = 'TARO'

上記の場合、クエリ①はnameにインデックスが貼ってありますが、インデックスは利用されません。
オプティマイザから見ると、クエリ①と②は全くの別物になります。
データベースにname:taroと保存されていた場合は、インデックス領域に保持されているのはtaro(小文字)であって
TAROではない(別物)のためインデックスが効かずクエリが低速になります。

なお、SELECT句で関数や計算式を利用するのは問題ありません。
そのため、下記例のように記載するようにしてみてください。

例)税抜金額のカラムを税込に計算して値の比較をしたい場合

× アンチパターン

 SELECT id, date, status FROM Order
   WHERE price * 1.1 >= @hoge

〇 推奨パターン

SELECT  Order.id, Order.date, Order.status
 FROM  (
         SELECT date, status, (price * 1.1) As tax_include_price
           FROM Order
       )As Order
 WHERE  tax_include_price >= @hoge

▼参考記事

3.テーブル結合時のひと工夫

クエリを記載するにあたり、1テーブルで完結することは少ないと思います。
複数テーブルをJOIN(結合)して、より多くの情報を取得する際、
下記の方法を参考に、JOINの仕方を工夫してみてください。

① JOINする前に条件を絞る

テーブルを結合してからWHERE句を記載すると、
結合後の大きなテーブルに対して、条件絞り込みを行うため、検索コストが高くなってしまいます。
そのため、JOINする前に条件絞り込みを行った状態にしてから、テーブル結合をするようにしましょう。

なお、最近のDBは優秀なので、オプティマイザがクエリを解析し最適化するため、そこまで処理に差がなくなることが多くなってきました。
ただし、クエリの内容によってオプティマイザが必ず最適な絞り込みをするとは限らないため、
パフォーマンスを重視した書き方(事前に絞り込む)をしておいた方が最適な絞り込みをする可能性が高くなります。
※実際に処理時間に差が出る場合は適用すると良い

WHERE時に条件指定

with worktable As (
      select  Order.order_id,
              Order.order_status
        from  Order
)
select  worktable.order_status,
        OrderItem.*
  from  OrderItem
        inner join worktable on OrderItem.order_id = worktable.order_id
 where  OrderItem.item_price >= 10000

JOIN時に指定

with worktable As (
      select  Order.order_id,
              Order.order_status
        from  Order
)
select  worktable.order_status,
        OrderItem.*
  from  OrderItem
        inner join worktable on (
                                  OrderItem.order_id = worktable.order_id
                                  and
                                  OrderItem.item_price >= 10000
                                )

② JOINを行う場合は、INDEXがはられているカラムで結合する

JOINを行う際に指定するON句で記載するカラムは、必ずインデックス定義したカラムで結合をしましょう。
JOIN部分で重さを実感した場合は、たいていこれが影響しています。
また複合インデックスの場合、条件に指定する項目をインデックスに定義したカラムと同一の順番でで指定しないと、インデックスが効かないといった問題が起こります。
インデックスを効かせるためには、インデックスで定義された順番と同じ順序で条件式を指定するようにしましょう。

▼参考記事

4.バルクインサートを利用してスピードアップ

外部システムとの連携において、CSVなどのファイルを用いて情報をやり取りすることがあるかと思います。
ECサイトでは、商品情報(ささげ)を外部システムから連携して、ECサイト側に取り込むことがよくあります。

この際の方法として、アプリ側でCSVデータをメモリに展開して、1レコードずつ処理を行う方法と、CSVファイルを仮テーブルにバルクインサートを行って、SQLで加工や登録・更新を行ってしまう方法とがあります。

データ量が少ない場合であれば、前者の対応方針でも問題なく実行できますが、
データ量が数万レコードあるようなファイル連携であれば、確実にバルクインサートを行ったほうがよいです。

以下、過去実際に扱った案件で検証した結果を記載します。
結果として100倍近い差がありました。

アプリ側で1件ずつ処理

CSVのレコードをメモリに展開し、ループを回しながら
CSVに更新対象となるキー情報がある場合更新、ない場合は登録
の処理を1行ずつ回す処理をしていた

対象:16,000~17,000程度
実行時間:5時間程度

バルクインサートで一括処理

連携されるCSVファイルの形式でバルクインサートを仮テーブルにINSERTし、
データの加工を実施して、対象テーブルに登録/更新を実施

対象件数:16,000~17,000程度
実行時間169秒(2分49秒)

100倍速くなった

5.条件分岐はクエリで行わずにアプリ側で

クエリを実行するうえで、実行計画を最適に利用できるかがパフォーマンスに大きく影響します。
実行計画とは、SQL側が、ユーザーが発行したクエリを解析し、決定した実データに対して、どのようにアクセス・計算を行うかが記載されている計画書です。
SQL側で「どのように」そのデータを探して取得するかをオプティマイザ機構が決定します。

どのSQLにおいても、1クエリに対して、1つの実行計画がキャッシュされるため、例えば、「可変パラメータが空」「可変パラメータが空ではない」という条件(例に記載されているアンチパターンを参照)の場合、
この2条件で検索結果に非常に大きな差異がある場合、「どちらかの条件で作成された実行計画」で実行されてしまうため、最適な実行計画で実行できなくなる可能性が高くなります。

この回避方法として、アプリ側でクエリを作成時に、可変パラメータによる条件指定を単純なture/falseの条件に文字列置換してしまい、クエリ実行時には可変パラメータによる条件を利用しない方法があります。(例に記載されている推奨パターンを参照)
この方法を使うと、「可変パラメータが空」と「可変パラメータが空ではない」という2つの別のクエリとして実行計画が作成され、実行計画を最適に利用することができます。

× アンチパターン

  WHERE  @hoge <> ''
        AND   ・・・

〇 推奨パターン

〇 推奨パターン
  WHERE
    <可変パラメータが空ではない場合表示>
      1 = 1
    </可変パラメータが空ではない場合表示>
    <可変パラメータが空の場合表示>
      1 = 0
    </可変パラメータが空の場合表示>
        AND   ・・・

※@hogeはクエリパラメータ
※置換する囲みタグはアプリ側でルール定義してください。

アプリ側の実装について

一般的なアプリでは、一定のルール(例:<@@value:[(可変)パラメータの値]@@>)タグを定義して、(可変)パラメータの値を見て、値がないのであればタグに囲われた部分に記載されているクエリを置換する等して、
実行するクエリを組み立ててあげることで無駄な条件分岐を実行対象から省くことが可能です。

最後に

今回の5つ以外にもまだまだたくさんのチューニング方法はあります。
パフォーマンスの低下はSEOの順位低下にも繋がり、ECサイトを運営している事業者にとっては、売上の低下にも繋がりかねない重要な要素になります。

普段からパフォーマンスを意識した開発を行うようにし、現状遅いと感じている個所があるのであれば、ぜひ本記事を参考にしていただければと思います。

以上です。これを読んだ人のお役に立てたら幸いです。

関連記事

プロジェクトストーリー

おすすめ記事

技術

コメント

この記事へのコメントはありません。

カテゴリー

TOP
TOP