Улучшение ужасной производительности MERGE

Недавно я задал вопрос о том, как решить проблему в запросе tsql, который заставил меня использовать оператор MERGE. Это, однако, оказывается проблематичным, поскольку его производительность ужасна.

Как сделать Я получаю вставленный идентификатор строки вместе со связанными данными после вставки без вставки связанных данных

Что мне нужно сделать, так это вставить строки на основе набора результатов и сохранить идентификатор вставленной строки вместе с данными, из которых она получена (см. связанный вопрос).

Я закончил с таким запросом.

DECLARE @temp AS TABLE(
      [action] NVARCHAR(20)
     ,[GlobalId] BIGINT
     ,[Personnumber] NVARCHAR(100)
     ,[Firstname] NVARCHAR(100)
     ,[Lastname] NVARCHAR(100)
);

;WITH person AS
(
    SELECT top 1
        t.[Personnumber]
        ,t.[Firstname]
        ,t.[Lastname]
    FROM [temp].[RawRoles] t
    WHERE t.Personnumber NOT IN 
        (
            SELECT i.Account FROM [security].[Accounts] i
        )
)

MERGE [security].[Identities] AS tar
USING person AS src
ON 0 = 1 -- all rows from src need to be inserted, ive already filtered out using CTE Query.
WHEN NOT MATCHED THEN
   INSERT
   (
     [Created], [Updated]
   )
   VALUES
   (
        GETUTCDATE(), GETUTCDATE()
   )
OUTPUT $action, inserted.GlobalId, src.[Personnumber], src.[Firstname], src.[Lastname]  INTO @temp;


SELECT * FROM @temp

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

Это отлично работает на строках ниже 10 тыс. Но набор данных, с которым я это делаю, близок к 2 миллионам строк. Я выполнял этот запрос около часа без его завершения (в расширенной базе данных Azure премиум-уровня).

Вопрос: Как сделать это быстрее. Можно ли добиться такого же результата без слияния?


person JensB    schedule 04.12.2019    source источник
comment
Почему 3 TOP 1 запроса не ORDER BY..? Что такое @sourceData, поскольку это не определено в вашем SQL.   -  person Larnu    schedule 04.12.2019
comment
что говорит план выполнения, потому что я думаю, что вы будете выполнять полное сканирование таблицы при этом слиянии. Вы улучшите производительность, выполнив обычную вставку и используя предложение вывода INSERT, чтобы получить те же результаты в своей временной таблице.   -  person gbjbaanb    schedule 04.12.2019
comment
@PanagiotisKanavos не совсем так, исходные данные были другими, но это никогда не было проблемой. Проблема в вставке слияния. Весь запрос до слияния занимает 20 секунд (то же самое было и с предыдущим запросом).   -  person JensB    schedule 04.12.2019
comment
Во всяком случае, схема, индексы, план выполнения. Желательно и статистику. Без них невозможно ответить, тем более сейчас, когда запрос не показывает ничего странного. Хотя я бы все равно использовал INSERT SELECT, а не MERGE   -  person Panagiotis Kanavos    schedule 04.12.2019
comment
@JensB это правда. Эти подзапросы TOP 1 требуют индивидуального выполнения в цикле. Упрощение их до одной GROUP BY с MIN/MAX было бы намного быстрее. Ничего странного в вопросе как сейчас нет, так что нельзя сказать почему тормозит   -  person Panagiotis Kanavos    schedule 04.12.2019
comment
При публикации плана запроса, пожалуйста, вставьте план. Это значительно упрощает работу для всех.   -  person Larnu    schedule 04.12.2019
comment
При чем здесь MERGE? Предложение match всегда ложно. Почему бы просто не использовать INSERT INTO Identities (...) OUTPUT inserted..... Select A,B C from RawRoles LEFT JOIN Accounts ON Account=PersonNumber Where Accounts.ID IS NULL ?   -  person Panagiotis Kanavos    schedule 04.12.2019
comment
Примечание: лично я не рекомендую использовать NOT IN с подзапросом. Если какая-либо возвращаемая строка имеет значение NULL, то поведение, которое вы получите, будет не таким, как вы ожидаете. Гораздо лучше использовать NOT EXISTS или LEFT JOIN и тестировать на NULL. DB‹›Fiddle   -  person Larnu    schedule 04.12.2019
comment
@PanagiotisKanavos Возможно, я ошибаюсь, но я думал, что Output может возвращать данные только из таблицы, в которую вы вставили, а не из столбцов, которые не были вставлены. Это то, что я делаю с ВЫВОДОМ, где я выбираю исходные данные, а не только вставленные данные.   -  person JensB    schedule 04.12.2019
comment
Правильно, @JensB. Использование MERGE - это обходной путь для этого; поскольку он может OUTPUT получать данные из входных объектов.   -  person Larnu    schedule 04.12.2019
comment
Это именно то, что мне нужно сделать. Я хочу получить идентификатор вставленной строки, а также данные из исходной строки, данные, которые не будут вставлены (в эту таблицу, но в другую таблицу с FK для этой).   -  person JensB    schedule 04.12.2019


Ответы (2)


На первый взгляд кажется, что MERGE не является причиной ухудшения производительности. Условие слияния всегда ложно (0=1), и вставка (в [security].[Identities]) является единственным возможным путем/путем вперед.

Сколько времени потребуется, чтобы вставить 2 миллиона строк в @temp в обход [security].[Identities] и MERGE ?

DECLARE @temp AS TABLE(
      [action] NVARCHAR(20)
     ,[GlobalId] BIGINT
     ,[Personnumber] NVARCHAR(100)
     ,[Firstname] NVARCHAR(100)
     ,[Lastname] NVARCHAR(100)
);

--is this fast?!?
INSERT INTO @temp(action, GlobalId, Personnumber, Firstname, LastName)
SELECT 'insert', 0, t.[Personnumber], t.[Firstname], t.[Lastname]
FROM [temp].[RawRoles] t
WHERE t.Personnumber NOT IN 
(
    SELECT i.Account FROM [security].[Accounts] i
);

Проверить:

  1. Каков тип данных [temp].[RawRoles].Personnumber? is Personnumber nvarchar(100)?
    Нужно ли хранить иностранные символы в номере человека? Nchar в два раза больше char. varchar/char может быть лучшим выбором, если у вас есть буквенно-цифровые (обычные латинские символы) или цифры с ведущими нулями. Если ваши требования могут быть выполнены с числовым типом данных, то предпочтительнее будет int/bigint/decimal.

  2. Есть ли индекс для [temp].[RawRoles].Personnumber? Без индекса проверка существования должна была бы сортировать [temp].[RawRoles].Personnumber или хешировать его. Это может быть дополнительными затратами на пропускную способность ресурса/dtu. Кластерный индекс для [temp].RawRoles может быть наиболее полезным, учитывая, что большинство temp.RawRoles будут окончательно обработаны/вставлены.

  3. Каков тип данных [security].[Accounts].Account? Есть ли индекс в столбце? Два столбца [security].[Accounts].Account и [temp].[RawRoles].Personnumber должны иметь один и тот же тип данных и в идеале иметь индекс для обоих. Если [security].[Accounts] является конечным пунктом назначения обработанного [temp].[RawRoles], тогда таблица может содержать миллионы строк, и для любой будущей обработки требуется индекс столбца Account. Недостатком индекса является более медленная вставка. Если 2 миллиона — это самый первый объем/данные, было бы лучше не иметь индекса для учетной записи при вставке «массы» в security.Accounts (но создать его позже).

Подводить итоги:

--contemplate&decide whether a change of the Account datatype is needed. (a datatype change can have many implications, for applications using the db)

