JavaRush /Java Blog /Random-JA /「不必要だが必要な作業」によって生じる SQL パフォーマンスの問題

「不必要だが必要な作業」によって生じる SQL パフォーマンスの問題

Random-JA グループに公開済み
記事を理解するために必要な知識のレベル:データベースと SQL に関する一般的な理解、DBMS に関する実践経験。
SQL パフォーマンスの問題の原因
おそらく、効果的な SQL クエリを作成するために学ぶことができる最も重要なことは、インデックス作成です。しかし、そのすぐ後ろの 2 位は、多くの SQL クライアントがデータベースに多くの「不必要だが必要な作業」を要求するという知識です。私の後に繰り返して:
不必要だが必要な作業
「不要不急の業務」とは何でしょうか?キャプテン・オブビアスはこう語っています。

不要

クライアント アプリケーションに次のデータが必要だとします。
SQL パフォーマンスの問題の原因
何も珍しいことはありません。私たちは映画データベース ( Sakila データベースなど) を使用しており、すべての映画のタイトルと評価をユーザーに表示したいと考えています。次のクエリにより、必要な結果が得られます。
SELECT title, rating
FROM film
ただし、アプリケーション (または ORM) は代わりに次のクエリを実行します。
SELECT *
FROM film
その結果、何が得られるでしょうか? 推測。私たちは役に立たない情報をたくさん受け取ります。
SQL パフォーマンスの問題の原因
右側には、いくつかの複雑な JSON が読み込まれているのがわかります。
  • ディスクから
  • キャッシュする
  • 有線で
  • クライアントの思い出に
  • そして最終的には[不要なものとして]捨てられる
はい、私たちはこの情報のほとんどを捨てます。この情報を抽出するために講じられたすべての措置は、完全に無駄であることが判明しました。本当ですか?本当ですか。

必須

そして今 - 最悪の部分。オプティマイザーは多くのことを実行できるようになりましたが、これらのアクションはデータベースにとって必須です。データベースは、クライアント アプリケーションがこのデータの 95% を必要としていないことを知る方法がありません。そして、これは最も単純な例にすぎません。複数のテーブルを接続することを想像してみてください... それで、データベースは高速だと言いますか? おそらくあなたが考えたこともなかったいくつかのことについて説明させてください。もちろん、個々のリクエストの実行時間は実際には何の影響も与えません。わかりました、実行速度は 1.5 倍遅くなりましたが、なんとか乗り越えられるでしょう? 便宜のためですか?時々これは真実です。しかし、利便性のために常にパフォーマンスを犠牲にしていると、これらの小さなことが積み重なり始めます。もはやパフォーマンス (個々のリクエストの実行速度) について話すのではなく、スループット (システムの応答時間) について話すようになります。そうすれば、解決するのがそれほど簡単ではない深刻な問題が始まります。そうなるとスケーラビリティが失われます。実行プラン、この場合は Oracle DBMS を見てみましょう。
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 |   166K|
|   1 |  TABLE ACCESS FULL| FILM |  1000 |   166K|
--------------------------------------------------
と比べて:
--------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes |
--------------------------------------------------
|   0 | SELECT STATEMENT  |      |  1000 | 20000 |
|   1 |  TABLE ACCESS FULL| FILM |  1000 | 20000 |
--------------------------------------------------
SELECT タイトルの代わりに SELECT * クエリを実行すると、評価にはデータベース内の 8 倍のメモリが使用されます。何も予想外なことはありませんよね?こうなることは分かっていました。しかし、このすべてのデータが必要ないリクエストの多くについては、これに同意しています。私たちはデータベースに対して不必要だが必須の作業を作成し、それがどんどん積み重なっていきます。必要なメモリの 8 倍のメモリを使用します (もちろん、乗数は変化します)。一方、他のすべての段階 (ディスク I/O、ネットワーク経由のデータ転送、クライアントによるメモリ消費) では問題はまったく同じですが、ここではそれらをスキップして次の点を見ていきます。

インデックスの使用

