リーダーには多くのフォロワーがいる場合があります。 notification_followers
テーブルは、リーダーがleader_id 1
およびnotifiable_id 0
(テーブルのID 1、2)の投稿を追加すると、単一の通知を受け取ります。現在のユーザー14
の後に誰かleader_id 0
とnotifiable_id 14
が付いている場合、同じテーブルは単一の通知を受け取ります(テーブルのID 3)。
notification_followers
(idは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か所に表示されるようになりました。
ユーザー14
がleader_id 1
のフォロワーであるかどうかを確認して、通知1
と2
を表示するかどうかを確認する必要があります。そのため、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_id
とnotification_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_id
は14
です。
これは機能するクエリですが、時間がかかりすぎます〜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
は次のとおりです。
クエリを最適化して数ミリ秒で実行するにはどうすればよいですか?
SQLダンプで更新
SQL DUMP TO REPLYUCE LOCALLYspeed_test
データベースをローカルで作成し、ファイルをインポートして、すべてのテーブルデータ(〜100K行)でライブクエリの問題をライブで確認します。
コメントからの要約:
これまでのところ、私は最高の結果を得ています
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 B
WHERE A INTERSECT
すべての行WHERE B
)。
したがって、WHERE
は、1回の実行で1つのインデックスだけから答えられるほど有望なターゲットではありません。さらに、OR
からのWHERE
ed操作の1つ(nf.created_at > uf.created_at
)は結合されたデータに依存するため、結合後(またはせいぜい)にのみ適用できます。
結果セットが大きすぎてメモリに保持できない場合は、特に高価になる可能性があるORDER BY
もあります。次に、ディスクへの書き込みと読み取りを常に行うようにソートする必要があります(より大きなバッファーの場合)。また、ディスクアクセスには時間がかかります。
したがって、notification_followers
に対する私の希望は、ORDER
と、理想的にはOR
ed比較の少なくとも1つをサポートする複合インデックスを見つけることでした。言及したように、私はそれに失敗しました。しかし、上記の部分についての議論を考えると、私の期待もそれほど高くありませんでした。
またはPRIMARY
は、DBMSの観点からはそれで十分ですが、問題ないかもしれません。私が理解しているように、InnoDBの主キーを持つテーブルは実際には クラスター化インデックス として保存されます。私がドキュメントで(すばやく)見つけることができなかったのは、レコードが主キーによって順番に二重リンクされている場合です。これにより、PRIMARY
がそのリンクリストの逆トラバースによってORDER
をサポートできるようになり、PRIMARY
が実際に適切な選択肢になります。
結合されたテーブルのON
sは、WHERE
およびORDER
とは対照的にかなり簡単です。 (例では、user_follows
との結合を使用します。notification_followers_read
はアナログです。)ここでは、2つの関連する列leader_id
とfollower_id
があります。
follower_id
は、複合インデックスの最初の列に適しています。リテラルと比較されるため、結合のパートナー行とは無関係です。インデックスの関連部分であるサブツリー-MySQLの「通常の」インデックスは 一部のBツリーバリアント -したがって、すべての結合パートナーに(再)使用できます。また、ここでuser_follows
から可能な行のセットが削減されたことにも注意してください。
また、そのインデックスの列としてleader_id
を使用すると、結合のuser_follows
の部分がこのインデックスのみから応答可能になります。そして実際にそれは働いた。
ステートメントの列の順序は、それらの列のインデックスと必ずしも同じではないことに注意してください。交換可能であるものは何でも、それがより良いと約束した場合、オプティマイザによって交換されます。したがって、順序は必ずしもとにかく保持されません。インデックスの適切な列の順序を見つけるには、主に、インデックスを最も「根本的な」方法でパーティション分割する順序を考える必要があります(残りの部分をできるだけ小さくします)。
むやみに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と一致しませんが、改善されます。)