--change the data type of Personnumber to the datatype of Account(security.Accounts)
ALTER TABLE temp.RawRoles ALTER COLUMN Personnumber "datatype of security.Accounts.Account" NOT NULL; -- rows having undefined Personnumber?

--clustered index Personnumber
CREATE /*UNIQUE*/ CLUSTERED INDEX uclxPersonnumber ON temp.RawRoles(Personnumber); --unique preferred, if possible

--index on account (not needed[?] when security.Accounts is empty)
CREATE INDEX idxAccount ON [security].Accounts(Account);


--baseline, how fast can we do a straight forward insertion of 2 million rows?
DECLARE @tempbaseline AS TABLE(
      [action] NVARCHAR(20)
     ,[GlobalId] BIGINT
     ,[Personnumber] NVARCHAR(100) --ignore this for now
     ,[Firstname] NVARCHAR(100)
     ,[Lastname] NVARCHAR(100)
);

INSERT INTO @tempbaseline([action], GlobalId, Personnumber, Firstname, LastName)
SELECT 'INSERT', 0, t.[Personnumber], t.[Firstname], t.[Lastname]
FROM [temp].[RawRoles] t
    WHERE NOT EXISTS (SELECT * FROM [security].[Accounts] i WHERE i.Account = t.Personnumber)    
--if the execution time (baseline) is acceptable, proceed with the merge code
--"merge with output into" should be be "slightly"/s slower than the baseline.
--if the baseline is not acceptable (simple insertion takes too much time) then merge is futile

/*
DECLARE @temp....


MERGE [security].[Identities] AS tar
USING 
(
    SELECT --top 1
        t.[Personnumber]
        ,t.[Firstname]
        ,t.[Lastname]
    FROM [temp].[RawRoles] t
    WHERE NOT EXISTS (SELECT * FROM [security].[Accounts] i WHERE i.Account = t.Personnumber)
) AS src
ON 0 = 1 -- all rows from src need to be inserted, ive already filtered out in the USING Query.
WHEN NOT MATCHED THEN
   INSERT
   (
     [Created], [Updated]
   )
   VALUES
   (
        GETUTCDATE(), GETUTCDATE()
   )
OUTPUT 'INSERT' /** only insert is possible $action */, inserted.GlobalId, src.[Personnumber], src.[Firstname], src.[Lastname]  INTO @temp;   


--delete the index on Account (the process will insert 2mil)
DROP INDEX idxAccount ON [security].Accounts --review and create this index after the bulk of accounts is inserted.

...your process

*/
person lptr    schedule 04.12.2019

Мне кажется, что ваша таблица Identity просто используется в качестве генератора последовательности, потому что вы не вставляете в нее ничего, кроме временных меток. Рассматривали ли вы возможность использования ПОСЛЕДОВАТЕЛЬНОСТИ вместо таблицы для генерации ключей? Использование последовательности могло бы устранить этот процесс, потому что вы могли бы генерировать ключ всякий раз, когда он был необходим.

Вывод миллионов строк в табличную переменную вряд ли возможен. Табличные переменные обычно хороши для нескольких тысяч строк.

INSERT INTO security.Accounts (GlobalId, Account, Firstname, Lastname)
SELECT NEXT VALUE FOR AccountSeq, r.Personnumber, r.Firstname, r.Lastname
FROM temp.RawRoles AS r
LEFT JOIN security.Accounts AS a ON r.Personnumber = a.Account
WHERE a.Personnumber IS NULL;

INSERT INTO security.identities (GlobalId, Created, Updated)
SELECT a.GlobalId, GETUTCDATE() AS Created, GETUTCDATE() AS Updated
FROM security.Accounts AS a
LEFT JOIN security.identities AS i ON a.GlobalId = i.GlobalId
WHERE i.GlobalId IS NULL;
person nvogel    schedule 04.12.2019
comment
Хорошая мысль. Мне было интересно, могу ли я сэкономить время, используя вместо этого GUID и создав этот встроенный код без предварительной вставки в эту таблицу. - person JensB; 04.12.2019