今日のほとんどのデータベースは、インデックスをカバーするという概念をすでに認識しています。カバリングインデックス自体は特別なタイプのインデックスではありません。ただし、「偶然」または意図的に、特定のクエリの「特別なインデックス」になる場合があります。次のクエリを考えてみましょう。
SELECT *
FROM actor
WHERE last_name LIKE 'A%'
その実装に関しては、予想外のことは何もありません。これは簡単なリクエストです。インデックスで範囲を表示し、テーブルにアクセスすれば完了です。
-------------------------------------------------------------------
| Id  | Operation                   | Name                | Rows  |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                     |     8 |
|   1 |  TABLE ACCESS BY INDEX ROWID| ACTOR               |     8 |
|*  2 |   INDEX RANGE SCAN          | IDX_ACTOR_LAST_NAME |     8 |
-------------------------------------------------------------------
良い計画ですね。これが本当に必要かというと、そうではありません。
SQL パフォーマンスの問題の原因
明らかに、メモリなどを無駄にしています。次のクエリを代替案として考えてみましょう。
SELECT first_name, last_name
FROM actor
WHERE last_name LIKE 'A%'
彼の計画は次のとおりです。
----------------------------------------------------
| Id  | Operation        | Name            | Rows  |
----------------------------------------------------
|   0 | SELECT STATEMENT |                 |     8 |
|*  1 |  INDEX RANGE SCAN| IDX_ACTOR_NAMES |     8 |
----------------------------------------------------
クエリのすべてのニーズを満たすインデックス (カバー インデックス) の存在のおかげで、テーブルへのアクセスを完全に排除することができました。大切ですか?そしてどうやって!このアプローチにより、一部のクエリを 1 桁高速化できます (または、変更後にインデックスがカバーできなくなった場合は、クエリを 1 桁遅くすることができます)。カバーインデックスは常に使用できるわけではありません。インデックスには料金を支払う必要があり、あまり多くのインデックスを追加しないでください。しかし、この場合、すべてが明らかです。パフォーマンスを評価してみましょう。
SET SERVEROUTPUT ON
DECLARE
  v_ts TIMESTAMP;
  v_repeat CONSTANT NUMBER := 100000;
BEGIN
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Наихудший вариант requestа: перерасход памяти ПЛЮС доступ к таблице
      SELECT *
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 1 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Улучшенный request, но все равно с доступом к таблице
      SELECT /*+INDEX(actor(last_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 2 : ' || (SYSTIMESTAMP - v_ts));
  v_ts := SYSTIMESTAMP;

  FOR i IN 1..v_repeat LOOP
    FOR rec IN (
      -- Оптимальный request: покрывающий индекс
      SELECT /*+INDEX(actor(last_name, first_name))*/
        first_name, last_name
      FROM actor
      WHERE last_name LIKE 'A%'
    ) LOOP
      NULL;
    END LOOP;
  END LOOP;

  dbms_output.put_line('Оператор 3 : ' || (SYSTIMESTAMP - v_ts));
END;
/

結果として、次のことが得られます。


演算子 1: +000000000 00:00:02.479000000

演算子 2: +000000000 00:00:02.261000000

演算子 3: +000000000 00:00:01.857000000

アクター テーブルには 4 つの列しかないため、ステートメント 1 と 2 のパフォーマンスの違いはそれほど大きくありませんが、依然として重大であることに注意してください。また、オプティマイザーにクエリに対して 1 つまたは別の特定のインデックスを選択させるために、Oracle オプティマイザー ヒントを使用したことにも注意してください。オペレーター 3 は、誰もが認めるこのレースの勝者です。パフォーマンスははるかに優れており、非常に単純なクエリについて話しています。繰り返しになりますが、SELECT * を記述すると、データベースに対して最適化できない不必要ではあるが必須の作業が作成されます。彼女は、カバー インデックスを選択しません。これは、選択した LAST_NAME インデックスよりもオーバーヘッドがわずかに高く、特に、たとえば、役に立たない LAST_UPDATE 列を取得するためにテーブルにアクセスする必要があるためです。しかし、SELECT * を深く分析すればするほど、事態はさらに悪いことが判明します。について話しましょう...

SQL変換

