Instrukcja Merge w T-SQL

Instrukcja MERGE to sposób, aby jednym zapytaniem wykonać 3 operacje naraz: INSERT, UPDATE, DELETE.

Najczęściej używa się jej do synchronizacji danych między dwiema tabelami:
- źródłową (np. dane z importu)
- docelową (np. tabela produkcyjna)

1. Przykład z życia – aktualizacja klientów po imporcie

Załóżmy, że codziennie ładujesz dane klientów do tabeli tymczasowej #Klienci_Import, a Twoja tabela docelowa to Klienci. Chcesz:
- zaktualizować dane, które się zmieniły,
- dodać nowych klientów,
- usunąć tych, których już nie ma w CRM.

Kod SQL:
--dane

drop table if exists Klienci;
drop table if exists #Klienci_Import;

create table Klienci (KlientID int IDENTITY(1,1) primary key not null, Imie varchar(20),Nazwisko varchar(20));
create table #Klienci_Import (KlientID int IDENTITY(1,1) primary key not null, Imie varchar(20),Nazwisko varchar(20));

--tabela bazowa i co sie bedzie dziac
insert into Klienci (Imie,Nazwisko) select 'Jarek','Kowalski' --bez zmian
insert into Klienci (Imie,Nazwisko) select 'Mirek','Maliniak' --to update
insert into Klienci (Imie,Nazwisko) select 'Wacek','Placek' --to del

--aktualna lista
insert into #Klienci_Import (Imie,Nazwisko) select 'Jarek','Kowalski'
insert into #Klienci_Import (Imie,Nazwisko) select 'Mirosław','Malinowski'

--merge na tabeli #Klienci na podstawie #Klienci_Import
MERGE Klienci WITH (HOLDLOCK) AS Target
USING #Klienci_Import AS Source
ON Target.KlientID = Source.KlientID
--upd
WHEN MATCHED AND (Target.Imie <> Source.Imie OR Target.Nazwisko <> Source.Nazwisko)
THEN UPDATE SET Target.Imie = Source.Imie, Target.Nazwisko = Source.Nazwisko
--ins
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Imie, Nazwisko)
    VALUES (Source.Imie, Source.Nazwisko)
--del
WHEN NOT MATCHED BY SOURCE THEN
    DELETE;

 --check
 select * from Klienci

Results:
KlientID    Imie                 Nazwisko
----------- -------------------- --------------------
1           Jarek                Kowalski
2           Mirosław             Malinowski

2. Co tu się dzieje

  1. Klienci_Import – to źródło danych (np. nowy import z CRM).
  2. Klienci – to tabela docelowa z aktualnymi danymi.
  3. SQL Server porównuje rekordy po KlientID:
    • jeśli rekord istnieje w obu tabelach, ale dane się różnią → robi UPDATE,
    • jeśli rekord istnieje tylko w źródle → robi INSERT,
    • jeśli rekord istnieje tylko w docelowej → robi DELETE.  

3. Dlaczego WITH (HOLDLOCK) jest ważne

HOLDLOCK mówi SQL Serverowi, żeby potraktował całą tabelę docelową jakby działała w trybie SERIALIZABLE – czyli żadna inna transakcja nie może jej zmienić w trakcie wykonywania MERGE.
Dzięki temu masz gwarancję, że:

  • dane nie zostaną przypadkowo nadpisane przez równoległe procesy,
  • MERGE wykona się w sposób spójny i przewidywalny.

Bez tej blokady, przy dużym ruchu w bazie, możesz złapać tzw. race condition, czyli błędną aktualizację spowodowaną zmianą danych w trakcie operacji.

4. Po co ten warunek w WHEN MATCHED

Domyślnie MERGE wykona UPDATE nawet wtedy, gdy wartości kolumn są identyczne.
To powoduje zbędne zapisy do loga, blokady, a czasem uruchamianie triggerów.
Dlatego warto dopisać warunek:

WHEN MATCHED AND (Target.Kolumna <> Source.Kolumna)

Dzięki temu aktualizujesz tylko faktyczne zmiany.

5. Podsumowanie

MERGE to potężne narzędzie, które pozwala w prosty sposób utrzymać dane zsynchronizowane między dwiema tabelami.
Z HOLDLOCK masz bezpieczeństwo, a z warunkiem w WHEN MATCHED – wydajność i czystość loga transakcyjnego.

To rozwiązanie sprawdza się idealnie przy:

  • codziennych importach danych,
  • procesach ETL,
  • integracjach z systemami zewnętrznymi.

Jedno polecenie, pełna kontrola nad danymi.

Skróty i ułatwienia do MSSQL Managment Studio

Kilka przydatnych funkcji MSSQL Managment Studio, które na pewno ułatwią pracę.

1) Szybka edycji wielu wierszy w tym samym miejscu. (skrót: SHIFT + ALT)
Przytrzymujesz Shift + Alt i przeciągasz myszką pionowo po tekście lub używasz strzałek w górę/dół, żeby zaznaczyć kolumnę znaków.

2) Ukrycie/Pokazanie panelu Wynik Zapytania 
CLTR+R chowanie panelu Result Query, ponownie CTRL+R włączenie, aby mieć więcej miejsca na ekranie.

3) Wszystkie nazwy kolumn bez pisania nazw
W drzewku obiektów wybierz odpowiednią nazwę Tabeli i folder Columns przeciągnij do okna zapytania


4) Lepiej uporządkowane okna zapytań
Aby mieć lepiej widoczne ważne zakładki w MngStudio gdy mamy wiele okien pootwieranych (używamy pinezki i są one trzymane od lewej strony). Po zaznaczeniu opcji "Show pinned tabs in a separate row" widzimy je dodatkowo na górze jeśli mamy włączoną pinezkę na zapytaniu.


5) Odświeżenie Local Cache for IntelliSense
Aby mieć aktualny IntelliSense i podpowiedzi dla obiektów MSSQL CTRL + SHIFT + R odświeży nam podpowiedzi do obiektów.

6) Zmiana kontekstu bazy dla zapytania

CLTR+U przenosi focus na dostępne bazy do wybrania, gdzie możemy wpisać, wybrać i przełączyć na nową bazę z aktualnym zapytaniem, bez użycia USE <twojabaza>.

7) Przejście do wskazanej linii kodu CTRL + G.

8) Komentowanie kodu CTRL + K i CTRL + C.

9) Odkomentowanie kodu CTRL + K i CTRL + U.

10) Uppercase na kodzie CTRL + SHIFT + U.

11) Lowercase na kodzie CTRL + SHIFT + L.


12) Pogląd otwartych zakładek (przegląd)
Naciśnij CTRL + TAB pojawi się okno gdzie masz otwarte wszystkie queries i możesz przejść do dowolnej bez przeklikiwania po pasku zapytań.

13) Podział zapytania na dwie części i przeglądanie zmian w kontekście dwóch widoków tego samego query. 
W tym celu prawy górny róg ma ikonę, którą należy chwycić i przeciągać aby uzyskać podział widoku query.


14. Podpowiedzi do budowania obiektów CTRL + K i CTRL + X.


15. Szybki dostęp do dokumentacji MSDN dla funkcji TSQL.
Zaznacz słowo klucz w kodzie tsql, naciśnij F1 i przenosi do dokumentacji MSDN.

16. Szczegóły obiektu F7
Klikamy na przykład na Tables danej bazy i uzyskujemy przegląd tabel z kluczowymi kolumnami np. liczba wierszy, ile zajmują na dysku etc.


17. Eksplorator szablonów kodu CTRL + ALT + T
Pokazuje okno szablonów do tworzenia obiektów w MSSQL i przykłady kodu
Mając kod możemy wybrać (query -> Specify values for template parameters…) skrót CTRL + SHIFT + M. pojawi się okno do wpisania wartości dla szablonu kodu.


18. Organizowanie skryptów SQL w projekt SQL
File -> New Project i mamy możliwość zapisywania swoich sql pod Queries i szybki dostęp w ramach projektu. W ramach tego projektu możemy dodawać wszystkie inne stare skrypty, taki łatwy dostęp naszych kodów.


Pełny backup bazy i przyrostowy

