Погана оцінка кардинальності дискваліфікує INSERT від мінімальної реєстрації?


11

Чому друге INSERTтвердження ~ 5x повільніше першого?

З огляду на кількість створених даних журналу, я думаю, що другий не відповідає мінімальному журналу. Однак документація в Посібнику з продуктивності завантаження даних вказує на те, що обидві вставки повинні бути спроможні мінімально реєструватися. Отже, якщо мінімальна реєстрація є ключовою різницею продуктивності, чому так, що другий запит не відповідає мінімальному журналу? Що можна зробити, щоб покращити ситуацію?


Запит №1: Вставка рядків 5 ММ ​​за допомогою INSERT ... WITH (TABLOCK)

Розглянемо наступний запит, який вставляє 5 мм рядків у купу. Цей запит виконує 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


Повний сценарій

Дивіться в цій пастебі повний набір сценаріїв для генерування тестових даних та виконання будь-якого з цих сценаріїв. Зауважте, що ви повинні використовувати базу даних, яка є у SIMPLE моделі відновлення .


Бізнес-контекст

Ми напівчасто пересуваємося мільйонами рядків даних, і важливо, щоб ці операції були максимально ефективними, як з точки зору часу виконання, так і з навантаження дискового вводу / виводу. Спочатку у нас було враження, що створення таблиці куч і використання INSERT...WITH (TABLOCK)- це хороший спосіб зробити це, але тепер стали менш впевненими, враховуючи, що ми спостерігали ситуацію, продемонстровану вище, у фактичному виробничому сценарії (хоча і зі складнішими запитами, а не тут спрощена версія).

Відповіді:


7

Чому так, що другий запит не відповідає мінімальній реєстрації?

Для другого запиту доступний мінімальний журнал , але двигун вирішує не використовувати його під час виконання.

Існує мінімальний поріг для 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. Ці об'єднання дають погані оцінки кардинальності для оператора вставки кінцевої таблиці (який я імітував у сценарії repro через поганий UPDATE STATISTICSвиклик), і тому це не так просто, як видавати UPDATE STATISTICSкоманду для усунення проблеми. Я повністю погоджуюся, що спрощення запиту таким чином, щоб Оцінювач кардинальності легше зрозумів, може бути хорошим підходом, але реалізація заданої складної бізнес-логіки не є дрібницею.
Джефф Паттерсон

У мене немає екземпляра SQL Server 2014, який би перевіряв це, проте Визначення проблем SQL Server 2014 New Cardinality Estimator і розширення пакета оновлень 1 для розмови про те, щоб увімкнути прапор сліду 4199, серед інших, увімкнути новий оцінювач кардинальності. Ви пробували це?
Макс Вернон,

Гарна ідея, але це не допомогло. Я просто спробував TF 4199, TF 610 (послаблює мінімальні умови ведення журналу), і обидва разом (ей, чому б і ні?), Але жодних змін для 2-го тестового запиту.
Джефф Паттерсон

4

Перепишіть вихідний запит, щоб збільшити оцінену кількість рядків

Розширюючи ідею Павла, вирішення проблеми, якщо ви справді відчайдушні, - це додати макетну таблицю, яка гарантує, що орієнтовна кількість рядків для вставки буде достатньо високою для якості для оптимізації масового завантаження. Я підтвердив, що це забезпечує мінімальну реєстрацію та покращує продуктивність запитів.

-- 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для операцій одноразової вставки, якщо потрібен мінімальний журнал. Як зазначає Пол, це забезпечить мінімальну реєстрацію журналу незалежно від оцінки рядків
  2. По можливості, пишіть запити простим чином, щоб оптимізатор запитів міркував ефективно. Наприклад, можна розбити запит на кілька фрагментів, наприклад, щоб дозволити побудову статистики на проміжній таблиці.
  3. Якщо у вас є доступ до SQL Server 2014, спробуйте це у своєму запиті; в моєму фактичному виробничому випадку я просто спробував це, і новий Оцінювач кардинальності дав набагато більшу (і кращу) оцінку; тоді запит був мінімально введений. Але це може не бути корисним, якщо вам потрібно підтримувати SQL 2012 та новіші версії.
  4. Якщо ви відчайдушно, може бути застосовано подібні рішення, як це!

Пов'язана стаття

Повідомлення в блозі Пола Уайта в травні 2019 року Мінімальний журнал за допомогою INSERT… SELECT в Heap Tables детальніше висвітлює частину цієї інформації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.