オプティマイザーは SQL クエリを変換するため、非常に優れたパフォーマンスを発揮します (これがどのように機能するかについては、チューリッヒでの Voxxed Days での最近の講演で説明しました)。たとえば、非常に強力な「例外 JOIN」変換があります。これらすべてのテーブルを毎回手動で結合することを避けるために作成する必要があった次のヘルパー ビューについて考えてみましょう。
CREATE VIEW v_customer AS
SELECT
  c.first_name, c.last_name,
  a.address, ci.city, co.country
FROM customer c
JOIN address a USING (address_id)
JOIN city ci USING (city_id)
JOIN country co USING (country_id)
このビューは、CUSTOMER 顧客テーブルとその住所の一部についてのさまざまなテーブルの間のすべての「...-to-one」結合を単純に実行します。ありがとう、正規化。このビューで少し作業した後、ビューに慣れて、その基礎となるテーブルのことを忘れてしまったと想像してください。そして、次のクエリを実行します。
SELECT *
FROM v_customer
その結果、非常に印象的な計画が得られました。
----------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost |
----------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |   599 | 47920 |   14 |
|*  1 |  HASH JOIN           |          |   599 | 47920 |   14 |
|   2 |   TABLE ACCESS FULL  | COUNTRY  |   109 |  1526 |    2 |
|*  3 |   HASH JOIN          |          |   599 | 39534 |   11 |
|   4 |    TABLE ACCESS FULL | CITY     |   600 | 10800 |    3 |
|*  5 |    HASH JOIN         |          |   599 | 28752 |    8 |
|   6 |     TABLE ACCESS FULL| CUSTOMER |   599 | 11381 |    4 |
|   7 |     TABLE ACCESS FULL| ADDRESS  |   603 | 17487 |    3 |
----------------------------------------------------------------
もちろんです。データベースはこれらすべての結合とテーブル全体のスキャンを実行しています。これは、すべてのデータを取得するように指示したためです。ここで、もう一度、本当に必要なのはこれだけだったと想像してください。
SQL パフォーマンスの問題の原因
マジで、そうですよね?今、あなたは私が何を言っているのか理解し始めています。しかし、過去の間違いから何かを学び、次のより最適なクエリを実行したと想像してください。
SELECT first_name, last_name
FROM v_customer
では、何が起こったのか確認してみましょう!
------------------------------------------------------------------
| Id  | Operation          | Name        | Rows  | Bytes | Cost  |
------------------------------------------------------------------
|   0 | SELECT STATEMENT   |             |   599 | 16173 |     4 |
|   1 |  NESTED LOOPS      |             |   599 | 16173 |     4 |
|   2 |   TABLE ACCESS FULL| CUSTOMER    |   599 | 11381 |     4 |
|*  3 |   INDEX UNIQUE SCAN| SYS_C007120 |     1 |     8 |     0 |
------------------------------------------------------------------
実行に関しては、より良い方向への劇的な変化。結合が削除されたのは、オプティマイザーがそれらが役に立たないことを認識できるためです。また、それが認識できる場合 (* を選択してこの作業を必須にしていない場合)、単純にそのような作業をすべて実行することはできません。この場合、なぜそうなるのでしょうか?外部キー CUSTOMER.ADDRESS_ID と主キー ADDRESS.ADDRESS_ID は、後者の値を 1 つだけ保証します。つまり、JOIN 操作は行数を増減しない「...-to-one」結合になります。 。また、行をまったく選択したり要求したりしないため、行をロードする意味がまったくありません。JOIN を削除しても、クエリの結果にはおそらくまったく影響しません。データベースは常にこれを行っています。次のクエリは、ほぼすべてのデータベースで実行できます。
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT 1 / 0 FROM dual
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (SELECT 1 / 0)
この場合、次のクエリを実行するときのように、算術例外がスローされることが予想されます。
SELECT 1 / 0 FROM dual

起こりました:


ORA-01476: 除数はゼロに等しいです

しかし、そんなことは起こらない。オプティマイザ (またはパーサー) は、EXISTS 述語 (SELECT ..) 内の選択リスト要素がクエリの結果を変更しないことを保証できるため、クエリを実行する必要はありません。このような!

その間...

