カーディナリティの推定値が低いと、INSERTは最小限のログから除外されますか?


11

2番目のINSERTステートメントが最初のステートメントより〜5倍遅いのはなぜですか?

生成されたログデータの量から、2番目のログは最小限のログ記録に適していないと思います。ただし、データ読み込みパフォーマンスガイドのドキュメントには、両方の挿入を最小限に記録できることが示されています。では、最小限のロギングが主要なパフォーマンスの違いである場合、2番目のクエリが最小限のロギングに適格ではないのはなぜですか?状況を改善するために何ができますか?


クエリ#1:INSERT ... WITH(TABLOCK)を使用して5MM行を挿入する

5MM行をヒープに挿入する次のクエリを考えてみます。このクエリは、によって報告されるトランザクションログデータで実行1 secondおよび生成64MBされsys.dm_tran_database_transactionsます。

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


クエリ#2:同じデータを挿入するが、SQLは行数を過小評価している

今度は非常によく似たクエリについて考えてみましょう。このクエリはまったく同じデータを操作しますがSELECT、カーディナリティの推定値が低すぎるテーブル(または実際の本番のケースでは結合が多い複雑なステートメント)から偶然に描画されます。このクエリは、トランザクションログデータを実行し5.5 secondsて生成し461MBます。

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


完全なスクリプト

テストデータを生成し、これらのシナリオのいずれかを実行するスクリプトの完全なセットについては、このPastebinを参照してください。SIMPLE 復旧モデルのデータベースを使用する必要があることに注意してください。


ビジネスコンテキスト

私たちは数百万行ものデータを半頻繁に移動していますが、実行時間とディスクI / O負荷の両方の観点から、これらの操作をできるだけ効率的にすることが重要です。最初はヒープテーブルを作成して使用するのINSERT...WITH (TABLOCK)が良い方法だと思っていましたが、実際の本番シナリオで上記の状況を確認したため、自信がなくなってきました(ただし、クエリが複雑ではなく、簡略版はこちら)。

回答:


7

2番目のクエリが最小限のログ記録に適さないのはなぜですか?

2番目のクエリでは最小限のロギングを使用できますが、エンジンは実行時にそれを使用しないことを選択します。

最小INSERT...SELECTロードしきい値があり、それを下回ると、バルクロード最適化を使用しないことを選択します。一括行セット操作の設定にはコストがかかります。数行のみを一括挿入すると、スペースを効率的に使用できなくなります。

状況を改善するために何ができますか?

SELECT INTOこのしきい値を持たない他の多くの方法(など)のいずれかを使用します。または、ソースクエリを何らかの方法で書き換えて、行/ページの推定数をのしきい値を超えるようにすることもできますINSERT...SELECT

さらに役立つ情報については、ジェフの自己回答も参照してください。


おそらく興味深いトリビア: 一括読み込みの最適化が使用されていない場合にのみSET STATISTICS IO、ターゲットテーブルの論理読み取りを報告します。


5

私は自分のテストリグで問題を再現することができました。

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

これは、最小限のログが記録される操作を実行する前にソーステーブルの統計を更新することで問題を「修正」しないのではないかという疑問を投げかけます。

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;

2
実際のコードには、SELECTの結果セットを生成する多数の結合を含む複雑なステートメントがありINSERTます。これらの結合は、最終テーブル挿入演算子(悪いUPDATE STATISTICS呼び出しを介して再現スクリプトでシミュレートしました)のカーディナリティの見積もりを低くUPDATE STATISTICSするため、問題を修正するコマンドを発行するほど簡単ではありません。Cardinality Estimatorが理解しやすいようにクエリを簡略化することは良いアプローチかもしれませんが、特定の複雑なビジネスロジックを実装することは簡単ではありません。
Geoff Patterson、

これをテストするためのSQL Server 2014インスタンスはありませんが、SQL Server 2014 の新しいカーディナリティエスティメーターの問題の特定とService Pack 1の改善では、とりわけ、新しいカーディナリティエスティメーターを有効にするためのトレースフラグ4199の有効について説明しています。試しましたか?
Max Vernon

良い考えですが、役に立ちませんでした。私はTF 4199、TF 610(最小限のログ記録条件を緩める)、および両方を試してみましたが(2つ目はなぜですか?)、2番目のテストクエリに変更はありません。
Geoff Patterson

4

推定された行数を増やすために、何らかの方法でソースクエリを書き換えます

Paulの考えを拡張して、本当に絶望的である場合の回避策は、挿入の推定行数がバルクロード最適化の品質に十分な高さになることを保証するダミーテーブルを追加することです。これにより最小限のログが取得され、クエリのパフォーマンスが向上することを確認しました。

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

最終的なポイント

  1. SELECT...INTO最小限のロギングが必要な場合に、1回限りの挿入操作に使用します。Paulが指摘するように、これにより、行の見積もりに関係なく、最小限のロギングが保証されます。
  2. 可能な限り、クエリオプティマイザーが効果的に推論できる単純な方法でクエリを記述します。たとえば、中間テーブルで統計を作成できるようにするために、クエリを複数の部分に分割することが可能です。
  3. SQL Server 2014にアクセスできる場合は、クエリで試してください。実際の運用ケースでは、試してみただけで、新しいCardinality Estimatorははるかに高い(そしてより良い)見積もりを出しました。クエリは最小限に記録されました。ただし、SQL 2012以前をサポートする必要がある場合は、これは役に立たない可能性があります。
  4. あなたが絶望的であるなら、このようなハックな解決策が適用されるかもしれません!

関連記事

Paul Whiteの2019年5月のブログ記事「INSERT…SELECT into Heap Tablesを使用した最小限のロギング」では、この情報の一部について詳しく説明しています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.