Производительность Sql Server Delete и Merge

У меня есть таблица, содержащая некоторые данные о покупке/продаже, в ней около 8 миллионов записей:

CREATE TABLE [dbo].[Transactions](
[id] [int] IDENTITY(1,1) NOT NULL,
[itemId] [bigint] NOT NULL,
[dt] [datetime] NOT NULL,
[count] [int] NOT NULL,
[price] [float] NOT NULL,
[platform] [char](1) NOT NULL
) ON [PRIMARY]

Каждые X минут моя программа получает новые транзакции для каждого itemId, и мне нужно его обновить. Мое первое решение - это два шага DELETE+INSERT:

delete from Transactions where platform=@platform and itemid=@itemid
insert into Transactions (platform,itemid,dt,count,price) values (@platform,@itemid,@dt,@count,@price)
[...]
insert into Transactions (platform,itemid,dt,count,price) values (@platform,@itemid,@dt,@count,@price)

Проблема в том, что этот оператор DELETE занимает в среднем 5 секунд. Это слишком долго.

Второе решение, которое я нашел, — использовать MERGE. Я создал такую ​​хранимую процедуру, которая принимает табличный параметр:

CREATE PROCEDURE [dbo].[sp_updateTransactions]
@Table dbo.tp_Transactions readonly,
@itemId bigint,
@platform char(1)
AS
BEGIN
MERGE Transactions AS TARGET
USING @Table AS SOURCE  
ON (    
TARGET.[itemId] = SOURCE.[itemId] AND
TARGET.[platform] = SOURCE.[platform] AND 
TARGET.[dt] = SOURCE.[dt] AND 
TARGET.[count] = SOURCE.[count] AND
TARGET.[price] = SOURCE.[price] ) 


WHEN NOT MATCHED BY TARGET THEN 
INSERT VALUES (SOURCE.[itemId], 
                SOURCE.[dt],
                SOURCE.[count],
                SOURCE.[price],
                SOURCE.[platform])

WHEN NOT MATCHED BY SOURCE AND TARGET.[itemId] = @itemId AND TARGET.[platform] = @platform THEN 
DELETE;

END

Эта процедура занимает около 7 секунд для таблицы с 70 тыс. записей. Так что с 8M это, вероятно, займет несколько минут. Узким местом является «Когда не совпадает» — когда я прокомментировал эту строку, эта процедура выполняется в среднем 0,01 секунды.

Итак, вопрос: как улучшить производительность оператора удаления?

Удалить необходимо, чтобы убедиться, что эта таблица не содержит транзакцию, которая была удалена в приложении. Но в реальности это случается очень редко, т.к. истинная потребность в удалении записей составляет менее 1 на 10000 обновлений транзакций.

Мой теоретический обходной путь состоит в том, чтобы создать дополнительный столбец, такой как «бит транзакцииУдаленный», и использовать UPDATE вместо DELETE, а затем выполнять очистку таблицы пакетным заданием каждые X минут или часов и выполнять

delete from transactions where transactionDeleted=1

Это должно быть быстрее, но мне нужно будет обновить все операторы SELECT в других частях приложения, чтобы использовать только записи transactionDeleted=0, и поэтому это также может повлиять на производительность приложения.

Знаете ли вы какое-нибудь лучшее решение?

ОБНОВЛЕНИЕ: Текущие индексы:

CREATE NONCLUSTERED INDEX [IX1] ON [dbo].[Transactions] 
(
[platform] ASC,
[ItemId] ASC
) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF,   IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON, FILLFACTOR = 50) ON [PRIMARY]


CONSTRAINT [IX2] UNIQUE NONCLUSTERED 
(
[ItemId] DESC,
[count] ASC,
[dt] DESC,
[platform] ASC,
[price] ASC
) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

person adek    schedule 02.10.2011    source источник
comment
около 7 секунд с таблицей с 70 тысячами записей - звучит не особенно быстро. Какие индексы у вас есть? Какая подсистема ввода-вывода?   -  person Mitch Wheat    schedule 02.10.2011
comment
@MitchWheat Я обновил вопрос с индексами. Подсистема ввода-вывода - есть 2 ssd-накопителя, если это то, о чем вы спрашиваете. Я перестраиваю индексы каждую ночь.   -  person adek    schedule 02.10.2011
comment
да. Сервер базы данных — SQL Server 2008 R2. База данных работает в простой модели восстановления. Файл данных — 8,1 ГБ, файл журнала — 133 МБ.   -  person adek    schedule 02.10.2011
comment
В какой таблице 70 тысяч строк? ТВП? Если нет, то сколько строк обычно в TVP? Кроме того, какое when not matched вы называете узким местом? Есть два..   -  person Martin Smith    schedule 02.10.2011
comment
@MartinSmith Цифры, которые я написал, относятся к таблице транзакций. DELETE/INSERT Я тестировал таблицу с 8M записями. MERGE Я протестировал таблицу до 180 тыс. записей, и результат был настолько плохим, что я остановился. ТВП в основном 1-10 записей. Реально редко бывает до 100 записей.   -  person adek    schedule 02.10.2011
comment
@MartinSmith, КОГДА НЕ СООТВЕТСТВУЕТ ИСТОЧНИКУ, является узким местом   -  person adek    schedule 02.10.2011
comment
@adek - Вы можете попробовать переписать его, чтобы цель отфильтровывалась раньше. см. этот ответ для примера, если это не помогает, опубликуйте свой план выполнения.   -  person Martin Smith    schedule 02.10.2011
comment
Является ли столбец ID внешним ключом в некоторых других таблицах?   -  person Mikael Eriksson    schedule 03.10.2011
comment
Вместо того, чтобы изменять весь код, чтобы выбрать только WHERE transactionDeleted=0, вы можете попытаться создать представление с тем же условием. Вам все еще нужно изменить код, но теперь выбрать из нового созданного представления. Изменения в будущем могут иметь меньшее влияние на приложения.   -  person Ruud van de Beeten    schedule 25.08.2012


Ответы (3)


Хорошо, вот еще один подход. Для аналогичной проблемы (большое сканирование, ЕСЛИ НЕ СООТВЕТСТВУЕТ ИСТОЧНИК, затем УДАЛИТЬ) я уменьшил время выполнения MERGE с 806 мс до 6 мс!

Одна из проблем, связанных с описанной выше проблемой, заключается в том, что предложение «WHEN NOT MATCHED BY SOURCE» сканирует всю таблицу TARGET.

Это не так очевидно, но Microsoft позволяет фильтровать таблицу TARGET (с помощью CTE) ПЕРЕД выполнением слияния. Таким образом, в моем случае количество строк TARGET было уменьшено с 250 000 до менее 10 строк. Большая разница.

Если предположить, что описанная выше проблема работает с ЦЕЛЬЮ, отфильтрованной @itemid и @platform, тогда код MERGE будет выглядеть следующим образом. Приведенные выше изменения в индексах также помогут этой логике.

WITH Transactions_CTE (itemId
                        ,dt
                        ,count
                        ,price
                        ,platform
                        )
AS
-- Define the CTE query that will reduce the size of the TARGET table.  
(  
    SELECT itemId
        ,dt
        ,count
        ,price
        ,platform
    FROM Transactions  
    WHERE itemId = @itemId
      AND platform = @platform  
)  
MERGE Transactions_CTE AS TARGET
USING @Table AS SOURCE
    ON (
        TARGET.[itemId] = SOURCE.[itemId]
        AND TARGET.[platform] = SOURCE.[platform]
        AND TARGET.[dt] = SOURCE.[dt]
        AND TARGET.[count] = SOURCE.[count]
        AND TARGET.[price] = SOURCE.[price]
        )
WHEN NOT MATCHED BY TARGET  THEN
        INSERT
        VALUES (
            SOURCE.[itemId]
            ,SOURCE.[dt]
            ,SOURCE.[count]
            ,SOURCE.[price]
            ,SOURCE.[platform]
            )
WHEN NOT MATCHED BY SOURCE THEN
        DELETE;
