web-dev-qa-db-ja.com

MySQL-左結合に時間がかかりすぎる、クエリを最適化する方法は?

リーダーには多くのフォロワーがいる場合があります。 notification_followersテーブルは、リーダーがleader_id 1およびnotifiable_id 0(テーブルのID 1、2)の投稿を追加すると、単一の通知を受け取ります。現在のユーザー14の後に誰かleader_id 0notifiable_id 14が付いている場合、同じテーブルは単一の通知を受け取ります(テーブルのID 3)。

notification_followersidはPRIMARY、データを除く各フィールドは独自のインデックスです

| id | uuid               | leader_id | notifable_id | data   | created_at
-----------------------------------------------------------------------------------
| 1  | 001w2cwfoqzp8F3... | 1         | 0            | Post A | 2018-04-19 00:00:00
| 2  | lvbuX4d5qCHJUIN... | 1         | 0            | Post B | 2018-04-20 00:00:00
| 3  | eEq5r5g5jApkKgd... | 0         | 14           | Follow | 2018-04-21 00:00:00

フォロワーに関連するすべての通知が1か所に表示されるようになりました。

ユーザー14leader_id 1のフォロワーであるかどうかを確認して、通知12を表示するかどうかを確認する必要があります。そのため、user_followsテーブルをスキャンして、ログインしたユーザーがfollowed_idに対してleader_idとして存在するかどうかを確認します。これにより、彼らは通知について知っていますが、リーダーをフォローしている場合のみ- before通知が投稿されました(新しいフォロワーは、ユーザーをフォローするときに古い投稿通知を受け取るべきではなく、新しい通知のみを受け取ります)。

user_follows(idはPRIMARY、各フィールドは独自のインデックスです)

| id | leader_id | follower_id | created_at
----------------------------------------------------
| 1  | 1         | 14         |  2018-04-18 00:00:00 // followed before, has notifs
| 2  | 1         | 15         |  2018-04-22 00:00:00 // followed after, no notifs

最後に注意する必要があるのは、通知が読み取られたかどうかをユーザーが知っておく必要があることです。ここにnotification_followers_readテーブルが入ります。follower_idnotification_uuidがすべて格納されますread_atタイムスタンプとともに通知を読み取ります。

notification_followers_read(notification_uuid、follower_idの複合インデックス)

| notification_uuid | follower_id | read_at
--------------------------------------------------------
  qIXE97AP49muZf... | 17          | 2018-04-21 00:00:00 // not for 14, we ignore it

ここで、ユーザーnf.idの自動インクリメント14 descによって注文された最新の10件の通知を返します。 これらの通知はまだこのユーザーによって読み込まれていませんであるため、notification_followersからの3つの通知すべてが表示されます。最初の2人はリーダーをフォローしたためbeforeリーダーが投稿を行い、3番目の通知はフォローされたためnotifiable_id14です。

これは機能するクエリですが、時間がかかりすぎます〜9秒

SELECT nf.id, nf.uuid, nf.leader_id, nf.data, nf.created_at, nfr.read_at
FROM notification_followers nf
LEFT JOIN user_follows uf ON uf.leader_id = nf.leader_id AND uf.follower_id = 14
LEFT JOIN notification_followers_read nfr ON nf.uuid = nfr.notification_uuid AND nfr.follower_id = 14
WHERE (nf.created_at > uf.created_at OR notifiable_id = 14)
ORDER BY nf.id DESC LIMIT 10

notification_followersには約10万件のレコードがあり、InnoDBを使用しています。クエリのEXPLAINは次のとおりです。

Explain

クエリを最適化して数ミリ秒で実行するにはどうすればよいですか?

SQLダンプで更新

SQL DUMP TO REPLYUCE LOCALLYspeed_testデータベースをローカルで作成し、ファイルをインポートして、すべてのテーブルデータ(〜100K行)でライブクエリの問題をライブで確認します

2
Wonka

コメントからの要約:

これまでのところ、私は最高の結果を得ています

CREATE INDEX nfr_fid_nuuid
             ON notification_followers_read
                (follower_id,
                 notification_uuid);

そして

CREATE INDEX uf_fid_lid
             ON user_follows
                (follower_id,
                 leader_id);

プライマリインデックスを除く他のすべてのインデックスは削除されました。 notification_followersの場合、PRIMARYインデックスを使用しました。これまでのところ、このテーブルでPRIMARYより良いものを見つけることができませんでした。

テストは、MySQL v5.7.21 32ビットWindows 7 32ビットで行われました。

実行時間は、前述のようにインデックスなしで約4秒、インデックスありで.2秒でした。

方法、理由、何でもに関するいくつかの行:(コメントにそのためのスペースがありませんでした)

(免責事項:それに関する私の知識は全体的に悪いことではありません。ただし、一部の側面では、私の理解は改善可能であるか、明らかに間違っているかもしれません。どこかで間違っている場合は、だれでも自由に修正できます。編集またはコメントを歓迎します。)

パフォーマンスに関する結合についての一般的なこと:

コメントで既に述べたように、結合の1つの目標は、セットをできるだけ早く結合することです。説明:最悪の場合、ネストされたループ結合を適用する必要がある場合、A JOIN B#A *#B(letを必要とします。 #A[〜#〜] a [〜#〜]の行数であり、[〜#〜 ] b [〜#〜])比較演算。したがって、[〜#〜] a [〜#〜](または[〜#〜] b [〜#〜])は、実際の結合操作が適用される前に除外できるため、1だけでなく#Bだけ操作の数を減らします。 (または#A)。パフォーマンスの点でそれが望まれるでしょう。

インデックスを介して結合を実行できる場合、特にある方法で、DBMSが結合に関連するインデックスの部分を簡単にローカライズできる(つまり、セットを小さく保つ)と、非常に効果的です。もちろん、インデックスがここで提供できる他のいくつかの利点があります(たとえば、行はソート済みの方法ですでにアクセス可能であり、より効率的な結合方法をサポートしています。インデックスが大幅に小さくなり、メモリに大きく収まるため、一定のディスクIOの必要性が減少します。 ...)。

しかし、それはすべてそれ自体がトピックであるため、これは大まかな要約として意図されています。

クエリについての質問は次のとおりです:

さて、クエリについて最初に気づくのは、それがLEFT OUTER JOINであることです(実際には2つですが、この考えでは問題ではありません)。 notification_followersはここの左側のテーブルなので、そのレコードセットは結合によって削減されず、WHEREがそれを実行できるだけです。

WHEREは残念ながらORです。これらはANDとは対照的に難しく、「悪い」ものです。これは、集合のカーディナリティを削減する交差のようなものではなく、ユニオンに似ているため、セットを大きく維持します(比較:A OR Bの場合、結果セットはすべての行WHERE A UNIONすべての行WHERE Bとは対照的です)結果セットがすべての行であるA AND BWHERE A INTERSECTすべての行WHERE B)。

したがって、WHEREは、1回の実行で1つのインデックスだけから答えられるほど有望なターゲットではありません。さらに、ORからのWHEREed操作の1つ(nf.created_at > uf.created_at)は結合されたデータに依存するため、結合後(またはせいぜい)にのみ適用できます。

結果セットが大きすぎてメモリに保持できない場合は、特に高価になる可能性があるORDER BYもあります。次に、ディスクへの書き込みと読み取りを常に行うようにソートする必要があります(より大きなバッファーの場合)。また、ディスクアクセスには時間がかかります。

したがって、notification_followersに対する私の希望は、ORDERと、理想的にはORed比較の少なくとも1つをサポートする複合インデックスを見つけることでした。言及したように、私はそれに失敗しました。しかし、上記の部分についての議論を考えると、私の期待もそれほど高くありませんでした。

またはPRIMARYは、DBMSの観点からはそれで十分ですが、問題ないかもしれません。私が理解しているように、InnoDBの主キーを持つテーブルは実際には クラスター化インデックス として保存されます。私がドキュメントで(すばやく)見つけることができなかったのは、レコードが主キーによって順番に二重リンクされている場合です。これにより、PRIMARYがそのリンクリストの逆トラバースによってORDERをサポートできるようになり、PRIMARYが実際に適切な選択肢になります。

結合されたテーブルのONsは、WHEREおよびORDERとは対照的にかなり簡単です。 (例では、user_followsとの結合を使用します。notification_followers_readはアナログです。)ここでは、2つの関連する列leader_idfollower_idがあります。

follower_idは、複合インデックスの最初の列に適しています。リテラルと比較されるため、結合のパートナー行とは無関係です。インデックスの関連部分であるサブツリー-MySQLの「通常の」インデックスは 一部のBツリーバリアント -したがって、すべての結合パートナーに(再)使用できます。また、ここでuser_followsから可能な行のセットが削減されたことにも注意してください。

また、そのインデックスの列としてleader_idを使用すると、結合のuser_followsの部分がこのインデックスのみから応答可能になります。そして実際にそれは働いた。

ステートメントの列の順序は、それらの列のインデックスと必ずしも同じではないことに注意してください。交換可能であるものは何でも、それがより良いと約束した場合、オプティマイザによって交換されます。したがって、順序は必ずしもとにかく保持されません。インデックスの適切な列の順序を見つけるには、主に、インデックスを最も「根本的な」方法でパーティション分割する順序を考える必要があります(残りの部分をできるだけ小さくします)。

3
sticky bit

むやみにLEFTを使用しないでください。最初のLEFTは間違っていると思います。2番目は次のように再定式化できます。

SELECT  nf.id, nf.uuid, nf.leader_id,
        nf.data, nf.created_at,
        ( SELECT  read_at
            FROM  notification_followers_read
            WHERE  nf.uuid = notification_uuid
              AND  follower_id = 14 
        ) AS read_at
    FROM  notification_followers nf
    JOIN  user_follows uf  ON uf.leader_id = nf.leader_id
    WHERE  (nf.created_at > uf.created_at
              OR  nf.notifiable_id = 14 
           )
      AND  uf.follower_id = 14
    ORDER BY  nf.id DESC
    LIMIT  10 

ONは、テーブルの関係を示すものであり、フィルタリング基準は含まれていません。

インデックス:

notification_followers_read:
       INDEX(notification_uuid, follower_id,  -- in either order
             read_at)   -- last (to make it 'covering')
user_follows:
       INDEX(leader_id, follower_id,  -- in either order
             created_at)   -- last

(これらのインデックスはstickbitと一致しませんが、改善されます。)

2
Rick James