Обновление и вставка при совпадении условия в TSQL-Merge

Я пытался написать хранимую процедуру, в которой я могу выполнить UpSert, используя Merge со следующим условием

  1. Если запись присутствует, измените EndDate цели на вчерашний день, т. е. текущий день - 1.

  2. Если запись отсутствует, вставьте новую запись

Вот таблица tblEmployee, которую я использовал в SP

CREATE TABLE tblEmployee
(
    [EmployeeID] [int] IDENTITY(1,1) NOT NULL, 
    [Name] [varchar](10) NOT NULL,  
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NOT NULL
)

Вот мой SP, который принимает UDTT в качестве входного параметра

CREATE PROCEDURE [dbo].[usp_UpsertEmployees]
@typeEmployee typeEmployee READONLY -- It has same column like tblEmployye except EmployeeID
AS
BEGIN
    SET NOCOUNT ON;      

    MERGE INTO tblEmployee AS TARGET
    USING @typeEmployee AS SOURCE
    ON TARGET.Name = SOURCE.Name 

    WHEN MATCHED and TARGET.StartDate < SOURCE.StartDate
    THEN 

            --First Update Existing Record EndDate to Previous Date as shown below 
            UPDATE 
            set TARGET.EndDate = DATEADD(day, -1, convert(date, SOURCE.StartDate))

            -- Now Insert New Record 
            --INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate);

    WHEN NOT MATCHED by TARGET 
    THEN
            INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate);

    SET NOCOUNT OFF;        
END

Как я могу выполнить как обновление существующей записи, так и добавление новой записи при совпадении столбца

Может ли кто-нибудь объяснить мне поток выполнения слияния в TSQL, т.е.

    WHEN MATCHED --Will this Execute Everytime

    WHEN NOT MATCHED by TARGET -- Will this Execute Everytime

    WHEN NOT MATCHED by SOURCE -- Will this Execute Everytime

Будут ли все вышеперечисленные 3 условия выполняться каждый раз при слиянии или только условие соответствия выполняется каждый раз

Заранее спасибо


person Amar    schedule 12.07.2016    source источник
comment
Получение ошибки компиляции Неверный синтаксис рядом с «Начало». Ожидается DELETE, INSERT или UPDATE.   -  person Amar    schedule 12.07.2016
comment
ну видимо ошибся...   -  person Zohar Peled    schedule 12.07.2016


Ответы (2)


Это не то, для чего предназначено MERGE (обновление и вставка в том же предложении). Для этого вы можете использовать предложение OUTPUT, чтобы получить только все обновленные записи. Комбинация MERGE/OUTPUT очень разборчива. Ваши обновления OUTPUT на самом деле являются целевыми записями, которые были обновлены, поэтому вам нужно запустить целевые записи в переменной temp/table. Затем вы сопоставляете их с ИСТОЧНИКОМ, чтобы выполнить ВСТАВКУ. Вам не разрешат объединять выходные результаты непосредственно с источником или даже использовать в качестве коррелированного подзапроса в WHERE.

Настройте некоторые образцы данных

Приведенный ниже код просто устанавливает некоторые примеры данных.

-- Setup sample data
DECLARE @typeEmployee TABLE (
    [Name] [varchar](10) NOT NULL,  
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NOT NULL
)
DECLARE @tblEmployee TABLE (
    [EmployeeID] [int] IDENTITY(1,1) NOT NULL, 
    [Name] [varchar](10) NOT NULL,  
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NOT NULL   
)
INSERT @tblEmployee VALUES ('Emp A', '1/1/2016', '2/1/2016')
INSERT @typeEmployee VALUES ('Emp A', '1/5/2016', '2/2/2016'), ('Emp B', '3/1/2016', '4/1/2016')

Обновления хранимой процедуры

Вы можете использовать OUTPUT в конце MERGE, чтобы он возвращал измененные записи целевых записей, и, включив $action, вы также получите, была ли это вставка, обновление или удаление.