ORM に関する最も厄介な問題の 1 つは、ORM が SELECT * クエリを非常に簡単に作成できることです。実際、たとえば HQL / JPQL では通常、デフォルトで使用されます。エンティティ全体を取得するので、SELECT 句を完全に省略できますね。例えば:
FROM v_customer
たとえば、Hibernate を使用した開発の専門家であり提唱者である Vlad Mihalcea 氏は、チェックアウト後に変更を保存したくない場合は、ほぼ常に [修飾された] クエリを使用することを推奨しています。ORM は、オブジェクト グラフの永続性の問題の解決を大幅に促進します。注: 永続性。実際にオブジェクト グラフを変更するタスクと変更を保存するタスクは密接に関連しています。しかし、そうするつもりがないのであれば、なぜわざわざエッセンスを抽出する必要があるのでしょうか?[洗練された]リクエストを書いてみませんか? 明確にしておきますが、パフォーマンスの観点から見ると、特定のユースケースに合わせて特別に調整されたクエリを作成することは、他のどのオプションよりも明らかに優れています。データセットは小さく、重要ではないため、気にしないかもしれません。素晴らしい。しかし、最終的にスケーラビリティが必要になった場合、エンティティ グラフの命令的走査ではなくクエリを使用するようにアプリケーションを再設計することは、非常に困難になります。そして、それがなくても何かできるでしょう。

行を数えて何かが存在するかどうかを確認する

リソースの最悪の無駄の 1 つは、データベースに何かがあるかどうかを確認するためだけに COUNT(*) クエリを実行することです。たとえば、特定のユーザーに注文があるかどうかを確認する必要があります。そしてリクエストを実行します。
SELECT count(*)
FROM orders
WHERE user_id = :user_id
初級。COUNT = 0 の場合、注文はありません。それ以外の場合は、はい。おそらく ORDERS.USER_ID 列にインデックスがあるため、パフォーマンスはそれほど悪くはありません。しかし、上記のクエリのパフォーマンスを次のオプションと比較するとどうなるでしょうか。
-- Oracle
SELECT CASE WHEN EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
) THEN 1 ELSE 0 END
FROM dual

-- Более адекватные диалекты SQL, например, PostgreSQL
SELECT EXISTS (
  SELECT *
  FROM orders
  WHERE user_id = :user_id
)
真の存在述語が最初の 文字列を見つけるとすぐに追加の文字列の検索を停止することをロケット科学者が理解する必要はありません。したがって、結果が「注文なし」となった場合でも、速度は同等になります。ただし、結果が「はい、注文があります」の場合は、正確な数量を数える必要がない場合は、はるかに早く答えが得られます。結局のところ、私たちは正確な数字には興味がありません。ただし、データベースにそれを計算するように指示しました (不要な作業) が、データベースは 1 より大きいすべての結果 (必要な作業)を無視していることを知りません。もちろん、同じ結果を得るために JPA ベースのコレクションに対して list.size() を呼び出した場合は、さらに悪いことになります。 これについては以前にブログに書きましたが、両方のオプションの比較テストを実施しました...

結論

この記事では明らかなことが述べられています。データベースに不必要だが必要な作業を強制しないでください。要件を考慮すると、特定の作業を実行する必要がないことがわかっているため、これは不要です。ただし、データベースにそれを行うように指示します。データベースにはこの作業が不要であることを確認する方法がないため、この作業が必要になります。この情報はクライアントのみが利用でき、サーバーは利用できません。したがって、データベースはそれを実行する必要があります。この記事では SELECT * に焦点を当てましたが、これは主に、これが非常に便利なオブジェクトであるためです。しかし、これはデータベースだけに当てはまるわけではありません。これは、クライアントがサーバーに不要だが必要な作業を行うように指示するすべての分散アルゴリズムに当てはまります。平均的なAngularJS アプリケーションには、UI がサービス B へのすべての呼び出しを 1 つの呼び出しにまとめるのではなく、サービス A の結果をループしてサービス B を複数回呼び出す、N+ 1 タスクがいくつありますか? これは非常に一般的な出来事です。解決策は常に同じです。コマンドを実行するエンティティに提供する情報が多いほど、(理論的には) コマンドの実行が速くなります。最適なクエリを作成します。いつも。システム全体がこれに感謝するでしょう。 原著
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION