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_USER przez aplikację lub innego użytkownika.

Skrypt:

  1. Sprawdza, czy baza istnieje.
  2. Jeśli baza jest w trybie MULTI_USER, przełącza ją na RESTRICTED_USER z ROLLBACK IMMEDIATE.
  3. Odnajduje wszystkie sesje użytkowników w tej bazie (z wyjątkiem Twojej) i zabija je jedna po drugiej.
  4. Przełącza bazę w tryb SINGLE_USER z ROLLBACK IMMEDIATE.
  5. 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_sessionstable variableTRY/CATCH – standardowe funkcjonalności).
  • Uprawnienia:
    • rola serwerowa sysadminlub równoważne uprawnienia do:
      • wykonywania ALTER DATABASE,
      • wykonywania KILL na sesjach innych użytkowników.
  • Kontekst:
    • skrypt wykonywany z bazy master.
  • 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.

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') zwraca NULL, 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 – tylko sysadmindbcreatordb_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      &lt;> @MySpid;

  • Tworzona jest tabelka tymczasowa @SessionsToKill.
  • Wstawiane są do niej session_id wszystkich 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).

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 WHILE pobierana jest zawsze jedna sesja do zabicia.
  • KILL @SessionId nie wymaga dynamicznego SQL – przyjmuje zmienną.
  • TRY/CATCH zabezpiecza 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 KILL się 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 IMMEDIATE to 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.

  1. Zmień nazwę w deklaracji @DbId:DECLARE @DbId int = DB_ID(N'MyDb'), @MySpid int = @@SPID, @SessionId int;
  2. 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;
  3. (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 sysadmin lub 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_USER na 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”.


Dariusz Brejnak

Od prawie trzydziestu lat jest pasjonatem informatyki, a zwłaszcza dziedzin dotyczących baz danych, hurtowni danych oraz ogólnie rozumianej tematyki BI. Jego druga pasja to fotografia http://dariuszbrejnak.pl