2

導入と背景

簡単なクエリを最適化する必要がありました (以下の例)。何度か書き直したところ、クエリの書き方によって、同じインデックス操作の推定行数が異なることがわかりました。

当初、クエリはクラスター化インデックス スキャンを実行しました。本番環境のテーブルにはバイナリ列が含まれているため、テーブルが非常に大きく (約 100 GB)、テーブル全体のスキャンの実行に時間がかかりすぎます。

質問

同じインデックス操作で推定行数が異なるのはなぜですか (例を示します)。オプティマイザーはここで何をしていますか?

サンプル データベース - SQL Server 2008 R2 を使用しています

動作を示す本番テーブルの非常に単純化されたバージョンを作成しようとしました。

-- CREATE THE SAMPLE TABLES
----------------------------
CREATE TABLE dbo.MasterTable(
    MasterId    smallint NOT NULL,
    Name        varchar(5) NOT NULL,
    CONSTRAINT PK_MasterTable PRIMARY KEY CLUSTERED (MasterId ASC)
) ON  [PRIMARY]

GO

CREATE TABLE dbo.DetailTable(
    DetailId    bigint IDENTITY(1,1) NOT NULL,
    MasterId    smallint NOT NULL,
    Name        nvarchar(50) NOT NULL,
    CreateDate  datetime NOT NULL,
    CONSTRAINT PK_DetailTable PRIMARY KEY CLUSTERED (DetailId ASC)
) ON  [PRIMARY]

GO

ALTER TABLE dbo.DetailTable
    ADD  CONSTRAINT FK1
    FOREIGN KEY(MasterId) REFERENCES dbo.MasterTable (MasterId)

GO

CREATE NONCLUSTERED INDEX IX_DetailTable
    ON dbo.DetailTable( MasterId ASC, Name ASC )

GO

-- INSERT SOME SAMPLE DATA
----------------------------
SET NOCOUNT ON
GO

-- These are some Codes. In our system we always use these codes to search for "types" of data.

INSERT INTO dbo.MasterTable (MasterId, Name)
VALUES (1, 'N1'), (2, 'N2'), (3, 'N3'), (4, 'N4'), (5, 'N5'), (6, 'N6'), (7, 'N7'), (8, 'N8')

GO

-- ADD ROWS TO THE DETAIL TABLE
-- Takes about 1 minute to run
-- Don't care about the logic, it's just to get a distribution similar to production system
----------------------------
declare @x int = 1
DECLARE @MasterID INT
while (@x <= 400000)
begin
    SET @MasterID = ABS(CHECKSUM(NEWID())) % 8 + 1

    INSERT INTO dbo.DetailTable(MasterId,Name,CreateDate)
    VALUES(
        CASE
            WHEN @MasterID IN (1, 3, 4) AND @x % 20 != 0 THEN 2
            WHEN @MasterID IN (5, 6) AND @x % 20 != 0 THEN 7
            WHEN @MasterID = 8 AND @x % 100 != 0 THEN 7
            ELSE @MasterID
        END,
        NEWID(),
        DATEADD(DAY, - ABS(CHECKSUM(NEWID())) % 1000, GETDATE())
)

SET @x = @x + 1
end

go
-- DO THE INDEX AND STATISTIC MAINTENANCE
----------------------------
alter index all on dbo.DetailTable reorganize
alter index all on dbo.MasterTable reorganize
update statistics dbo.DetailTable WITH FULLSCAN
update statistics dbo.MasterTable WITH FULLSCAN
go

準備が整ったので、クエリを開始しましょう

最初に統計を見てみましょうRANGE_HI_KEY=8。489 個の EQ_ROWS があります。

-- CHECK THE STATISTICS
----------------------------
dbcc show_statistics ('dbo.DetailTable', IX_DetailTable)
GO

次に、クエリを実行します。最初のものは、最適化する必要があった元のクエリです。実行時に現在の実行計画を有効にしてください。「インデックス シーク (非クラスター化) [DetailTable].[IX_DetailTable]」操作を見てください。

-- ORIGINAL QUERY
----------------------------
SELECT d.DetailId
FROM dbo.DetailTable d
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

GO

-- FORCESEEK
----------------------------
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

GO

-- Actual: 489, Estimated 50.000


-- TABLE VARIABLE
----------------------------
DECLARE @MasterId AS TABLE( MasterId SMALLINT )
INSERT INTO @MasterId (MasterId)
SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN @MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

GO

-- Actual: 489, Estimated 40.000

-- TEMP TABLE
----------------------------
CREATE TABLE #MasterId( MasterId SMALLINT )
INSERT INTO #MasterId (MasterId)
    SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'

