Плохая оценка кардинальности дисквалифицирует INSERT от минимальной регистрации?

11

Почему второе INSERTутверждение примерно в 5 раз медленнее первого?

Исходя из объема сгенерированных данных журнала, я думаю, что второе не соответствует минимальному уровню ведения журнала. Тем не менее, документация в Руководстве по производительности при загрузке данных указывает на то, что обе вставки должны быть минимально зарегистрированы. Итак, если минимальное ведение журнала является ключевым отличием производительности, почему второй запрос не соответствует минимальному ведению журнала? Что можно сделать, чтобы улучшить ситуацию?


Запрос № 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


Полный сценарий

Смотрите этот Pastebin для полного набора сценариев для генерации тестовых данных и выполнения любого из этих сценариев. Обратите внимание, что вы должны использовать базу данных, которая находится в SIMPLE модели восстановления .


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

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

Джефф Паттерсон
источник

Ответы:

7

Почему второй запрос не подходит для минимальной регистрации?

Минимальное ведение журнала доступно для второго запроса, но движок решает не использовать его во время выполнения.

Существует минимальный порог для INSERT...SELECTниже которого он выбирает не использовать оптимизацию объемной нагрузки. Установка массового набора строк требует больших затрат, и массовая вставка только нескольких строк не приведет к эффективному использованию пространства.

Что можно сделать, чтобы улучшить ситуацию?

Используйте один из многих других методов (например SELECT INTO), который не имеет этого порога. Кроме того, вы можете переписать исходный запрос каким-либо образом, чтобы увеличить предполагаемое количество строк / страниц сверх порогового значения для INSERT...SELECT.

Смотрите также ответ Джеффа для получения более полезной информации.


Возможно, интересные мелочи: SET STATISTICS IO сообщает о логических чтениях для целевой таблицы, только если оптимизация массовой загрузки не используется .

Пол Уайт 9
источник
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 о новых проблемах Оценщика мощности и об улучшении пакета обновления 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 в таблицы кучи» более подробно описывается эта информация.

Джефф Паттерсон
источник