Однако набор результатов из MERGE / OUTPUT нельзя напрямую соединить с таблицей SOURCE, поэтому вы можете выполнить INSERT, так как вы получите только записи TARGET. Вы также не можете использовать результаты OUTPUT в коррелированном подзапросе из таблицы SOURCE. Проще всего использовать временную таблицу или табличную переменную для захвата вывода.

-- Logic to do upsert
DECLARE @Updates TABLE (
    [Name] [varchar](10) NOT NULL,  
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NOT NULL
)

INSERT @Updates
    SELECT
        Name,
        StartDate,
        EndDate
    FROM (
        MERGE INTO @tblEmployee AS TARGET
        USING @typeEmployee AS SOURCE
            ON TARGET.Name = SOURCE.Name 
        WHEN MATCHED AND TARGET.StartDate < SOURCE.StartDate
        THEN
            --First Update Existing Record EndDate to Previous Date as shown below 
            UPDATE SET
                EndDate = DATEADD(DAY, -1, CONVERT(DATE, SOURCE.StartDate))
        WHEN NOT MATCHED BY TARGET -- OR MATCHED AND TARGET.StartDate >= SOURCE.StartDate -- Handle this case?
        THEN
            INSERT VALUES(SOURCE.Name, SOURCE.StartDate, SOURCE.EndDate)
        OUTPUT $action, INSERTED.Name, INSERTED.StartDate, INSERTED.EndDate
        -- Use the MERGE to return all changed records of target table
    ) AllChanges (ActionType, Name, StartDate, EndDate)
    WHERE AllChanges.ActionType = 'UPDATE' -- Only get records that were updated

Теперь, когда вы перехватили выходные данные MERGE и отфильтровали, чтобы получить только обновленные записи TARGET, вы можете выполнить оставшуюся работу INSERT, отфильтровав только ИСТОЧНИКИ, которые были частью обновления MERGE.

INSERT @tblEmployee
    SELECT
        SOURCE.Name,
        SOURCE.StartDate,
        SOURCE.EndDate
    FROM @typeEmployee SOURCE
    WHERE EXISTS (
        SELECT *
        FROM @Updates Updates
        WHERE Updates.Name = SOURCE.Name
            -- Other join conditions to ensure 1:1 match against SOURCE (start date?)
    )

Вывод

Это результат выборки записей после изменения. Запланированные изменения TARGET были внесены.

-- Show output
SELECT * FROM @tblEmployee
person Jason W    schedule 12.07.2016
comment
Небольшое объяснение может быть полезно для вашего кода выше - person Amar; 13.07.2016
comment
Добавлены дополнительные пояснения. У вас есть какой-то конкретный вопрос? С удовольствием объясню любую часть... - person Jason W; 13.07.2016
comment
Один комментарий, который вы можете вставить в таблицу, с которой вы объединяетесь, без необходимости иметь дополнительную таблицу @Updates в памяти. - person costa; 26.07.2018

Следуя идее из принятого ответа, это работает и на сервере Sql 2008 r2:

create table Test1 (
  Id int, FromDate date, ThruDate date, Value int
)


insert into dbo.Test1
 (Id, FromDate, ThruDate, [Value])
select 
  t.Id, t.FromDate, T.ThruDate, t.Value * 100
from (

MERGE  dbo.Test1 AS Target
 USING (
         select 1 as Id, '2000-01-01' as FromDate, '2000-12-31' as ThruDate, 2 as Value
       ) AS Source
    ON (  target.id = source.Id
        )
WHEN MATCHED
THEN
  UPDATE SET Target.[Id] = Source.[Id]
           , Target.[FromDate] = Source.[FromDate]
           , Target.[ThruDate] = Source.[ThruDate]
           , Target.[Value] = Source.[Value]
WHEN NOT MATCHED BY TARGET
THEN
  INSERT ([Id]
        , [FromDate]
        , [ThruDate]
        , [Value])
  VALUES (Source.[Id]
        , Source.[FromDate]
        , Source.[ThruDate]
        , Source.[Value])
OUTPUT $ACTION as Act, Inserted.*
) t
where t.Act  = 'Update'

Вы можете играть с разными значениями.

person costa    schedule 26.07.2018