[MySQL] count() クエリのパフォーマンスのレビュー
1. 背景
使用されるデータベースはMySQL8
、ストレージ エンジンは ですInnodb
。
通常、ページング インターフェイスはデータベースに対して 2 回クエリを実行し、1 回目は特定のデータを取得し、2 回目はレコードの総行数を取得し、結果を統合して返します。
次のような特定のデータについて SQL をクエリします。
select id, name from user limit 1, 20;
パフォーマンス上の問題はありません。
ただし、別の SQL では count(*) を使用してレコード行の合計数をクエリします。次に例を示します。
select count(*) from user;
しかし、パフォーマンスが悪いという問題があります。
なぜこのようなことが起こるのでしょうか?
2. count(*) のパフォーマンスが低いのはなぜですか?
MySQL では、count(*)
テーブルに記録されている行の総数をカウントする機能があります。
パフォーマンスcount(*)
はストレージ エンジンに直接関係しており、すべてのストレージ エンジンのcount(*)
パフォーマンスが低いわけではありません。
MySQL で最も一般的に使用されるストレージ エンジンはinnodb
次のとおりですmyisam
。
myisam では総行数がディスクに保存されるため、count(*) を使用すると追加の計算を行わずにそのデータを返すだけで済むため、実行効率が非常に高くなります。
InnoDB は異なりますが、トランザクションをサポートし、MVCC
複数バージョンの同時実行制御が存在するため、同じ時点の異なるトランザクションでは、同じクエリ SQL によって返されるレコード行数が不確実になる可能性があります。
innodb で count(*) を使用する場合、ストレージ エンジンからデータを 1 行ずつ読み込んで蓄積する必要があるため、実行効率が非常に低くなります。
テーブル内のデータ量が少ない場合は問題ありませんが、テーブル内のデータ量が多くなると、innodb ストレージ エンジンが count(*) 統計を使用するとパフォーマンスが非常に低下します。
3. count(*) のパフォーマンスを最適化するにはどうすればよいですか?
上記のことから、count(*)
パフォーマンスに問題があることがわかりましたが、それを最適化するにはどうすればよいでしょうか?
次のような側面から始めることができます。
3.1. Redis キャッシュの追加
総ビュー数や総訪問者数などの単純なカウント(*)の場合は、Redis を使用してインターフェイスを直接キャッシュすることができ、リアルタイムでカウントする必要はありません。
ユーザーが指定されたページを開くたびに、キャッシュに count = count+1 が設定されます。
ユーザーが初めてページにアクセスしたとき、Redis のカウント値は 1 に設定されます。今後ユーザーがページにアクセスするたびに、カウントは 1 ずつ増加し、最終的に Redis (Redis のメモリ使用量) にリセットされます。
このようにして、数量を表示する必要がある場合、カウント値を Redis から見つけて返すことができます。
このシナリオでは、データ ポイント テーブルからの count(*) リアルタイム統計を使用する必要がなく、パフォーマンスが大幅に向上します。
ただし、同時実行性が高い状況では、キャッシュとデータベースの間でデータの不整合が発生する可能性があります。
ただし、合計ビュー数や合計訪問者数をカウントするなどのビジネス シナリオの場合、データの精度は高くなく、データの不整合は許容されます。
3.2. レベル 2 キャッシュの追加
一部のビジネス シナリオでは、新しいデータがほとんどなく、そのほとんどが統計操作であり、クエリ条件が多数あります。現時点では、従来の count(*) リアルタイム統計を使用した場合のパフォーマンスは明らかに良くありません。
ID、名前、ステータス、時間、ソースなどの 1 つ以上の条件を通じてページ上のブランドの数をカウントできる場合。
この場合、ユーザーには多くの組み合わせ条件があり、結合インデックスを追加することは役に立ちません。ユーザーは 1 つ以上のクエリ条件を選択できます。結合インデックスが失敗する場合があり、インデックスは条件を満たす場合にのみ追加できます。ユーザーが最も頻繁に使用する条件。
つまり、インデックスを作成できる組み合わせ条件もあれば、インデックスを作成できない組み合わせ条件もあります。インデックスを作成できないこれらのシナリオを最適化するにはどうすればよいでしょうか?
回答: を使用します二级缓存
。
2 次キャッシュは実際にはメモリ キャッシュです。
2次キャッシュの機能を使用caffine
または実装できます。guava
Caffineが統合されておりSpring Boot
、非常に使いやすくなっています。
2 次キャッシュを増やす必要があるクエリ メソッドでアノテーションを使用するだけです@Cacheable
。
@Cacheable(value = "brand", , keyGenerator = "cacheKeyGenerator")
public BrandModel getBrand(Condition condition) {
return getBrandByCondition(condition);
}
次に、cacheKeyGenerator をカスタマイズしてキャッシュ キーを指定します。
public class CacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + UNDERLINE
+ method.getName() + ","
+ StringUtils.arrayToDelimitedString(params, ",");
}
}
このキーはさまざまな条件から構成されます。
このようにして、特定の条件の組み合わせを通じてブランド データをクエリした後、結果はメモリにキャッシュされ、有効期限は 5 分に設定されます。
その後、ユーザーが同じ条件を使用して 5 分以内に再度データをクエリすると、データは 2 次キャッシュから直接取得され、直接返されます。
これにより、count(*) のクエリ効率が大幅に向上します。
ただし、2 次キャッシュを使用する場合は、異なるサーバーに異なるデータが存在する可能性があります。実際のビジネスシーンに応じて選択する必要があり、すべてのビジネスシーンに適用できるわけではありません。
3.3. マルチスレッド実行
有効な注文が何件あり、無効な注文が何件あるのかを数えるというような要件を実行したことがあるかどうかはわかりません。
この場合、通常は 2 つの SQL を記述する必要があり、有効な注文をカウントする SQL は次のとおりです。
select count(*) from order where status = 1;
無効な注文をカウントする SQL は次のとおりです。
select count(*) from order where status = 0;
ただし、インターフェイス内にある場合、これら 2 つの SQL を同期的に実行する効率は非常に低くなります。
この時点で、SQL に変更できます。
select count(*), status from order
group by status;
キーワード グループ化を使用してgroup by
同じステータスの数をカウントすると、2 つのレコードのみが生成されます。1 つのレコードは有効な注文の数であり、もう 1 つのレコードは無効な注文の数です。
しかし問題があります: ステータス フィールドには 1 と 0 の 2 つの値しかありません。繰り返しの度合いが非常に高く、差別化の度合いが非常に低いです。インデックスを付けることができず、テーブル全体をスキャンすることになります。あまり効率的ではありません。
他に解決策はありますか?
回答: マルチスレッドを使用してください。
CompleteFuture
SQL への2 つの线程
非同期呼び出しを使用して有効な注文をカウントし、SQL を使用して無効な注文をカウントし、最後にデータを要約することで、クエリ インターフェイスのパフォーマンスを向上させることができます。
3.4. 結合テーブルの削減
ほとんどの場合、count(*) はリアルタイムで合計数量をカウントするために使用されます。
ただし、テーブル自体のデータ量は少なくても、結合テーブルが多すぎる場合は、count(*) の効率も影響を受ける可能性があります。
たとえば、製品情報をクエリする場合は、製品名、ユニット、ブランド、分類などの情報に基づいてデータをクエリする必要があります。
このとき、次のような必要なデータを見つけるための SQL を記述します。
select count(*)
from product p
inner join unit u on p.unit_id = u.id
inner join brand b on p.brand_id = b.id
inner join category c on p.category_id = c.id
where p.name = '后端码匠' and u.id=123 and b.id = 124 and c.id=125;
製品テーブルを使用して、join
ユニット、ブランド、カテゴリの 3 つのテーブルを削除します。
実際、これらのクエリ条件は製品テーブル内のデータをクエリでき、追加のテーブルを結合する必要はありません。
SQLを次のように変更できます。
select count(*)
from product
where name = '后端码匠' and unit_id = 123 and brand_id = 124 and category_id = 125;
カウント (*) する場合、積テーブルのみをクエリし、冗長なテーブル結合を削除することで、クエリ効率が大幅に向上します。
3.5. ClickHouse への変更
場合によっては、結合テーブルが多すぎて、冗長な結合を削除できないことがあります。
たとえば、上記の例では、製品情報をクエリする場合、製品名、ユニット名、ブランド名、カテゴリ名などの情報に基づいてデータをクエリする必要があります。
現時点では、製品テーブルに基づいてデータをクエリすることは不可能であり、join
ユニット、ブランド、カテゴリの 3 つのテーブルにアクセスする必要があります。
回答: データは に保存できますClickHouse
。
列存储
ClickHouseはトランザクションをサポートしないデータベースをベースにしており、非常に高いクエリ パフォーマンスを備えており、10 億を超えるデータをクエリし、数秒でそれを返すことができると主張しています。
ビジネス コードの埋め込みを避けるために、Canal
リスニングMySQL
ログを使用できますbinlog
。製品テーブルに新しいデータを追加するときは、ユニット、ブランド、カテゴリのデータを同時にクエリし、新しい結果セットを生成して、それを ClickHouse に保存する必要があります。
データをクエリする場合、ClickHouseからクエリを実行するため、count(*)を使用したクエリ効率がN倍向上します。
特別な注意事項: ClickHouse を使用するときは、新しいデータを頻繁に追加せず、データをバッチで挿入するようにしてください。
実は、クエリ条件が多い場合にはClickHouseの使用にはあまり適しておらず、この時点で変更することもできますElasticSearch
が、MySQLと同様の問題があります深分页
。
4. count のさまざまな使用法のパフォーマンス比較
count(*) について話しているので、count(1)、count(id)、count (通常のインデックス列)、count (インデックスなしの列) など、count ファミリーの他のメンバーについても説明する必要があります。
それで、違いは何ですか?
- count(*): 何も処理せずに全行のデータを取得し、行数に1を加算します。
- count(1): すべての行のデータを取得します。各行の固定値は 1 (行数 + 1) です。
- count(id): id は主キーを表します。データのすべての行から id フィールドを解析する必要があります。id は NULL であってはならず、行数は 1 ずつ増加します。
- count (通常のインデックス列): すべての行のデータから通常のインデックス列を解析し、NULL かどうかを判断する必要があります。NULL でない場合は行数 + 1。
- count (インデックスなし列): テーブル全体をスキャンしてすべてのデータを取得します。分析中にインデックス付き列は追加されず、NULL かどうかが判断されます。そうでない場合は行数が +1 されます。
これから、高値から低値までの最終的なカウント パフォーマンスは次のようになります。
count(*) ≈ count(1) > count(id) > count (通常のインデックス列) > count (インデックスのない列)
したがって、count(*)
実際にはこれが最も高速です。select *
と混同しないでください。