Dla baz zwykle w maintance plan trzeba przygotować backup, gdzie np. w weekend/codziennie następuje pełen backup bazy, a w między czasie w krótszych interwałach dziennych/godzinowych następuje backupowanie przyrostowe. 

1) Pełny backup z kompresją:

BACKUP DATABASE [test1]
TO  DISK = N'C:\Baza_MDF\test1_20250815_1601_full.bak' 
WITH 
NOFORMAT, 
NOINIT,  
NAME = N'test1-Full Database Backup', 
SKIP, 
NOREWIND, 
NOUNLOAD, 
COMPRESSION,
STATS = 10, 
CHECKSUM
GO

declare @backupSetId as int
select @backupSetId = position 
from msdb..backupset 
where database_name=N'test1' and
backup_set_id=(select max(backup_set_id) 
from msdb..backupset where database_name=N'test1' )

if @backupSetId is null 
begin raiserror(N'Verify failed. Backup information for database ''test1'' not found.', 16, 1) end
RESTORE VERIFYONLY FROM  DISK = N'C:\Baza_MDF\test1_20250815_1601_full.bak' 
WITH  
FILE = @backupSetId,  
NOUNLOAD,  
NOREWIND
GO

1) Przyrostowy backup z kompresją:

BACKUP DATABASE [test1] 
TO  DISK = N'C:\Baza_MDF\test1_20250815_1605_differential.bak' 
WITH  DIFFERENTIAL , 
NOFORMAT, 
NOINIT,  
NAME = N'test1-Diff Database Backup', 
SKIP, 
NOREWIND, 
NOUNLOAD, 
COMPRESSION,  
STATS = 10, 
CHECKSUM
GO

declare @backupSetId as int
select @backupSetId = position 
from msdb..backupset 
where database_name=N'test1' and
backup_set_id=(select max(backup_set_id) from msdb..backupset where database_name=N'test1' )

if @backupSetId is null 
begin raiserror(N'Verify failed. Backup information for database ''test1'' not found.', 16, 1) end
RESTORE VERIFYONLY FROM  DISK = N'C:\Baza_MDF\test1_20250815_1605_differential.bak' WITH  FILE = @backupSetId,  NOUNLOAD,  NOREWIND
GO

3) Przywracamy bazę z fulla i danego differential backup

Pamiętać: W SQL Server jak już przywrócisz bazę z FULL do jakiegoś Diff z opcją WITH RECOVERY, to baza wraca do normalnego trybu i łańcuch przywracania się kończy

Jeśli chcesz, aby kolejne backupy były kontynuacją tego stanu po Diff 1, 
to po przywróceniu z Diff 1 robisz od razu nowy full backup

a) Ubijamy sesje do danej bazy 

USE master;
GO

DECLARE @dbName SYSNAME = N'test1'; -- wpisz nazwę bazy
DECLARE @sql NVARCHAR(MAX) = N'';

SELECT @sql += 'KILL ' + CAST(s.session_id AS NVARCHAR(10)) + ';'
FROM sys.dm_exec_sessions AS s
JOIN sys.dm_exec_requests AS r
    ON s.session_id = r.session_id
WHERE r.database_id = DB_ID(@dbName)
  AND s.session_id <> @@SPID; -- pomijamy swoje połączenie

-- Dodatkowo uwzględniamy sesje idle z sys.dm_exec_sessions
SELECT @sql += 'KILL ' + CAST(s.session_id AS NVARCHAR(10)) + ';'
FROM sys.dm_exec_sessions AS s
WHERE s.database_id = DB_ID(@dbName)
  AND s.session_id <> @@SPID;

IF @sql <> ''
BEGIN
    PRINT @sql; -- możesz sprawdzić przed wykonaniem
    EXEC sp_executesql @sql;
END
ELSE
    PRINT 'Brak aktywnych połączeń do bazy ' + @dbName;

b) Restore do danego punktu

Mając full i dany differental możemy zrobić restore w oparciu o daną przyrostówkę:

use master
RESTORE DATABASE test1
FROM DISK = 'C:\Baza_MDF\test1_20250815_1637_full.bak'
WITH NORECOVERY, REPLACE;

