7

3,000 万件近くのレコードを含むテーブルがあります。ほんの数列。列の 1 つに30'Born'を超える異なる値がなく、それに定義されたインデックスがあります。その列をフィルタリングして、結果を効率的にページングできる必要があります。

今のところ私は持っています (たとえば、検索している年が '1970' の場合 - これはストアド プロシージャのパラメーターです):

WITH PersonSubset as
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
    FROM Person WITH (INDEX(IX_Person_Born)) 
    WHERE Born = '1970'
)
SELECT *, (SELECT count(*) FROM PersonSubset) AS TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30

その種のすべてのクエリ (Born使用されるパラメーターのみ) は、100 万を超える結果を返します。最大のオーバーヘッドは、合計結果を返すために使用されるカウントにあることに気付きました。select 句から削除する(SELECT count(*) FROM PersonSubset) AS TotalPeopleと、全体が大幅に高速化されます。

そのクエリでカウントを高速化する方法はありますか。私が気にしているのは、ページングされた結果と合計数が返されるようにすることです。

4

4 に答える 4

11

コメントで次の議論を更新

ここでの問題の原因は、インデックスのカーディナリティが非常に低いことです。IX_Person_Born

SQL インデックスは、値をすばやく絞り込むのに非常に優れていますが、同じ値を持つレコードが多数ある場合は問題があります。

これは、電話帳のインデックスのようなものと考えることができます。「Smith, John」を見つけたい場合、最初に S で始まる名前がたくさんあることがわかり、次に Smith という人のページが次々と表示されます。たくさんのジョン。あなたは本をスキャンすることになります。

電話帳のインデックスがクラスター化されているため、これは複雑になります。レコードは姓でソートされます。代わりに、"John" という名前の全員を見つけたい場合は、多くの検索を行うことになります。

ここには 3,000 万件のレコードがありますが、異なる値は 30 しかありません。つまり、可能な限り最良のインデックスでも約 100 万件のレコードが返されることを意味します。これらの 100 万件の結果のそれぞれは、実際のレコードではありません。これは、インデックスからテーブル (電話帳に例えると、ページ番号) へのルックアップであるため、処理がさらに遅くなります。

年ではなく、カーディナリティ インデックス (完全な生年月日など) の方がはるかに高速です。

これは、すべての OLTP リレーショナル データベースの一般的な問題ですlow cardinality + huge datasets = slow queries。インデックス ツリーはあまり役に立たないためです。

要するに、T-SQL とインデックスを使用してカウントを取得するための大幅に迅速な方法はありません。

いくつかのオプションがあります。

1. データ集約

OLAP/Cube のロールアップまたは自分で行う:

select Born, count(*) 
from Person 
group by Born

長所は、キューブの検索やキャッシュのチェックが非常に高速であることです。問題は、データが古くなり、それを説明する何らかの方法が必要になることです。

2. 並列クエリ

2 つのクエリに分割します。

SELECT count(*) 
FROM Person 
WHERE Born = '1970'

SELECT TOP 30 *
FROM Person 
WHERE Born = '1970'

次に、これらを並列サーバー側で実行するか、ユーザー インターフェイスに追加します。

3.ノーSQL

この問題は、SQL を使用しないソリューションが従来のリレーショナル データベースに勝る大きな利点の 1 つです。非 SQL システムでは、Personテーブルは多数の安価なサーバー間でフェデレーション (またはシャーディング) されます。ユーザーが検索すると、すべてのサーバーが同時にチェックされます。

この時点で、技術的な変更はおそらく出ていますが、調査する価値があるかもしれないので、私はそれを含めました.

私は過去にこの種のサイズのデータ​​ベースで同様の問題を抱えており、(コンテキストに応じて) オプション 1 と 2 の両方を使用しました。ここでの合計がページングの場合は、おそらくオプション 2 と AJAX を使用します。呼び出してカウントを取得します。

于 2012-11-29T15:42:28.227 に答える
2
DECLARE @TotalPeople int
  --does this query run fast enough?  If not, there is no hope for a combo query.
SET @TotalPeople = (SELECT count(*) FROM Person WHERE Born = '1970')


WITH PersonSubset as
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
    FROM Person WITH (INDEX(IX_Person_Born)) 
    WHERE Born = '1970'
)
SELECT *, @TotalPeople as TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30

通常、低速なクエリを高速なクエリと組み合わせて高速なクエリにすることはできません。


「Born」列の 1 つに 30 を超える異なる値がなく、それに定義されたインデックスがあります。

SQL Server がインデックスまたは統計を使用していないか、インデックスと統計が十分に役に立ちません。

これは、Sqlの手を強制する必死の対策です(書き込みに非常にコストがかかるという潜在的なコストがかかります-それを測定し、ビューが存在する間は Person テーブルへのスキーマ変更をブロックします)。

CREATE VIEW dbo.BornCounts WITH SCHEMABINDING
AS
SELECT Born, COUNT_BIG(*) as NumRows
FROM dbo.Person
GROUP BY Born

GO 

CREATE UNIQUE CLUSTERED INDEX BornCountsIndex ON BornCounts(Born)

ビューにクラスター化インデックスを配置することにより、システムが維持するコピーになります。このコピーのサイズは 3000 万行よりはるかに小さく、探している情報が正確に含まれています。ビューを使用するためにクエリを変更する必要はありませんでしたが、必要に応じてクエリでビューの名前を自由に使用できます。

于 2012-11-29T15:40:37.660 に答える
1
WITH PersonSubset as
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
    FROM Person WITH (INDEX(IX_Person_Born)) 
    WHERE Born = '1970'
)
SELECT *, **max(Row) AS TotalPeople**
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30

なぜそれが好きではないのですか?

編集、太字が機能しない理由がわかりません:<

于 2012-11-29T16:05:43.783 に答える
1

これは、システム dmv を使用した斬新なアプローチです。これは、「十分な」カウントでやり遂げることができ、[Born] の個別の値ごとにインデックスを作成することを気にせず、内部が少し汚れていることを気にしない場合です。 .

年ごとにフィルター選択されたインデックスを作成します。

--pick a column to index, it doesn't matter which.    
CREATE INDEX IX_Person_filt_1970 on Person ( id )  WHERE Born = '1970'
CREATE INDEX IX_Person_filt_1971 on Person ( id )  WHERE Born = '1971'
CREATE INDEX IX_Person_filt_1972 on Person ( id )  WHERE Born = '1972'

次に、sys.partitions の [rows] 列を使用して行数を取得します。

WITH PersonSubset as
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY Born asc) AS Row
    FROM Person WITH (INDEX(IX_Person_Born)) 
    WHERE Born = '1970'
)
SELECT *, 
    (
    SELECT sum(rows) 
    FROM sys.partitions p 
        inner join sys.indexes i on p.object_id = i.object_id and p.index_id =i.index_id 
        inner join sys.tables t on t.object_id = i.object_id 
    WHERE t.name ='Person' 
        and i.name = 'IX_Person_filt_' + '1970' --or at @p1 
    )  AS TotalPeople
FROM PersonSubset
WHERE Row BETWEEN 0 AND 30

Sys.partitions は、100% のケースで正確であるとは限りません (通常、正確であるか、非常に近いです)。 [Born] 以外でフィルタリングする必要がある場合、このアプローチは機能しません。

于 2012-12-07T04:23:08.707 に答える