7

I have a query involving two tables: table A has lots of rows, and contains a field called b_id, which references a record from table B, which has about 30 different rows. Table A has an index on b_id, and table B has an index on the column name.

My query looks something like this:

SELECT COUNT(A.id) FROM A INNER JOIN B ON B.id = A.b_id WHERE (B.name != 'dummy') AND <condition>;

With condition being some random condition on table A (I have lots of those, all exhibiting the same behavior).

This query is extremely slow (taking north of 2 seconds), and using explain, shows that query optimizer starts with table B, coming up with about 29 rows, and then scans table A. Doing a STRAIGHT_JOIN, turned the order around and the query ran instantaneously.

I'm not a fan of black magic, so I decided to try something else: come up with the id for the record in B that has the name dummy, let's say 23, and then simplify the query to:

SELECT COUNT(A.id) FROM A WHERE (b_id != 23) AND <condition>;

To my surprise, this query was actually slower than the straight join, taking north of a second.

Any ideas on why the join would be faster than the simplified query?

UPDATE: following a request in the comments, the outputs from explain:

Straight join:

+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+
| id | select_type | table | type   | possible_keys   | key     | key_len | ref           | rows   | Extra       |
+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+
|  1 | SIMPLE      | A     | ALL    | b_id            | NULL    | NULL    | NULL          | 200707 | Using where |
|  1 | SIMPLE      | B     | eq_ref | PRIMARY,id_name | PRIMARY | 4       | schema.A.b_id |     1  | Using where |
+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+

No join:

+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | A     | ALL  | b_id          | NULL | NULL    | NULL | 200707 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

UPDATE 2: Tried another variant:

SELECT COUNT(A.id) FROM A WHERE b_id IN (<all the ids except for 23>) AND <condition>;

This runs faster than the no join, but still slower than the join, so it seems that the inequality operation is responsible for part of the performance hit, but not all.

4

5 に答える 5

4

MySQL 5.6 以降を使用している場合は、クエリ オプティマイザに何をしているのかを尋ねることができます。

SET optimizer_trace="enabled=on";

## YOUR QUERY 
SELECT COUNT(*) FROM transactions WHERE (id < 9000) and user != 11;
##END YOUR QUERY

SELECT trace FROM information_schema.optimizer_trace;

SET optimizer_trace="enabled=off";

ほとんどの場合、MySQL リファレンスの次のセクションを参照する必要があります。 オプティマイザーのトレースとオプティマイザー


最初の説明を見ると、おそらくオプティマイザーがテーブルBを使用して結合に基づいて必要な行にフィルターをかけ、外部キーを使用してテーブル内の行を取得できるため、クエリが高速であるように見えますA

説明では、興味深いのはこのビットです。一致する行は 1 つだけで、 を使用していschema.A.b_idます。事実上、これは行を事前にフィルタリングしており、Aそこからパフォーマンスの違いが生じると思います。

   | ref           | rows   | Extra       |
   | schema.A.b_id |     1  | Using where |

そのため、クエリでよくあることですが、すべてインデックスにかかっています。より正確には、インデックスがありません。個々のフィールドにインデックスがあるからといって、それらが実行中のクエリに適しているとは限りません。

基本的なルール: Using IndexEXPLAINと表示されていない場合は、適切なインデックスを追加する必要があります。

Explain の出力を見ると、最初に興味深いのは、皮肉なことに各行の最後の部分です。すなわちExtra

最初の例では

|  1 | SIMPLE      | A     | .... Using where |
|  1 | SIMPLE      | B     | ...  Using where |

これらの両方を使用するのはよくありません。理想的には少なくとも 1 つ、できれば両方がUsing index

あなたがするとき

SELECT COUNT(A.id) FROM A WHERE (b_id != 23) AND <condition>;

テーブルスキャンを実行しているため、インデックスを追加する必要がある場所の使用を参照してください。

たとえば、あなたがした場合

EXPLAIN SELECT COUNT(A.id) FROM A WHERE (Id > 23)

Using where;が表示されるはずです。インデックスの使用(ここでは Id が主キーであり、インデックスがあると仮定します)

最後に条件を追加した場合

EXPLAIN SELECT COUNT(A.id) FROM A WHERE (Id > 23) and Field > 0

