잘못된 카디널리티 추정은 INSERT를 최소 로깅에서 제외시킵니다.


11

두 번째 INSERT문장 이 왜 첫 번째 문장보다 ~ 5 배 느린가요?

생성 된 로그 데이터 양에서 두 번째는 최소 로깅에 적합하지 않다고 생각합니다. 그러나 Data Loading Performance Guide 의 문서에는 두 삽입물 모두를 최소한으로 기록 할 수 있어야합니다. 최소 로깅이 주요 성능 차이라면 왜 두 번째 쿼리가 최소 로깅에 적합하지 않습니까? 상황을 개선하기 위해 무엇을 할 수 있습니까?


쿼리 # 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

두 번째 쿼리가 최소 로깅에 적합하지 않은 이유는 무엇입니까?

두 번째 쿼리에는 최소 로깅을 사용할 수 있지만 엔진은 런타임시이를 사용하지 않기로 선택합니다.

벌크로드 최적화를 사용하지 않도록 선택 하는 최소 임계 값INSERT...SELECT있습니다. 대량 행 집합 작업을 설정하는 데 드는 비용이 있으며 몇 개의 행만 대량 삽입하면 공간 활용이 효율적이지 않습니다.

상황을 개선하기 위해 무엇을 할 수 있습니까?

SELECT INTO이 임계 값이없는 다른 많은 방법 (예 :) 중 하나를 사용하십시오 . 또는에 대한 임계 값을 초과하여 예상 행 / 페이지 수를 늘리기 위해 소스 쿼리를 다시 작성할 수 있습니다 INSERT...SELECT.

보다 유용한 정보 는 Geoff의 자체 답변 을 참조하십시오.


아마도 흥미로운 퀴즈 : 대량로드 최적화가 사용되지 않는 경우에만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호출을 통해 repro 스크립트에서 시뮬레이션)에 대한 카디널리티 추정치가 좋지 않으므로 UPDATE STATISTICS문제를 해결하기 위해 명령을 실행하는 것만 큼 간단하지 않습니다 . Cardinality Estimator가 이해하기 쉽도록 쿼리를 단순화하는 것이 좋은 접근 방법이지만, 복잡한 비즈니스 로직을 구현하는 것은 쉬운 일이 아니라는 점에 동의합니다.
Geoff Patterson

SQL Server 2014 인스턴스에서이를 테스트 할 수는 없지만 SQL Server 2014 새 카디널리티 추정기 문제 식별 및 서비스 팩 1 개선 사항 에 따라 추적 플래그 4199를 활성화하여 새 카디널리티 추정기를 활성화하는 방법에 대해 설명합니다. 당신은 그것을 시도 했습니까?
Max Vernon

좋은 생각이지만 도움이되지 않았습니다. 방금 TF 4199, TF 610 (최소 로깅 조건) 및 두 가지를 함께 시도했지만 두 번째 테스트 쿼리에는 아무런 변화가 없었습니다.
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최소한의 로깅이 필요한 경우 일회성 삽입 작업에 사용하십시오 . Paul이 지적한 것처럼 행 추정치에 관계없이 최소 로깅을 보장합니다.
  2. 가능하면 쿼리 최적화 프로그램이 효과적으로 추론 할 수있는 간단한 방법으로 쿼리를 작성하십시오. 예를 들어, 통계가 중간 테이블에 빌드되도록하기 위해 쿼리를 여러 조각으로 나눌 수 있습니다.
  3. SQL Server 2014에 액세스 할 수 있으면 쿼리에서 사용해보십시오. 실제 생산 사례에서는 방금 시도해 보았고 새로운 카디널리티 추정기는 훨씬 더 높은 (및 더 나은) 추정치를 산출했습니다. 그런 다음 쿼리가 최소한으로 기록되었습니다. 그러나 SQL 2012 및 이전 버전을 지원해야하는 경우에는 도움이되지 않을 수 있습니다.
  4. 필사적이라면 이와 같은 해키 솔루션이 적용될 수 있습니다!

관련 기사

Paul White의 2019 년 5 월 블로그 게시물 INSERT… SELECT를 사용하여 최소 로깅으로 힙 테이블에 선택하면 이 정보 중 일부에 대해 자세히 설명합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.