1. Cel skryptu
Celem skryptu jest bezpieczne i kontrolowane przełączenie bazy danych w tryb SINGLE_USER w środowisku produkcyjnym, w którym:
- zwykle pracujesz z bazy
master, - w momencie przełączania mogą istnieć inne aktywne sesje do danej bazy,
- chcesz mieć pewność, że:
- nikt poza Twoją sesją nie korzysta z bazy,
- baza nie zostanie „przejęta” w trybie
SINGLE_USERprzez aplikację lub innego użytkownika.
Skrypt:
- Sprawdza, czy baza istnieje.
- Jeśli baza jest w trybie
MULTI_USER, przełącza ją naRESTRICTED_USERzROLLBACK IMMEDIATE. - Odnajduje wszystkie sesje użytkowników w tej bazie (z wyjątkiem Twojej) i zabija je jedna po drugiej.
- Przełącza bazę w tryb
SINGLE_USERzROLLBACK IMMEDIATE. - Pozostawia miejsce na wykonanie własnych operacji administracyjnych (np. zmiana nazwy bazy, plików), a następnie przywrócenie
MULTI_USER.
Przykładowo dokument opisuje bazę Foo, ale skrypt można łatwo dostosować do dowolnej innej bazy.
2. Założenia i wymagania
- Silnik: Microsoft SQL Server 2012+ (skrypt korzysta z
sys.dm_exec_sessions,table variable,TRY/CATCH– standardowe funkcjonalności). - Uprawnienia:
- rola serwerowa
sysadminlub równoważne uprawnienia do:- wykonywania
ALTER DATABASE, - wykonywania
KILLna sesjach innych użytkowników.
- wykonywania
- rola serwerowa
- Kontekst:
- skrypt wykonywany z bazy
master.
- skrypt wykonywany z bazy
- Baza przykładowa:
- w przykładzie używana jest baza
Foo. - przy dostosowaniu do innej bazy należy zmienić:
DB_ID(N'Foo')→DB_ID(N'NowaBaza'),- wszystkie wystąpienia
ALTER DATABASE [Foo] ..., - komunikat w
RAISERROR.
- w przykładzie używana jest baza
3. Pełny skrypt T‑SQL (dla bazy Foo)
USE master;
GO
----------------------------------------------------
-- Parametry / zmienne
----------------------------------------------------
DECLARE
@DbId int = DB_ID(N'Foo'), -- <<< tu zmienisz nazwę bazy
@MySpid int = @@SPID,
@SessionId int;
----------------------------------------------------
-- 0. Sprawdzenie czy baza istnieje
----------------------------------------------------
IF @DbId IS NULL
BEGIN
RAISERROR('Baza Foo nie istnieje.', 16, 1); -- przy zmianie bazy zmień też tekst
RETURN;
END;
PRINT 'Moja sesja (nie będzie zabita): ' + CAST(@MySpid AS varchar(10));
----------------------------------------------------
-- 1. Jeśli baza jest w MULTI_USER → RESTRICTED_USER
----------------------------------------------------
PRINT 'Aktualny tryb dostępu dla bazy o DB_ID='
+ CAST(@DbId AS varchar(10)) + ': '
+ (SELECT user_access_desc
FROM sys.databases
WHERE database_id = @DbId);
IF (SELECT user_access_desc FROM sys.databases WHERE database_id = @DbId) = 'MULTI_USER'
BEGIN
PRINT 'Przelaczam baze (DB_ID=' + CAST(@DbId AS varchar(10))
+ ') na RESTRICTED_USER z ROLLBACK IMMEDIATE...';
-- tu nadal musi być nazwa bazy (składnia ALTER DATABASE)
ALTER DATABASE [Foo]
SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;
END
ELSE
BEGIN
PRINT 'Pomijam krok RESTRICTED_USER (baza nie jest w MULTI_USER).';
END;
----------------------------------------------------
-- 2. Zbierz sesje do zabicia do tabeli i KILL jedna po jednej
----------------------------------------------------
DECLARE @SessionsToKill TABLE (session_id int PRIMARY KEY);
INSERT INTO @SessionsToKill(session_id)
SELECT s.session_id
FROM sys.dm_exec_sessions s
WHERE s.database_id = @DbId
AND s.is_user_process = 1
AND s.session_id <> @MySpid;
IF NOT EXISTS (SELECT 1 FROM @SessionsToKill)
BEGIN
PRINT 'Brak innych sesji użytkowników w tej bazie.';
END
ELSE
BEGIN
PRINT 'Zabijam inne sesje w tej bazie, jedna po jednej...';
WHILE EXISTS (SELECT 1 FROM @SessionsToKill)
BEGIN
SELECT TOP (1) @SessionId = session_id
FROM @SessionsToKill
ORDER BY session_id;
PRINT 'KILL ' + CAST(@SessionId AS varchar(10)) + '...';
BEGIN TRY
KILL @SessionId; -- tu nie ma dynamicznego SQL, KILL przyjmuje zmienną
END TRY
BEGIN CATCH
PRINT 'Nie udalo sie zabic sesji ' + CAST(@SessionId AS varchar(10))
+ '. ERROR: ' + ERROR_MESSAGE();
END CATCH;
DELETE FROM @SessionsToKill WHERE session_id = @SessionId;
END
END;
----------------------------------------------------
-- 3. Ustawienie bazy w SINGLE_USER
----------------------------------------------------
PRINT 'Przelaczam baze (DB_ID=' + CAST(@DbId AS varchar(10))
+ ') na SINGLE_USER z ROLLBACK IMMEDIATE...';
ALTER DATABASE [Foo]
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
PRINT 'Gotowe. Baza Foo jest w trybie SINGLE_USER.';
----------------------------------------------------
-- 4. TUTAJ DOPISZ SWOJE OPERACJE (rename bazy, plików itd.)
-- w tej samej sesji / oknie
----------------------------------------------------
-- np.:
-- ALTER DATABASE [Foo] MODIFY NAME = [Foo_New];
-- ALTER DATABASE [Foo_New] MODIFY FILE (...);
----------------------------------------------------
-- 5. Na końcu Twojej procedury:
----------------------------------------------------
-- ALTER DATABASE [Foo] SET MULTI_USER;
-- albo po rename:
-- ALTER DATABASE [Foo_New] SET MULTI_USER;
4. Szczegółowy opis działania krok po kroku
4.1. Inicjalizacja zmiennych
DECLARE
@DbId int = DB_ID(N'Foo'),
@MySpid int = @@SPID,
@SessionId int;
@DbId– identyfikator bazy (database_id) uzyskany na podstawie nazwy.@MySpid– numer bieżącej sesji (Twoje połączenie w SSMS / innym kliencie).@SessionId– zmienna pomocnicza w pętli zabijającej sesje.
4.2. Sprawdzenie istnienia bazy
IF @DbId IS NULL
BEGIN
RAISERROR('Baza Foo nie istnieje.', 16, 1);
RETURN;
END;
- Jeśli
DB_ID(N'Foo')zwracaNULL, to znaczy, że baza o takiej nazwie nie istnieje. - Skrypt kończy się komunikatem błędu i nie wykonuje dalszych kroków.
4.3. Sprawdzenie i ewentualna zmiana trybu dostępu
PRINT 'Aktualny tryb dostępu dla bazy o DB_ID='
+ CAST(@DbId AS varchar(10)) + ': '
+ (SELECT user_access_desc
FROM sys.databases
WHERE database_id = @DbId);
IF (SELECT user_access_desc FROM sys.databases WHERE database_id = @DbId) = 'MULTI_USER'
BEGIN
ALTER DATABASE [Foo]
SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;
END
- Odczytywany jest aktualny tryb dostępu (
user_access_desc):MULTI_USER,RESTRICTED_USER,SINGLE_USER.
- Jeśli baza jest w
MULTI_USER, zostaje przełączona na:RESTRICTED_USER– tylkosysadmin,dbcreator,db_owner,WITH ROLLBACK IMMEDIATE– wszystkie bieżące transakcje w tej bazie są natychmiast cofane, sesje tracą połączenie.
Jeśli baza jest już w innym trybie (RESTRICTED_USER lub SINGLE_USER), skrypt wypisuje informację i nie próbuje na siłę zmieniać.
4.4. Zbieranie sesji do zabicia
DECLARE @SessionsToKill TABLE (session_id int PRIMARY KEY);
INSERT INTO @SessionsToKill(session_id)
SELECT s.session_id
FROM sys.dm_exec_sessions s
WHERE s.database_id = @DbId
AND s.is_user_process = 1
AND s.session_id <> @MySpid;
- Tworzona jest tabelka tymczasowa
@SessionsToKill. - Wstawiane są do niej
session_idwszystkich sesji:- należących do użytkowników (
is_user_process = 1), - pracujących w tej bazie (
database_id = @DbId), - innych niż Twoja (
session_id <> @MySpid).
- należących do użytkowników (
4.5. KILL sesji jedna po drugiej
WHILE EXISTS (SELECT 1 FROM @SessionsToKill)
BEGIN
SELECT TOP (1) @SessionId = session_id
FROM @SessionsToKill
ORDER BY session_id;
PRINT 'KILL ' + CAST(@SessionId AS varchar(10)) + '...';
BEGIN TRY
KILL @SessionId;
END TRY
BEGIN CATCH
PRINT 'Nie udalo sie zabic sesji ' + CAST(@SessionId AS varchar(10))
+ '. ERROR: ' + ERROR_MESSAGE();
END CATCH;
DELETE FROM @SessionsToKill WHERE session_id = @SessionId;
END
- W pętli
WHILEpobierana jest zawsze jedna sesja do zabicia. KILL @SessionIdnie wymaga dynamicznego SQL – przyjmuje zmienną.TRY/CATCHzabezpiecza sytuacje, w których:- sesja „zniknie” (rozłączy się) między zebraniem listy a próbą zabicia,
- lub wystąpi inny błąd.
- Nawet jeśli pojedynczy
KILLsię nie uda, skrypt idzie dalej, zabija pozostałe sesje.
4.6. Przełączenie bazy w SINGLE_USER
ALTER DATABASE [Foo]
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
- Po zabiciu wszystkich innych sesji baza zostaje przełączona w tryb
SINGLE_USER. WITH ROLLBACK IMMEDIATEto dodatkowe zabezpieczenie, gdyby jednak jakaś sesja zdążyła „wejść” pomiędzy pętlą a tym poleceniem.- Od tej chwili tylko jedna sesja może korzystać z bazy – typowo będzie to Twoja sesja, która wykonywała skrypt.
5. Przykład scenariusza: zmiana nazwy bazy i plików
Uruchamiasz skrypt z sekcji 3 w nowym oknie SSMS (kontekst master). Po komunikacie:
Gotowe. Baza Foo jest w trybie SINGLE_USER.
w tej samej sesji / oknie wykonujesz swoje operacje, np.:
-- Zmiana nazwy bazy
ALTER DATABASE [Foo] MODIFY NAME = [Foo_New];
GO
-- Przykład: zmiana ścieżek plików (po wcześniejszym OFFLINE, jeśli przenosisz fizyczne pliki)
-- ALTER DATABASE [Foo_New] SET OFFLINE WITH ROLLBACK IMMEDIATE;
-- ... przeniesienie plików na poziomie systemu plików ...
-- ALTER DATABASE [Foo_New]
-- MODIFY FILE (NAME = N'Foo_New_Data', FILENAME = N'D:\SQLData\Foo_New.mdf');
-- ALTER DATABASE [Foo_New]
-- MODIFY FILE (NAME = N'Foo_New_Log', FILENAME = N'D:\SQLLogs\Foo_New_log.ldf');
-- ALTER DATABASE [Foo_New] SET ONLINE;
Na końcu przywracasz tryb MULTI_USER:ALTER DATABASE [Foo_New] SET MULTI_USER;
Typowe użycie:
6. Dostosowanie skryptu do innej bazy
Załóżmy, że chcesz zastosować skrypt do bazy MyDb.
- Zmień nazwę w deklaracji
@DbId:DECLARE @DbId int = DB_ID(N'MyDb'), @MySpid int = @@SPID, @SessionId int; - Zmień nazwę bazy w poleceniach
ALTER DATABASE:ALTER DATABASE [MyDb] SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE; ... ALTER DATABASE [MyDb] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; ... -- Na końcu: -- ALTER DATABASE [MyDb] SET MULTI_USER; - (Opcjonalnie) dostosuj tekst komunikatu w
RAISERROR, aby był spójny:RAISERROR('Baza MyDb nie istnieje.', 16, 1);
7. Lista kontrolna (checklista DBA)
Przed uruchomieniem skryptu:
- [ ] Masz otwarte osobne okno w SSMS, kontekst
master. - [ ] Upewniłeś się, że nie ma krytycznych operacji aplikacyjnych w tej bazie.
- [ ] W skrypcie jest poprawnie ustawiona nazwa bazy w:
- [ ]
DB_ID(N'...'), - [ ]
ALTER DATABASE [... ].
- [ ]
- [ ] Masz uprawnienia
sysadminlub równoważne.
Po uruchomieniu skryptu:
- [ ] Sprawdziłeś komunikaty
PRINT, czy wszystkie niechciane sesje zostały zabite. - [ ] Wykonałeś swoje operacje administracyjne w tej samej sesji.
- [ ] Na końcu przywróciłeś
MULTI_USERna właściwej nazwie bazy (po ewentualnym rename).
Dzięki temu skryptowi masz powtarzalny, bezpieczny schemat przełączania bazy w SINGLE_USER bez dynamicznego SQL, z minimalnym użyciem nazwy bazy oraz z odpornością na typowe problemy z KILL wykonywanym „hurtowo”.