次に、2 つのフィールドのインデックスを追加する必要があるwhere の使用を参照してください。フィールドにインデックスがあるだけでは、MySQL が複数のフィールドにまたがるクエリ中にそのインデックスを使用できるわけではありません。これは、クエリ オプティマイザが内部的に決定するものです。内部規則については正確にはわかりません。ただし、通常、クエリに一致するようにインデックスを追加すると、非常に役立ちます。

したがって、インデックスを追加します (上記のクエリの 2 つのフィールドに):

ALTER TABLE `A` ADD INDEX `IndexIdField` (`Id`,`Field`)

これらの 2 つのフィールドに基づいてクエリを実行するときにインデックスが存在するように変更する必要があります。

テーブルTransactionsを持つデータベースの1つでこれを試しました。User

このクエリを使用します

EXPLAIN SELECT COUNT(*) FROM transactions WHERE (id < 9000) and user != 11;

2 つのフィールドでインデックスなしで実行:

PRIMARY,user    PRIMARY 4   NULL    14334   Using where

次に、インデックスを追加します。

ALTER TABLE `transactions` ADD INDEX `IndexIdUser` (`id`, `user`);

次に、同じクエリをもう一度、今度は

PRIMARY,user,Index 4    Index 4 4   NULL    12628   Using where; Using index

今回はインデックスを使用しているため、結果としてはるかに高速になります。


@Wrikken のコメントから - また、私は正確なスキーマ/データを持っていないため、この調査の一部ではスキーマに関する仮定が必要であることに注意してください (これは間違っている可能性があります)。

SELECT COUNT(A.id) FROM A FORCE INDEX (b_id)

would perform at least as good as 

SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id.

OP の最初の EXPLAIN を見ると、クエリに 2 つの要素があることがわかります。*eq_ref*のEXPLAINドキュメントを参照すると、この関係に基づいて考慮すべき行が定義されることがわかります。

Explain 出力の順序は、必ずしも一方を実行してから他方を実行することを意味するわけではありません。それは単に、クエリを実行するために選択されたものです (少なくとも私が知る限り)。

何らかの理由で、クエリ オプティマイザーがインデックスを使用しないことを決定b_idしました。ここでは、クエリのために、オプティマイザーがテーブル スキャンを実行する方が効率的であると判断したと想定しています。

2 番目の説明は、 のインデックスを考慮していないため、少し気になりb_idます。おそらく が原因ですAND <condition>(これは省略されているので、それが何であるかを推測しています)。インデックスを使用してこれを試すと、インデックスb_idが使用されます。ただし、条件が追加されるとすぐに、インデックスは使用されません。

だから、するとき

  SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id.

これはすべて、上の PRIMARY インデックスが速度差の原因であることを示しています。このテーブルには外部キーがあると説明されているBため、私はそれを想定しています。schema.A.b_idこれは、インデックスよりも適切な関連行のコレクションである必要がありますb_id-したがって、クエリオプティマイザーはこの関係を使用して、選択する行を定義できます-また、プライマリインデックスはセカンダリインデックスよりも優れているため、そこから行を選択する方がはるかに高速ですB を作成し、関係リンクを使用して A の行と照合します。

于 2013-10-29T08:21:54.430 に答える
2

ここでは奇妙な動作は見られません。必要なのは、MySQL がインデックスを使用する方法の基本を理解することです。私が通常お勧めする記事は次のとおりです。MySQL がインデックスを使用する 3 つの方法

のようなものを書いている人々を観察するのはいつも面白いです。なぜならWHERE (B.name != 'dummy') AND <condition>、これAND <condition>が MySQL オプティマイザが特定のインデックスを選択した理由かもしれません。WHERE b_id != 23 AND <condition>パフォーマンスを向上させるには、さまざまなインデックスが必要です。

理解しておくべきことの 1 つは、MySQL は等価比較を好み、範囲条件と不等価比較を好まないということです。通常は、範囲条件を使用したり値を指定したりするよりも、正しい値を指定する方が適切です!=

それでは、2 つのクエリを比較してみましょう。

ストレートジョインあり