SELECT d.DetailId
FROM dbo.DetailTable d --WITH (FORCESEEK)
INNER JOIN #MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

-- Actual 489, Estimated 489

DROP TABLE #MasterId

GO

分析と最終質問

「インデックス シーク (非クラスター化) [DetailTable].[IX_DetailTable]」の操作をご覧ください。

上記のスクリプトのコメントは、推定および実際の行数について取得した値を示しています。

私たちの実稼働環境では、このテーブルには 3,300 万行があり、上記のクエリの推定行数は 300 万から 1,600 万まで異なります。

要約する:

  1. DetailTable と MasterTable の間の結合が行われると、推定行数は 12.5% です (マスター テーブルには 8 つの値があり、理にかなっています...)

  2. DetailTable とテーブル変数の間の結合が行われると、推定行数は 10% です。

  3. DetailTable と一時テーブルの間の結合が行われると、推定される行数は実際の行数とまったく同じになります

問題は、なぜこれらの値が異なるのかということです。

統計は最新のものであり、見積もりを行うのは本当に簡単です。

これだけは理解しておきたい。

4

1 に答える 1

0

誰も答えないので、答えようとします:

オプティマイザーにフォローを強制しないでください

(1) 元のクエリについての説明:

SELECT d.DetailId
FROM dbo.DetailTable d
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

このクエリが遅いのはなぜですか?

インデックスがこのクエリをカバーしていないため、このクエリは遅くなります。両方のクエリはインデックススキャンを使用しており、「ハッシュ結合」で結合しています。

ここに画像の説明を入力

mastertable の行全体をスキャンする理由 Master テーブルのインデックスは列 Name ではなく列 MasterId にあるためです。

Detailtable で行全体をスキャンする理由 ここでもインデックスが (DETAILID) "CLUSTERED" AND (MasterId ASC, Name ASC) "NON CLUSTERED"
にあるため、Createdate 列にはありません。

NONCLUSTERED インデックスを 1 つ持つと、この特定のクエリの ON 列 (CREATEDATE,MasterId ) に役立ちます。

マスター テーブルも巨大な場合は、(Name) 列に NONCLUSTERED インデックスを作成できます。

(2) FORCESEEK の説明:

-- フォースシーク

SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'
GO

ここに画像の説明を入力

オプティマイザーが 50,000 行を見積もった理由

ここでは、列 d.MasterId = m.MasterId に参加しており、オプティマイザに詳細テーブルのシークを選択させるよう強制しているため、オプティマイザは INDEX IX_DetailTable() を使用して LOOP join を使用してマスターテーブルに参加します。

オプティマイザーはループ結合を選択してマスターテーブルのすべての行 (実際には 1 つ) をディテールテーブルに結合するため、マスターテーブルから 1 つのキーを選択し、インデックス全体を探して、一致する値をさらに反復子に渡します。

そのため、オプティマイザーは Average of rows per value を選択します。列 40000 テーブルのカーディナリティ (行) に 8 つの一意の値があるため、40000 / 8 は 50,000 行と推定されます (十分に公平です)。

(3) -- テーブル変数

クエリは次のとおりです。

DECLARE @MasterId AS TABLE( MasterId SMALLINT )
INSERT INTO @MasterId (MasterId)
SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN @MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

GO

ここに画像の説明を入力

統計はテーブル変数で維持されないため、オプティマイザには行数がわかりません (したがって、1 行と推定されます) 適切な計画を作成するために対処します。ここでも推定行は 1 で、実際の行は 1 です。

しかし、どのようにオプティマイザーが「40.000」ROWSを推定したか

個人的に私はこれをチェックしたことがなく、この質問のためにサーバーのテストを行いましたが、オプティマイザーが推定行を計算する方法がわからないので、誰かが来て私たちに教えてくれたら素晴らしいです.

(4) -- 一時テーブル

あなたのクエリ

CREATE TABLE #MasterId( MasterId SMALLINT )
INSERT INTO #MasterId (MasterId)
    SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'

SELECT d.DetailId
FROM dbo.DetailTable d --WITH (FORCESEEK)
INNER JOIN #MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

-- Actual 489, Estimated 489
DROP TABLE #MasterId

ここに画像の説明を入力

ここでも、オプティマイザーはテーブル変数で選択したのと同じクエリ プランを選択していますが、違いは、統計が一時テーブルで維持することです。したがって、クエリ オプティマイザーでは、実際に結合する行の公正な ID があります。「N8」キーには 8 があり、dbo.DetailTable の 8 の推定行数は 489 です。

于 2015-03-23T09:42:16.570 に答える