person David Coster    schedule 23.12.2016
comment
Это серьезный код ниндзя. Я никогда не знал, что вы можете использовать CTE в качестве цели MERGE. Вы только что сократили мое слияние с более чем двух минут до примерно 3 секунд — ура! - person Pete; 05.07.2017
comment
Да, это работает. Использование отфильтрованного CTE в качестве целевой таблицы в операторе MERGE помогает избежать операции сканирования кластеризованного индекса в целевой таблице, когда используется NOT MATCHED BY SOURCE THEN DELETE. - person Boogier; 19.02.2019
comment
Я уверен, что все забыли об этом, но я только что наткнулся на это, и что меня смущает: вы можете предоставить предикат с AND на WHEN NOT MATCHED BY SOURCE. Но почему-то это не решает проблему с производительностью, как это делает CTE. Почему? У них одно и то же значение — фильтровать мое таргет-пространство. Я что-то упустил или SQL-сервер просто... плохо справляется с этим? - person Pxtl; 26.03.2021

Использование поля BIT для IsDeleted (или IsActive, как делают многие люди) допустимо, но требует изменения всего кода, а также создания отдельного задания SQL для периодического прохождения и удаления «удаленных» записей. Это может быть путь, но есть что-то менее навязчивое, чтобы попробовать сначала.

Я заметил в вашем наборе из двух индексов, что ни один из них не является CLUSTERED. Можно предположить, что поле IDENTITY есть? Вы можете рассмотреть возможность сделать индекс [IX2] UNIQUE CLUSTERED и изменить PK (опять же, я предполагаю, что поле IDENTITY является CLUSTERED PK) на NONCLUSTERED. Я бы также переупорядочил поля IX2, чтобы сначала поставить [Platform] и [ItemID]. Поскольку ваша основная операция ищет [Platform] и [ItemID] как набор, может помочь их физическое упорядочивание таким образом. И поскольку этот индекс уникален, он является хорошим кандидатом на КЛАСТЕРИЗАЦИЯ. Это, безусловно, стоит протестировать, так как это повлияет на все запросы к таблице.

Кроме того, если изменение индексов, как я предложил, помогает, все же, возможно, стоит попробовать обе идеи и, следовательно, также использовать поле IsDeleted, чтобы увидеть, повысит ли это производительность еще больше.

РЕДАКТИРОВАТЬ: я забыл упомянуть, сделав индекс IX2 CLUSTERED и переместив поле [Platform] наверх, вы должны избавиться от индекса IX1.

РЕДАКТИРОВАТЬ2:

Просто чтобы быть очень ясным, я предлагаю что-то вроде:

CREATE UNIQUE CLUSTERED  INDEX [IX2]
(
[ItemId] DESC,
[platform] ASC,
[count] ASC,
[dt] DESC,
[price] ASC
)

И, честно говоря, изменение того, какой индекс является CLUSTERED, также может негативно повлиять на запросы, в которых JOIN выполняются в поле [id], поэтому вам необходимо тщательно протестировать. В конце концов, вам нужно настроить систему для ваших наиболее частых и/или дорогостоящих запросов, и, возможно, вам придется смириться с тем, что некоторые запросы в результате будут выполняться медленнее, но это может стоить того, чтобы эта операция была намного быстрее.

person Solomon Rutzky    schedule 02.10.2011
comment
Изменение индексов, как вы написали, значительно повышает производительность и значительно упрощает план выполнения. Тестирую, скоро напишу результаты :) - person adek; 02.10.2011

См. это https://stackoverflow.com/questions/3685141/how-to-improve-performance-when-deleting-entities-from-database/3685275#3685275

будет ли обновление стоить столько же, сколько и удаление? Нет. Обновление было бы намного более легкой операцией, особенно если бы у вас был индекс в ПК (э-э-э, это guid, а не int). Дело в том, что обновление битового поля обходится гораздо дешевле. (Массовое) удаление приведет к перетасовке данных.

В свете этой информации ваша идея использовать битовое поле очень актуальна.

person Ali Khalid    schedule 02.10.2011