[すべての悪い習慣/ベストプラクティスの投稿のインデックスを参照]
繰り返し行われるBadHabits&Best Practicesプレゼンテーションのスライドの1つは、「COUNT(*)
の悪用」というタイトルです。 。"私はこの虐待が実際にかなり見られます、そしてそれはいくつかの形をとります。
テーブルの行数は?
私は通常これを見ます:
SELECT @count = COUNT(*) FROM dbo.tablename;
SQL Serverは、このカウントを取得するために、テーブル全体に対してブロッキングスキャンを実行する必要があります。それは高価です。この情報はカタログビューとDMVに保存され、すべてのI/Oやブロックなしで取得できます。
SELECT @count = SUM(p.rows) FROM sys.partitions AS p INNER JOIN sys.tables AS t ON p.[object_id] = t.[object_id] INNER JOIN sys.schemas AS s ON t.[schema_id] = s.[schema_id] WHERE p.index_id IN (0,1) -- heap or clustered index AND t.name = N'tablename' AND s.name = N'dbo';
(sys.dm_db_partition_stats
から同じ情報を取得できます 、ただしその場合はp.rows
を変更してください p.row_count
へ (一貫性があります!)。実際、これはsp_spaceused
と同じビューです。 を使用してカウントを導出します。上記のクエリよりも入力がはるかに簡単ですが、その情報が必要な場合を除いて、余分な計算がすべて行われるため、カウントを導出するためだけに使用することはお勧めしません。また、外部の分離レベルに従わないメタデータ関数を使用しているため、このプロシージャを呼び出すときにブロックを待機することになりかねないことにも注意してください。)
さて、これらのビューが100%、マイクロ秒単位で正確ではないのは事実です。ヒープを使用していない限り、sys.dm_db_index_physical_stats()
からより信頼性の高い結果を得ることができます。 列record_count
(これも一貫性があります!)ただし、この関数はパフォーマンスに影響を与える可能性があり、ブロックする可能性があり、SELECT COUNT(*)
よりもさらにコストがかかる可能性があります。 –同じ物理操作を実行する必要がありますが、mode
に応じて追加情報を計算する必要があります (この場合は気にしない断片化など)。ドキュメントの警告は、可用性グループを使用している場合に関連するストーリーの一部を示しています(そして、同様の方法でデータベースミラーリングに影響を与える可能性があります):
ドキュメントには、この数値がヒープに対して信頼できない理由も説明されています(また、行とレコードの不整合の準パスが与えられます):
ヒープの場合、この関数から返されるレコードの数は、ヒープに対してSELECT COUNT(*)を実行することによって返される行の数と一致しない場合があります。これは、行に複数のレコードが含まれる場合があるためです。たとえば、一部の更新状況では、更新操作の結果として、単一のヒープ行に転送レコードと転送レコードが含まれる場合があります。また、ほとんどの大きなLOB行は、LOB_DATAストレージ内の複数のレコードに分割されます。
だから私はsys.partitions
に傾倒します これを最適化する方法として、わずかな精度を犠牲にします。
- "しかし、DMVを使用できません。カウントは非常に正確である必要があります!"
「超正確な」カウントは、実際にはかなり無意味です。 「非常に正確な」カウントの唯一のオプションは、テーブル全体をロックし、誰もが行を追加または削除できないようにすることです(ただし、共有読み取りを妨げることはありません)。例:
SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!
そのため、クエリはハミングし、すべてのデータをスキャンして、その「完璧な」カウントに向けて取り組んでいます。その間、書き込み要求はブロックされ、待機しています。突然、正確なカウントが返されると、テーブルのロックが解除され、キューに入れられて待機していたすべての書き込み要求が、テーブルに対してあらゆる種類の挿入、更新、および削除を開始します。あなたのカウントは今どのくらい「超正確」ですか?すでにひどく時代遅れになっている「正確な」カウントを取得する価値はありましたか?システムがビジーでない場合、これはそれほど問題にはなりませんが、システムがビジーでない場合、DMVはかなり正確であると強く主張します。
NOLOCK
を使用することもできます 代わりに、それは単にライターがデータを読んでいる間にデータを変更できることを意味し、他の問題にもつながります(これについては最近話しました)。多くの球場では問題ありませんが、目標が正確さである場合は問題ありません。 DMVは、多くのシナリオですぐに(または少なくともはるかに近くに)配置され、ごくわずかなシナリオではさらに遠くに配置されます(実際、私が考えることはできません)。
最後に、読み取りコミットスナップショットアイソレーションを使用できます。 Kendra Littleには、スナップショット分離レベルに関するすばらしい投稿がありますが、NOLOCK
で述べた警告のリストを繰り返します。 記事:
- Sch-SロックはRCSIの下でも取得する必要があります。
- スナップショットアイソレーションレベルはtempdbの行バージョン管理を使用するため、実際にそこでの影響をテストする必要があります。
- RCSIは効率的な割り当て順序スキャンを使用できません。代わりに範囲スキャンが表示されます。
- Paul White(@SQL_Kiwi)には、これらの分離レベルについて読む必要のあるすばらしい投稿がいくつかあります。
- コミットされたスナップショットアイソレーションを読む
- 読み取りコミットスナップショットアイソレーションでのデータ変更
- SNAPSHOT分離レベル
さらに、RCSIを使用しても、「正確な」カウントを取得するには時間がかかります(およびtempdbの追加リソース)。操作が終了するまでに、カウントはまだ正確ですか?その間に誰もテーブルに触れていない場合のみ。したがって、RCSI(リーダーがライターをブロックしない)の利点の1つが無駄になります。
WHERE句に一致する行はいくつですか?
これは少し異なるシナリオです。テーブルの特定のサブセットに存在する行数を知る必要があります。 WHERE
でない限り、これにDMVを使用することはできません。 句は、フィルタリングされたインデックスに一致するか、正確なパーティション(または複数)を完全にカバーします。
WHERE
の場合 句は動的であるため、上記のようにRCSIを使用できます。
WHERE
の場合 句は動的ではなく、RCSIを使用することもできますが、次のいずれかのオプションを検討することもできます。
- フィルタリングされたインデックス –たとえば、
is_active = 1
のような単純なフィルターがある場合 またはstatus < 5
、次に、次のようなインデックスを作成できます。CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;
これで、DMVからかなり正確なカウントを取得できます。これは、このインデックスを表すエントリがあるためです(heap(0)/ clustered index(1)に依存する代わりに、index_idを識別する必要があります)。ただし、フィルタリングされたインデックスの弱点のいくつかを考慮する必要があります。
- インデックス付きビュー -たとえば、顧客ごとの注文を頻繁にカウントしている場合は、インデックス付きビューが役立つ可能性があります(ただし、これを「インデックス付きビューがすべてのクエリを改善する」という一般的な推奨と見なさないでください):
CREATE VIEW dbo.view_name WITH SCHEMABINDING AS SELECT customer_id, customer_count = COUNT_BIG(*) FROM dbo.table_name GROUP BY customer_id; GO CREATE UNIQUE CLUSTERED INDEX ix_v ON dbo.view_name(customer_id);
これで、ビュー内のデータが具体化され、カウントがテーブルデータと同期されることが保証されます(
MERGE
を使用した場合など、これが当てはまらないいくつかのあいまいなバグがあります 、しかし一般的にこれは信頼できます)。これで、ビューをクエリすることで、はるかに低いクエリコスト(1回または2回の読み取り)で、顧客ごと(または顧客のセット)のカウントを取得できます。SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;
ただし、無料のランチはありません 。インデックス付きビューを維持するためのオーバーヘッドと、それがワークロードの書き込み部分に与える影響を考慮する必要があります。このタイプのクエリをあまり頻繁に実行しない場合は、問題を起こす価値がない可能性があります。
これも少し違う質問です。しかし、私はよくこれを見ます:
IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists
明らかに実際の数は気にしないので、少なくとも1つの行が存在する場合にのみ気にします。実際には、次のように変更する必要があると思います。
IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)
これは、少なくともテーブルの最後に到達する前に短絡する可能性があり、ほとんどの場合、COUNT
を上回ります。 バリエーション(SQLServerがIF (SELECT COUNT...) > 0
を変換するのに十分賢い場合もあります より単純なIF EXISTS()
)。行が見つからない(またはスキャンの最後のページで最初の行が見つかった)絶対的な最悪のシナリオでは、パフォーマンスは同じになります。
[すべての悪い習慣/ベストプラクティスの投稿のインデックスを参照]