A.id 順序 (主キーであり、クラスター化されている、つまり、データはディスク上にその順序で格納されている) の各行について、ディスクから行のデータを取得して、<condition>b_id が満たされているかどうかを確認し、次に (繰り返します)一致する行ごとに) b_id の適切な行を見つけ、ディスクに移動し、b.name を取得し、それを「ダミー」と比較します。この計画はまったく効率的ではありませんが、A テーブルには 200000 行しかないため、かなり効率的です。

ストレートジョイントなし

テーブル B の各行について、名前が一致するかどうかを比較し、A.b_id インデックス (これはインデックスであるため、明らかに b_id でソートされているため、ランダムな順序で A.id が含まれています) を調べ、各 A.id について指定された A.b_id は、ディスク上の対応する A 行を見つけて をチェックし<condition>、一致する場合は id をカウントし、一致しない場合は行を破棄します。

ご覧のとおり、2 番目のクエリに非常に長い時間がかかるという事実に奇妙な点はありません。基本的に、MySQL に A テーブルのほぼすべての行にランダムにアクセスさせます。最初のクエリでは、A テーブルを格納されている順序で読み取ります。ディスク。

結合のないクエリは、インデックスをまったく使用しません。実際には、ストレート ジョインを使用したクエリとほぼ同じ時間がかかるはずです。私の推測では、b_id!=23andの順序<condition>が重要です。

UPD1: 結合なしでクエリのパフォーマンスを次のものと比較できますか:

SELECT COUNT(A.id)
FROM A
WHERE IF(b_id!=23, <condition>, 0);

UPD2: EXPLAIN にインデックスが表示されないという事実は、インデックスがまったく使用されていないという意味ではありません。インデックスは、少なくとも読み取り順序を定義するために使用されます。他に有用なインデックスがない場合、通常は主キーですが、上で述べたように、同等条件と対応するインデックスがある場合、MySQL はそのインデックスを使用します。 . したがって、基本的に、どのインデックスが使用されているかを理解するには、行が出力される順序を見ることができます。順序が主キーと同じ場合は、インデックスが使用されていない (つまり、主キー インデックスが使用されている) 場合、行の順序がシャッフルされている場合は、他のインデックスが含まれていた場合よりも低くなります。

あなたの場合、2番目の条件はほとんどの行に当てはまりますが、インデックスはまだ使用されています.b_idを取得するMySQLはランダムな順序でディスクに保存されるため、低速です。ここにはブラック マジックはなく、この 2 番目の条件はパフォーマンスに影響します。

于 2013-11-01T15:05:06.493 に答える
0

おそらく、これは回答ではなくコメントである必要がありますが、少し長くなります。

まず第一に、(ほぼ) まったく同じ Explain を持つ 2 つのクエリが異なる速度で実行されるとは信じがたいことです。さらに、説明に余分な行がある方が高速に実行される場合、これは可能性が低くなります。そして、ここではより速いという言葉が鍵だと思います。

速度 (クエリが完了するまでにかかる時間) を比較しましたが、これは非常に経験的なテスト方法です。たとえば、キャッシュを不適切に無効にした可能性があります。これにより、その比較が役に立たなくなります。言うまでもなく<insert your preferred software application here>、テストの実行時にページ フォールトやその他の操作を行った可能性があり、クエリ速度が低下する可能性があります。

クエリのパフォーマンスを測定する正しい方法は、Explain に基づいています (それがそこにある理由です)。

したがって、質問に答えなければならない最も近いことは、結合が単純化されたクエリよりも高速になる理由についてのアイデアはありますか? ...要するに、レイヤー8エラーです。

ただし、処理を高速化するために考慮すべき他のコメントがいくつかあります。あなたA.idの説明によると、主キー(名前の匂いがします)の場合、なぜcount(A.id)すべての行をスキャンする必要があるのですか?インデックスから直接データを取得できるはずですがUsing index、追加のフラグには表示されません。一意のインデックスさえ持っておらず、それはnull不可のフィールドではないようです。それも変なにおいがする。フィールドが null ではなく、一意のインデックスがあることを確認し、説明を再度実行し、余分なフラグに含まれていることを確認してUsing indexから、(適切に) クエリの時間を計ります。はるかに高速に実行する必要があります。

また、上で述べたのと同じパフォーマンスの向上をもたらすアプローチはcount(A.id)count(*).

ちょうど私の2セント。

于 2013-10-29T05:10:27.717 に答える