use master
RESTORE DATABASE test1
FROM DISK = 'C:\Baza_MDF\test1_20250815_1639_differential.bak'
WITH RECOVERY;

 

Przeniesienie plików bazy MSSQL do nowej lokalizacji

Przeniesienie bazy mssql do nowej lokalizacji wymaga odpowiedniej kolejności:

1) Sprawdź aktualne ścieżki plików

use master
SELECT name, physical_name, type_desc
FROM sys.master_files
WHERE database_id = DB_ID('test1');

wynik z name i physical_name skopiuj sobie na bok, z tej lokalizacji będziesz kopiował zwrócone pliki do nowego folderu

2) Odłącz bazę (DETACH)

USE master;
GO
ALTER DATABASE test1 
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
EXEC sp_detach_db 'test1';


3) Fizyczne przeniesienie plików

Skopiuj pliki bazy tj: .mdf, .ndf, .ldf w nowe lokalizacje na dysku.

4) Prawa do folderu i plików

a) Upewnij się, że konto usługi SQL Server ma dostęp do nowego katalogu (prawo odczytu/zapisu).
Zwykle: 
NT Service\MSSQLSERVER (ale sprawdzić kto uruchamia usługę)
Klikasz docelowy folder z bazą - właściwości - Zabezpieczenia - Edytuj - Dodaj

właściwości folderu
właściwości folderu dodanie użytkownika MSSQL

b) Jeśli wykonujesz podłączenie z MngStudio logując się swoim kontem domenowym,  upewnij się aby ten użytkownik również miał prawa folderu z plikami baz tj. opcja zaawansowane i dodać swoje konto.
Bez tych punktów MSSQL będzie miał błąd dostępu do plików i nie podepnie bazy.

5. Podłącz bazę z nowymi ścieżkami (ATTACH)

USE master;
GO
CREATE DATABASE test1 ON 
    (FILENAME = 'C:\Baza_MDF\test1.mdf'),
    (FILENAME = 'C:\Baza
_MDF\test1.ndf'),
    (FILENAME = 'F:\Baza
_MDF\test1.ldf')
FOR ATTACH;



Zmiana tempdb do nowej lokalizacji

Przeniesienie bazy tempdb do nowej lokalizacji:

1) Wykonujemy zapytanie, które przygotuje nam SQL do zmiany lokalizacji tempdb

DECLARE @new_path NVARCHAR(4000) = N'T:\New_File_Path\';
 
SELECT
      N'ALTER DATABASE tempdb MODIFY FILE (NAME = ' + QUOTENAME(f.[name]) + N', '
    + N'FILENAME = ''' + @new_path + RIGHT(physical_name, CHARINDEX('\', REVERSE(physical_name)) -1)
    + ''');'
    AS [AlterCommand]
FROM sys.master_files f
WHERE f.database_id = DB_ID(N'tempdb');

 Wynik:

ALTER DATABASE tempdb MODIFY FILE (NAME = [tempdev], FILENAME = 'T:\New_File_Path\tempdb.mdf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [templog], FILENAME = 'T:\New_File_Path\templog.ldf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp2], FILENAME = 'T:\New_File_Path\tempdb_mssql_2.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp3], FILENAME = 'T:\New_File_Path\tempdb_mssql_3.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp4], FILENAME = 'T:\New_File_Path\tempdb_mssql_4.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp5], FILENAME = 'T:\New_File_Path\tempdb_mssql_5.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp6], FILENAME = 'T:\New_File_Path\tempdb_mssql_6.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp7], FILENAME = 'T:\New_File_Path\tempdb_mssql_7.ndf');
ALTER DATABASE tempdb MODIFY FILE (NAME = [temp8], FILENAME = 'T:\New_File_Path\tempdb_mssql_8.ndf');


3) Tworzymy nowy folder ma dysku pod tempdb


4) Użytkownik, który uruchomia usługę MSSQL musi być dodany aby miał prawa (najlepiej pełne) do dysku. 
Zwykle jest to użytkownik domenowy, albo wbudowane wirtualne konto MSSQL np: NT Service\MSSQLSERVER

5) Restart MSSQL (bazy tempdb w tym momencie się odtwarzają na nowej lokalizacji)