Perkiraan kardinalitas yang buruk mendiskualifikasi INSERT dari penebangan minimal?

11

Mengapa INSERTpernyataan kedua ~ 5x lebih lambat dari yang pertama?

Dari jumlah data log yang dihasilkan, saya pikir yang kedua tidak memenuhi syarat untuk minimal logging. Namun, dokumentasi dalam Panduan Kinerja Pemuatan Data menunjukkan bahwa kedua sisipan harus dapat dicatat secara minimal. Jadi jika logging minimum adalah perbedaan kinerja utama, mengapa permintaan kedua tidak memenuhi syarat untuk logging minimal? Apa yang bisa dilakukan untuk memperbaiki situasi?


Kueri # 1: Memasukkan baris 5MM menggunakan INSERT ... WITH (TABLOCK)

Pertimbangkan kueri berikut, yang menyisipkan baris 5MM ke tumpukan. Kueri ini dieksekusi di 1 seconddan menghasilkan 64MBdata log transaksi seperti yang dilaporkan oleh 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


Kueri # 2: Memasukkan data yang sama, tetapi SQL meremehkan # baris

Sekarang pertimbangkan permintaan yang sangat mirip ini, yang beroperasi pada data yang persis sama tetapi kebetulan mengambil dari tabel (atau SELECTpernyataan kompleks dengan banyak bergabung dalam kasus produksi aktual saya) di mana perkiraan kardinalitas terlalu rendah. Kueri ini dijalankan di 5.5 secondsdan menghasilkan 461MBdata log transaksi.

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


Skrip lengkap

Lihat Pastebin ini untuk set lengkap skrip untuk menghasilkan data pengujian dan menjalankan salah satu skenario ini. Perhatikan bahwa Anda harus menggunakan database yang ada dalam SIMPLE model pemulihan .


Konteks bisnis

Kami setengah sering berpindah-pindah di sekitar jutaan baris data, dan penting agar operasi ini seefisien mungkin, baik dalam hal waktu eksekusi maupun beban I / O disk. Kami awalnya mendapat kesan bahwa membuat heap table dan menggunakan INSERT...WITH (TABLOCK)adalah cara yang baik untuk melakukan ini, tetapi sekarang menjadi kurang percaya diri mengingat kami mengamati situasi yang ditunjukkan di atas dalam skenario produksi aktual (walaupun dengan pertanyaan yang lebih kompleks, bukan versi yang disederhanakan di sini).

Geoff Patterson
sumber

Jawaban:

7

Mengapa kueri kedua tidak memenuhi syarat untuk pencatatan minimum?

Pencatatan minimum tersedia untuk kueri kedua, tetapi mesin memilih untuk tidak menggunakannya saat runtime.

Ada batas minimum untuk di INSERT...SELECTbawah ini yang ia pilih untuk tidak menggunakan optimisasi beban curah. Ada biaya yang diperlukan untuk menyiapkan operasi rowset massal, dan menyisipkan hanya beberapa baris secara massal tidak akan menghasilkan pemanfaatan ruang yang efisien.

Apa yang bisa dilakukan untuk memperbaiki situasi?

Gunakan salah satu dari banyak metode lain (misalnya SELECT INTO) yang tidak memiliki ambang ini. Atau, Anda mungkin dapat menulis ulang kueri sumber dengan beberapa cara untuk meningkatkan taksiran jumlah baris / halaman di atas ambang batas INSERT...SELECT.

Lihat juga jawaban-sendiri Geoff untuk informasi yang lebih berguna.


Kemungkinan menarik: SET STATISTICS IO melaporkan pembacaan logis untuk tabel target hanya ketika optimasi pemuatan massal tidak digunakan .

Paul White 9
sumber
5

Saya dapat membuat kembali masalah dengan rig pengujian saya sendiri:

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;

Ini menimbulkan pertanyaan, mengapa tidak "memperbaiki" masalah dengan memperbarui statistik pada tabel sumber sebelum menjalankan operasi dengan log minimal?

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;
Max Vernon
sumber
2
Di kode asli, ada SELECTpernyataan kompleks dengan banyak gabungan yang menghasilkan hasil yang ditetapkan untuk INSERT. Gabungan ini menghasilkan perkiraan kardinalitas yang buruk untuk operator penyisipan tabel akhir (yang telah saya simulasikan dalam skrip repro melalui UPDATE STATISTICSpanggilan buruk ), dan karena itu tidak sesederhana mengeluarkan UPDATE STATISTICSperintah untuk memperbaiki masalah. Saya sepenuhnya setuju bahwa menyederhanakan kueri sehingga lebih mudah bagi Penaksir Kardinalitas untuk memahami mungkin merupakan pendekatan yang baik, tetapi itu bukan trival untuk menerapkan logika bisnis yang rumit.
Geoff Patterson
Saya tidak memiliki contoh SQL Server 2014 untuk menguji ini, namun Mengidentifikasi masalah Penaksir Kardinalitas Baru SQL Server 2014 dan pembicaraan peningkatan Service Pack 1 tentang mengaktifkan jejak bendera 4199, antara lain, untuk memungkinkan penduga kardinalitas baru. Sudahkah Anda mencobanya?
Max Vernon
Ide bagus, tapi itu tidak membantu. Saya baru saja mencoba TF 4199, TF 610 (mengendurkan kondisi minimum logging), dan keduanya bersamaan (hei, mengapa tidak?), Tetapi tidak ada perubahan untuk kueri pengujian ke-2.
Geoff Patterson
4

Tulis ulang kueri sumber dengan cara tertentu untuk meningkatkan taksiran jumlah baris

Memperluas gagasan Paul, solusi jika Anda benar-benar putus asa adalah dengan menambahkan tabel dummy yang menjamin bahwa perkiraan jumlah baris untuk insert akan cukup tinggi untuk kualitas untuk optimalisasi pemuatan massal. Saya mengonfirmasi bahwa ini mendapat pencatatan minimum dan meningkatkan kinerja kueri.

-- 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)

Takeaways terakhir

  1. Gunakan SELECT...INTOuntuk operasi penyisipan satu kali jika diperlukan penebangan minimal. Seperti yang ditunjukkan Paul, ini akan memastikan penebangan minimal terlepas dari estimasi baris
  2. Jika memungkinkan, tulis kueri dengan cara sederhana yang dapat dipikirkan pengoptimal kueri secara efektif. Dimungkinkan untuk memecah kueri menjadi beberapa bagian, misalnya, untuk memungkinkan statistik dibangun di atas tabel perantara.
  3. Jika Anda memiliki akses ke SQL Server 2014, cobalah di kueri Anda; dalam kasus produksi saya yang sebenarnya, saya hanya mencobanya dan Pengukur Kardinalitas yang baru menghasilkan perkiraan yang jauh lebih tinggi (dan lebih baik); kueri kemudian minimal dicatat. Tetapi ini mungkin tidak membantu jika Anda perlu mendukung SQL 2012 dan yang lebih lama.
  4. Jika Anda putus asa, solusi hacky seperti ini mungkin berlaku!

Artikel terkait

Posting blog Paul White Mei 2019 Minimal Masuk dengan INSERT… PILIH ke Heap Tables mencakup beberapa informasi ini secara lebih rinci.

Geoff Patterson
sumber