SQL Server

[PL] Skryptowanie w SQL Server 2008 – Klucze obce

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Intro

To już trzeci wpis z serii “Skryptowanie w SQL Server 2008″. Dwa poprzednie dostępne są tu:

Tym razem przedstawiam sposób na skryptowanie kluczy obcych. Kiedy takie skryptowanie może się przydać? Na przykład wtedy, gdy chcemy usunąć, a następnie odtworzyć owe klucze (typowy scenariusz: import wyczyszczonych i zwalidowanych danych). W podsumowaniu podam, jakie rozwinięcia mojego kodu można zaimplementować, by za pomocą praktycznie tego samego kodu wykonywać inne zadania związane z kluczami obcymi.

Skryptowanie kluczy obcych

Zadanie: zeskryptować wszystkie klucze obce w bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;

-- tu będziemy trzymać generowany kod
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);

DECLARE
  @object_name sysname,
  @object_id int,
  @object_schema_name sysname,
  @parent_object_name sysname,
  @parent_object_schema_name sysname,
  @is_not_for_replication bit,
  @delete_referential_action int,
  @update_referential_action int,
  @referenced_object_schema_name sysname,
  @referenced_object_name sysname,
  @column_name sysname,
  @referenced_column_name sysname,
  @columns nvarchar(4000),
  @referenced_columns nvarchar(4000);

-- kursor do wyciągania pojedynczych kluczy obcych
DECLARE CursorFK CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    name,
    [object_id],
    SCHEMA_NAME([schema_id]),
    OBJECT_SCHEMA_NAME(parent_object_id),
    OBJECT_NAME(parent_object_id),
    OBJECT_SCHEMA_NAME(referenced_object_id),
    OBJECT_NAME(referenced_object_id),
    is_not_for_replication,
    delete_referential_action,
    update_referential_action
  FROM sys.foreign_keys
  WHERE is_ms_shipped = 0;
OPEN CursorFK;
FETCH NEXT FROM CursorFK
INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;

WHILE @@FETCH_STATUS = 0 BEGIN
  SELECT @columns = '', @referenced_columns = '';

  -- generujemy sprawdzenie, czy klucz nie istnieje
  INSERT INTO @SQL (Line)
  SELECT 'IF OBJECT_ID(N''' +
        REPLACE(
          QUOTENAME(@object_schema_name) + '.' +
          QUOTENAME(@object_name), '''', ''''''
        ) + ''', N''F'') IS NULL';

  -- generujemy polecenie ALTER TABLE ... ADD CONSTRAINT
  INSERT INTO @SQL (Line)
  SELECT '  ALTER TABLE ' +
    QUOTENAME(@parent_object_schema_name) + '.' +
    QUOTENAME(@parent_object_name);
  INSERT INTO @SQL (Line)
  SELECT '  ADD CONSTRAINT ' + QUOTENAME(@object_name);

  -- kursor do wyciągania kolejnych kolumn klucza obcego
  DECLARE CursorFKColumns CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR
    SELECT
      pc.name,
      rc.name
    FROM sys.foreign_key_columns AS f
    INNER JOIN sys.columns AS pc
    ON f.parent_object_id = pc.[object_id]
    AND f.parent_column_id = pc.column_id
    INNER JOIN sys.columns AS rc
    ON f.referenced_object_id = rc.[object_id]
    AND f.referenced_column_id = rc.column_id
    WHERE f.constraint_object_id = @object_id
    ORDER BY f.constraint_column_id
  OPEN CursorFKColumns;
  FETCH NEXT FROM CursorFKColumns
  INTO
    @column_name,
    @referenced_column_name;
  WHILE @@FETCH_STATUS = 0 BEGIN
    -- doklejamy do zestawów kolumn
    SET @columns = @columns + ', ' + QUOTENAME(@column_name);
    SET @referenced_columns = @referenced_columns + ', ' + QUOTENAME(@referenced_column_name);

    FETCH NEXT FROM CursorFKColumns
    INTO
      @column_name,
      @referenced_column_name;
  END;
  CLOSE CursorFKColumns;
  DEALLOCATE CursorFKColumns;

  -- generujemy kod z listami kolumn
  INSERT INTO @SQL (Line)
  SELECT '  FOREIGN KEY (' + STUFF(@columns, 1, 2, '') + ')';
  INSERT INTO @SQL (Line)
  SELECT '  REFERENCES ' +
    QUOTENAME(@referenced_object_schema_name) + '.' +
    QUOTENAME(@referenced_object_name) + '(' +
    STUFF(@referenced_columns, 1, 2, '') + ')';

  -- akcje klucza przy usuwaniu wierszy
  IF @delete_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON DELETE ' +
      CASE @delete_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- akcje klucza przy modyfikacji wierszy
  IF @update_referential_action <> 0 BEGIN
    INSERT INTO @SQL (Line)
    SELECT '  ON UPDATE ' +
      CASE @update_referential_action
        WHEN 1 THEN 'CASCADE'
        WHEN 2 THEN 'SET NULL'
        WHEN 3 THEN 'SET DEFAULT'
      END;
  END;

  -- czy wymuszamy regułę klucza w replikacjach
  IF @is_not_for_replication = 1
    INSERT INTO @SQL (Line)
    SELECT '  NOT FOR REPLICATION';

  UPDATE @SQL
  SET Line = Line + ';'
  WHERE LineId = SCOPE_IDENTITY();

  INSERT INTO @SQL (Line) SELECT 'GO';

  FETCH NEXT FROM CursorFK
  INTO @object_name, @object_id, @object_schema_name,
     @parent_object_schema_name, @parent_object_name,
     @referenced_object_schema_name, @referenced_object_name,
     @is_not_for_replication,
     @delete_referential_action, @update_referential_action;
END;
CLOSE CursorFK;
DEALLOCATE CursorFK;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane kluczy obcych zwracają widoki sys.foreign_keys (metadane tabeli, w której tworzymy klucz, opcje dla poleceń DELETE / UPDATE, opcja wyłączenia klucza dla replikacji) oraz sys.foreign_key_columns (metadane kolumn oraz tabeli, do której odwołuje się klucz obcy),
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi i apostrofami,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • rozwiązanie oparłem o dwa kursory: CursorFK – do iterowania po kluczach obcych oraz zagnieżdżony CursorFKColumns – do iterowania po kolumnach bieżącego klucza,
  • ponieważ nie jestem zwolennikiem wyłączania więzów, moje rozwiązanie nie uwzględnia kolumny is_disabled z widoku sys.foreign_keys (nie generuję klauzuli WITH NOCHECK nawet, gdy klucz jest wyłączony).

Słowo podsumowania

Powyższy kod można rozwinąć:

  • można dołożyć filtrowanie po tabelach (czy to tabeli, w której klucz jest utworzony, czy po tabeli, do której klucz się odwołuje),
  • można generować tylko składnie ALTER TABLE … DROP CONSTRAINT, jeżeli zależy nam tylko na usunięciu wybranych kluczy,
  • można wykorzystać kolumnę is_system_named z widoku sys.foreign_keys, jeżeli chcemy odnaleźć te klucze, które otrzymały nazwy wygenerowane systemowo (jeżeli nie chcemy mieć w bazie danych obiektów nazywanych dość przypadkowo).

Na koniec informacja – w repozytorium kodu umieściłem kod pseudo-systemowej procedury sp_dropforeignkeys, która może posłużyć do wygenerowania kodu usuwającego klucze obce wskazujące na wybraną tabelę. W procedurze tej wykorzystałem te same obiekty systemowe, co w powyższym kodzie, więc komentarz do niej wydaje mi się zbędny. Po stworzeniu w bazie master i oznaczeniu jako obiekt systemowy procedura powinna działać w kontekście dowolnej bazy danych.

[PL] SQL Server – Baza danych z danymi geograficznymi Polski

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Jeżeli chcesz pobawić się trochę danymi geograficznymi i raportami pokazującymi mapy w SQL Server 2008 R2, ten wpis jest specjalnie dla Ciebie. Kiedyś musiałem przygotować prezentację na temat danych przestrzennych i z tamtej prezentacji została mi baza danych Spatials, która w dwóch tabelach przechowuje kształty Polski (tabela dbo.Country) i województw (dbo.Province). Backup daje się odtworzyć na SQL Server 2008 R2 (jest skompresowany i zajmuje około 1 MB). Poniżej przykładowy raport, jaki możesz zbudować w oparciu o dane z tej bazy.

image

Przyjemnej zabawy z danymi geograficznymi :-)

Pobierz kopię zapasową bazy Spatials (1 MB)

[PL] SQL Server – FOR XML EXPLICIT (nie) dla opornych

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Wstęp

Klauzula FOR XML EXPLICIT jest bodaj najrzadziej używaną z klauzul FOR XML. O wiele częściej decydujemy się na użycie FOR XML PATH (od SQL Server 2005) lub FOR XML AUTO (SQL Server 2000), ponieważ obie klauzule wydają się prostsze w użyciu niż FOR XML EXPLICIT (jak to okreslił jeden z moich kolegów – “FOR XML EXPLICIT przy FOR XML PATH wygląda jak neanderltalczyk przy homo sapiens”). Ale mało kto bierze pod uwagę, że FOR XML EXPLICIT daje – jako jedna z dwóch klauzul FOR XML na SQL Server 2005 i nowszych oraz jako jedyna klauzula na SQL Server 2000 – możliwość zwracania dokumentów XML, w których znajdą się zarówno elementy, jak i atrybuty. A już prawie nikt nie wie, że jedynie FOR XML EXPLICIT umożliwia generowanie dokumentów XML zawierających sekcje CDATA. I to właśnie owo niedocenianie i unikanie klauzuli FOR XML EXPLICIT skłoniło mnie do napisania tego tekstu.

W tym miejscu robię delikatne założenie – zakładam mianowicie, że wiesz, czym jest XML, co to znaczy, że dokument XML jest “well-formed”, czym się różni element od atrybutu. Jeżeli te pojęcia są Ci obce, dalsza część tego wpisu nie jest dla Ciebie (najpierw poczytaj o podstawach XML-a, a dopiero potem baw się XML-em w SQL Serverze).

Generujemy poligon

Załóżmy, że mamy dwie, uproszczone do granic możliwości, powiązane ze sobą logicznie tabele – #Faktury i #Pozycje. Tabela #Faktury zawiera typowe dane nagłówkowe faktur – numer faktury, datę wystawienia i nazwę kontrahenta. Tabela #Pozycje zawiera pozycje (linijki) faktury, w których znajdują się: numer faktury (aby było łatwo powiązać pozycje z fakturami), numer pozycji na fakturze (takie typowe Lp), nazwę produktu, cenę (dla uproszczenia bez rozgraniczenia na netto i brutto oraz z pominięciem VAT-u) oraz ilość towaru. Kod do utworzenia tabel i wypełnienia ich danymi dwóch przykładowych faktur:

IF OBJECT_ID(N'tempdb.dbo.#Faktury') IS NOT NULL
  DROP TABLE #Faktury;
CREATE TABLE #Faktury (
  NrFaktury varchar(10),
  DataWystawienia datetime,
  Kontrahent varchar(100)
);

IF OBJECT_ID(N'tempdb.dbo.#Pozycje') IS NOT NULL
  DROP TABLE #Pozycje;
CREATE TABLE #Pozycje (
  NrFaktury varchar(10),
  NrPozycji int,
  Produkt varchar(50),
  Ilosc int,
  Cena money
);

INSERT INTO #Faktury (NrFaktury, DataWystawienia, Kontrahent)
SELECT '04/07/2010','20100716','FirmaA'
UNION ALL
SELECT '05/07/2010','20100716','FirmaB';

INSERT INTO #Pozycje (NrFaktury, NrPozycji, Produkt, Ilosc, Cena)
SELECT '04/07/2010',1,'Produkt1',10,100.00
UNION ALL
SELECT '04/07/2010',2,'Produkt2',2,10.00
UNION ALL
SELECT '04/07/2010',3,'Produkt3',1,10.50
UNION ALL
SELECT '05/07/2010',1,'Produkt1',5,10.00
UNION ALL
SELECT '05/07/2010',2,'Produkt3',1,10.50;

FOR XML EXPLICIT krok po kroku

Postawmy sobie pierwsze zadanie.

Zadanie: użyć klauzuli FOR XML EXPLICIT w zapytaniu odwołującym się do powyższych dwóch tabel, by uzyskać w wyniku dokument XML o następującej postaci:

<Faktura Numer="04/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaA</Kontrahent>
  <Pozycja NrPozycji="1" Produkt="Produkt1" Cena="100.0000" Ilosc="10" />
  <Pozycja NrPozycji="2" Produkt="Produkt2" Cena="10.0000" Ilosc="2" />
  <Pozycja NrPozycji="3" Produkt="Produkt3" Cena="10.5000" Ilosc="1" />
</Faktura>
<Faktura Numer="05/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaB</Kontrahent>
  <Pozycja NrPozycji="1" Produkt="Produkt1" Cena="10.0000" Ilosc="5" />
  <Pozycja NrPozycji="2" Produkt="Produkt3" Cena="10.5000" Ilosc="1" />
</Faktura>

Rozwiązanie:

SELECT
  1 AS Tag,
  NULL AS Parent,
  F.NrFaktury AS [Faktura!1!Numer],
  F.DataWystawienia AS [Faktura!1!DataWystawienia!ELEMENT],
  F.Kontrahent AS [Faktura!1!Kontrahent!ELEMENT],
  NULL AS [Pozycja!2!NrPozycji],
  NULL AS [Pozycja!2!Produkt],
  NULL AS [Pozycja!2!Cena],
  NULL AS [Pozycja!2!Ilosc]
FROM #Faktury F
UNION ALL
SELECT
  2,
  1,
  P.NrFaktury,
  NULL,
  NULL,
  P.NrPozycji,
  P.Produkt,
  P.Cena,
  P.Ilosc
FROM #Pozycje P
ORDER BY [Faktura!1!Numer], Tag, [Pozycja!2!NrPozycji]
FOR XML EXPLICIT;

Komentarz (krok po kroku):

Aby zrozumieć, jak działa FOR XML EXPLICIT, uruchom najpierw kod powyższego zapytania bez klauzuli FOR XML EXPLICIT. Wynik powinien wyglądać następująco:

image

Kolorami żółtym i zielonym zaznaczyłem obie faktury i ich pozycje (wiersze zawierające dane nagłówkowe są oznaczone ciemniejszymi odcieniami).

Zwróć uwagę na kolejność wierszy. Nieprzypadkowo wiersze pozycji znajdują się tuż za wierszem nagłówka odpowiadającej pozycjom faktury. Sortowanie jest kluczem do sukcesu w zapytaniach z FOR XML EXPLICIT. W tym przypadku dane zostały posortowane względem kolejno: numeru faktury (tu bardzo ważna uwaga – zauważ, że w drugim zapytaniu SELECT wybierającym pozycje faktur także wybieram numer faktury – zapewniam w ten sposób wspólną kolumnę dla wszystkich wierszy wynikowych zapytania), kolumny Tag (o której za chwilę, a dzięki której wiersze nagłówków znajdują się w wyniku przed wierszami pozycji) oraz numeru pozycji (dzięki temu w dokumencie XML pozycje dla każdej faktury będą umieszczone zgodnie z kolejnością na fakturze). To uporządkowanie rekordów jest ważne, ponieważ FOR XML EXPLICIT najpierw buduje taką tabelę, a następnie działa na niej niczym kursor – buduje dokument XML doklejając odpowiednio spreparowane dane z kolejnych wierszy ujęte w elementy i atrybuty. To od kolejności wierszy zależy postać wynikowego dokumentu XML.

Zapytanie składa się z dwóch zapytań SELECT połączonych operatorem UNION ALL. W każdym z dwóch zapytań liczba kolumn jest taka sama (to oczywiste, bo takie są wymagania operatora UNION). Pierwsze dwie kolumny – Tag i Parent - są kolumnami specjalnymi umożliwiającymi poprawne działanie klauzuli FOR XML EXPLICIT. Każde zapytanie otrzymuje swój numer (Tag, wcale nie musi być unikalny) oraz numer “rodzica” (Parent, jeżeli jest NULL, rodzica nie ma). Numery te decydują, gdzie w strukturze wynikowego dokumentu XML zostaną umieszczone dane z poszczególnych wierszy.

Prześledźmy na podstawie dwóch pierwszych wierszy pokazanych poniżej, jak budowany jest wynikowy dokument XML.

image

Na początek brany jest pierwszy rekord. Dla niego w kolumnie Tag widnieje wartość 1. SQL Server szuka w tym wierszu, gdzie znajdują się wartości w kolumnach, które mają znacznik 1 w nazwie (np. Faktura!1!Numer). Nazwy kolumn decydują o tym, do którego Tag-a zalicza się wartość oraz jak będą nazywały się elementy / atrybuty. Jeżeli na końcu nazwy kolumny pojawi się słowo ELEMENT, wartość będzie umieszczona w elemencie (inaczej znajdzie się w atrybucie). Dla pierwszego wiersza SQL Server buduje więc coś takiego:

<Faktura Numer="04/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaA</Kontrahent>

Nazwy kolumn zdecydowały, że elementem nadrzędnym jest <Faktura>, numer faktury znalazł się w atrybucie Numer, data wystawienia faktury w elemencie <DataWystawienia>, a nazwa kontrahenta w elemencie <Kontrahent>.

Następnie brany jest drugi wiersz. W nim w kolumnie Tag SQL Server znajduje wartość 2, zatem uwzględnia w tym wierszu tylko te kolumny, w których nazwach występuje znacznik 2 (np, Pozycja!2!NrPozycji). Ponieważ w kolumnie Parent znajduje się wartość 1, a poprzedni wiersz miał Tag równy 1 (patrz czerwona strzałka na rysunku obrazującym wiersze), SQL Server wie, że XML powstały z bieżącego wiersza należy zagnieździć w XML-u powstałym z poprzedniego wiersza. Dzięki temu dokument zbudowany z dwóch wierszy wygląda tak:

<Faktura Numer="04/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaA</Kontrahent>
  <Pozycja NrPozycji="1" Produkt="Produkt1" Cena="100.0000" Ilosc="10" />

I tak kolejne pozycje będą doklejane wewnątrz znacznika <Faktura>, aż zostanie napotkany wiersz, w którym w kolumnie Tag będzie znajdowała się wartość 1 (będzie to początek następnej faktury). Wówczas element <Faktura> otwarty przy tworzeniu XML-a z pierwszego wiersza zostanie zamknięty, czyli pojawi sie koniec elementu – </Faktura>.

FOR XML EXPLICIT – tips & tricks

A teraz spróbujmy zmodyfikować nieco zadanie.

Zadanie: użyć klauzuli FOR XML EXPLICIT w zapytaniu odwołującym się do tabel #Faktury i #Pozycje, by uzyskać w wyniku dokument XML o następującej postaci:

<Faktura Numer="04/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaA</Kontrahent>
  <Pozycje>
    <Pozycja NrPozycji="1" Produkt="Produkt1" Cena="100.0000" Ilosc="10" />
    <Pozycja NrPozycji="2" Produkt="Produkt2" Cena="10.0000" Ilosc="2" />
    <Pozycja NrPozycji="3" Produkt="Produkt3" Cena="10.5000" Ilosc="1" />
  </Pozycje>
</Faktura>
<Faktura Numer="05/07/2010">
  <DataWystawienia>2010-07-16T00:00:00</DataWystawienia>
  <Kontrahent>FirmaB</Kontrahent>
  <Pozycje>
    <Pozycja NrPozycji="1" Produkt="Produkt1" Cena="10.0000" Ilosc="5" />
    <Pozycja NrPozycji="2" Produkt="Produkt3" Cena="10.5000" Ilosc="1" />
  </Pozycje>
</Faktura>

Różnica w stosunku do poprzedniego zadania polega na tym, że teraz chcemy pozycje mieć otoczone znacznikami <Pozycje> i </Pozycje>.

Rozwiązanie:

SELECT
  1 AS Tag,
  NULL AS Parent,
  F.NrFaktury AS [Faktura!1!Numer],
  F.DataWystawienia AS [Faktura!1!DataWystawienia!ELEMENT],
  F.Kontrahent AS [Faktura!1!Kontrahent!ELEMENT],
  NULL AS [Pozycje!2],
  NULL AS [Pozycja!3!NrPozycji],
  NULL AS [Pozycja!3!Produkt],
  NULL AS [Pozycja!3!Cena],
  NULL AS [Pozycja!3!Ilosc]
FROM #Faktury F
UNION ALL
SELECT
  2,
  1,
  F.NrFaktury,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL
FROM #Faktury F
UNION ALL
SELECT
  3,
  2,
  P.NrFaktury,
  NULL,
  NULL,
  NULL,
  P.NrPozycji,
  P.Produkt,
  P.Cena,
  P.Ilosc
FROM #Pozycje P
ORDER BY [Faktura!1!Numer], Tag, [Pozycja!3!NrPozycji]
FOR XML EXPLICIT;

I teraz zadanie dla Ciebie – spróbuj prześledzić działanie FOR XML EXPLICIT w powyższym kodzie (kieruj się wskazówkami podanymi przeze mnie w pierwszym zadaniu – zakomentuj na przykład klauzulę FOR XML EXPLICIT i uruchom zapytanie ponownie, by zobaczyć, jak układają się wiersze). Powyższy trick możesz stosować wszędzie tam, gdzie chodzi o wygenerowanie dodatkowego elementu, w którym zostaną osadzone elementy z danymi (tak, to jest metoda na wygenerowanie elementu ROOT na SQL Server 2000!).

Podsumowanie

Wszystkich tych, którzy dotąd omijali FOR XML EXPLICIT szerokim łukiem, zachęcam do poeksperymentowania. Sugeruję obejrzeć różne warianty, oglądać, jak zmienia się wynikowy XML w zależności od sortowania i wartości w kolumnach Tag i Parent. Zabawę polecam zwłaszcza osobom, którym przychodzi w pracy generować XML-a na SQL Server 2000. Tam klauzula FOR XML EXPLICIT może być niekiedy jedynym sensownym ratunkiem w sytuacji, gdy mamy wygenerować dokument XML o z góry narzuconej strukturze.

[PL] SQL Server – Monitorowanie logowania a logon triggery

VN:F [1.7.9_1023]
Rating: 5.0/5 (1 vote cast)

Dzisiaj na forum WSS.pl wśród wielu ciekawych wątków o tematyce wokół SQL Servera, znalazł się wątek poświęcony monitorowaniu logowań do instancji SQL Servera loginów o uprawnieniach sysadmina. Autor wątku próbował zaatakować temat używając mechanizmu audytów dostępnego od SQL Server 2008, ale poległ na braku możliwości filtrowania logowań. Doradziłem dwa rozwiązania – logon trigger i Extended Events. Taki przykładowy logon trigger mógłby wyglądać tak:

CREATE TRIGGER trg_LogSysadminLogon
ON ALL SERVER
FOR LOGON
AS
BEGIN
SET NOCOUNT ON
IF IS_SRVROLEMEMBER('sysadmin') = 1
  INSERT INTO master.dbo.SysadminLogon (Sysadmin, LogonDate)
  SELECT SUSER_SNAME(), GETDATE()
END
GO

Komentarz do kodu: logon trigger dość prosty, używając funkcji IS_SRVROLEMEMBER sprawdzamy, czy bieżący login (ten, który próbuje się logować), jest sysadminem i jeżeli jest, zapisujemy jeden wiersz (nazwę logina i datę logowania) w tabeli dbo.SysadminLogon w bazie master.

Już po fakcie zacząłem się jednak zastanawiać, czy nie pospieszyłem się ze swoją poradą i doradzaniem logon triggera. Swego czasu napisałem wspólnie z Markiem Adamczukiem artykuł o logon triggerach dla polskiej strony Technet. W artykule tym wyraźnie zaznaczyliśmy, że istnieje co najmniej kilka przyczyn, z których działanie logon triggera może skutecznie uniemożliwić zalogowanie się użytkownika do serwera baz danych. I tu mnie tknęło. Chcemy mieć mechanizm monitorowania. Coś, co nie ingeruje w sam proces logowania, a jedynie zapisuje informację o tym, że jakiś sysadmin się zalogował. A skoro logon trigger przez swą naturę może skutecznie uniemożliwić owe logowanie (albo na przykład opóźnić je na skutek problemów wydajnościowych operacji w nim zapisanych), niespecjalnie nadaje się na mechanizm bezpiecznego (bezinwazyjnego, jak bym to nazwał) logowania. Wystarczy jeden błąd, jeden brak uprawnień, jeden wykonany w ciele triggera SELECT “w powietrze” (tak, tak, wykonanie zapytania SELECT w logon triggerze też powoduje błąd logowania!) i użytkownik zobaczy komunikat nieciekawej treści:

Logon failed for login ‘jkowalsk’ due to trigger execution.

Changed database context to ‘master’.

Changed language setting to us_english. (Microsoft SQL Server, Error: 17892)

Powiało grozą :-)

I tu Extended Events (w skrócie XE) są jednak mechanizmem o wiele bezpieczniejszym i już wiem, o czym będzie jeden z następnych wpisów na moim blogu. Cierpliwości, XE nadchodzą :-)

[EDYCJA: 2010-09-03]

A jednak wygląda na to, że Extended Events nie nadają się do śledzenia logowania (nie udało mi się znaleźć zdarzenia XE, które dałoby możliwość takiego śledzenia). Słuszną opcją za to wydaje się użycie Event Notifications, które oferują asynchroniczność i brak ingerencji w sam proces logowania.

[/EDYCJA]

[PL] SQL Server – Limity w SQL Server 2008 R2 Express Edition

VN:F [1.7.9_1023]
Rating: 5.0/5 (1 vote cast)

Zajawka nie na temat

Dzień 23 sierpnia 2010 roku – ależ to był poniedziałek! Najpierw z samego rana awaria bloga. Przez chwilę myślałem, że to jakiś atak DDOS albo serwer nie wytrzymał naporu czytelników (czy to możliwe, żeby blog po jednym wpisie stał się aż tak popularny?). Ale kolega z firmy hostującej bloga wyjaśnił mi, że to nie był czynnik ludzki. Grunt, ze blog odżył i duskusja pod najbardziej obleganym wpisem na moim blogu trwała w najlepsze :-) 

Zajawka na temat – użytkownik zasiewa niepewność

Najlepsze jednak nadeszło po południu. Na forum WSS.pl użytkownik lechuCC zapytał o limit pamięci RAM w SQL Server 2008 R2 Express Edition. Udzieliliśmy z Krzyśkiem Stachyrą standardowej odpowiedzi wyczytanej na oficjalnej stronie produktu SQL Server 2008 (ale nie na stronie SQL Server 2008 R2! tam nic na ten temat nie ma, ale przed udzieleniem odpowiedzi koledze nawet nie szukałem…), że limit pamięci wynosi 1GB dla bufora danych. Kolega sprawdzał na wiele sposobów, ale zawsze wychodziło mu blisko 1,4GB, a to jednak trochę więcej niż rzekome 1GB… 

Postanowiłem się przekonać, jaka jest szara rzeczywistość :-) Opis wykonanych czynności przedstawiam poniżej. 

Test szarej rzeczywistości

Na instancji SQL Server 2008 R2 Express Edition, w której wynik zapytania: 

SELECT @@VERSION;

wyglądał tak: 

Microsoft SQL Server 2008 R2 (RTM) – 10.50.1600.1 (X64)
    Apr  2 2010 15:48:46    Copyright (c) Microsoft Corporation
    Express Edition (64-bit) on Windows NT 6.1 <X64> (Build 7600: )
 

założyłem sobie bazę danych o nazwie Test. Powiększyłem plik danych tej bazy do 2GB i plik dziennika transakcji do 100MB (asekuracyjnie). W tejże bazie puściłem w ruch taki kawałek kodu: 

USE Test;
GO
IF OBJECT_ID(N'dbo.t', N'U') IS NOT NULL
  DROP TABLE dbo.t;
GO
CREATE TABLE dbo.t (a char(8000));
GO
INSERT INTO dbo.t (a)
SELECT REPLICATE('a', 8000)
FROM sys.all_objects WHERE is_ms_shipped = 1; -- 1981 wierszy
CHECKPOINT;
GO 100

Po 100 wykonaniach wsadu z poleceniem INSERT mam sporą tabelę – z liczbą wierszy równą 198100, a każdy wiersz rezyduje na osobnej stronie danych (bo wstawiłem tyle danych w jeden wiersz, że drugi już na tych samych 8 kilobajtach się nie zmieści). 

Teraz opróżniam cały bufor danych: 

DBCC DROPCLEANBUFFERS;
GO

I wykonuję zapytanie, którego celem jest wrzucenie do bufora danych wszystkich danych z tabeli dbo.t: 

SELECT COUNT(*) FROM dbo.t; -- wynik: 198100, wykonany został skan tabeli

Zaglądam do bufora zapytaniami, które już prezentowałem na blogu. Na początek informacja, która baza ile megabajtów zajmuje w buforze: 

SELECT
  CASE
    WHEN database_id = 32767 THEN 'mssqlsystemresource'
    ELSE DB_NAME(database_id)
  END AS [Database],
  CONVERT(numeric(38,2),(8.0 / 1024) * COUNT(*)) AS [In buffer cache (MB)]
FROM sys.dm_os_buffer_descriptors
GROUP BY database_id
ORDER BY 2 DESC;
GO

Wynik: 

Database            In buffer cache (MB)
------------------- --------------------
Test                1383.83
tempdb              0.24
mssqlsystemresource 0.19
master              0.06

Czyli że baza Test zajmuje blisko 1,4GB… 

Skoro tak, to przekonajmy się, który obiekt tyle miejsca zajął spośród obiektów z bazy Test: 

USE Test;
GO
SELECT
  QUOTENAME(OBJECT_SCHEMA_NAME(p.object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(p.object_id)) AS Object,
  CONVERT(numeric(38,2),(8.0 / 1024) * COUNT(*)) AS [In buffer cache (MB)]
FROM sys.dm_os_buffer_descriptors AS d
INNER JOIN sys.allocation_units AS u
ON d.allocation_unit_id = u.allocation_unit_id
INNER JOIN sys.partitions AS p
ON (u.type IN (1,3) AND u.container_id = p.hobt_id)
OR (u.type = 2 AND u.container_id = p.partition_id)
WHERE d.database_id = DB_ID()
GROUP BY QUOTENAME(OBJECT_SCHEMA_NAME(p.object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(p.object_id))
ORDER BY 2 DESC;
GO

Wynik: 

Object                   In buffer cache (MB)
------------------------ --------------------
[dbo].[t]                1382.40
[sys].[sysobjvalues]     0.12
[sys].[syscolpars]       0.07
[sys].[sysschobjs]       0.02
[sys].[syssingleobjrefs] 0.02
[sys].[sysiscols]        0.02
[sys].[sysallocunits]    0.01
[sys].[sysrowsets]       0.01
[sys].[sysidxstats]      0.01

I proszę. Jedna duża tabela zajmuje w buforze rzeczone blisko 1,4GB. 

Niespodzianka? Pomyślałem, że może moje zapytania są błędnie napisane. Zatem pora zajrzeć do liczników monitora wydajności (perfmona): 

SELECT
  object_name,
  counter_name,
  cntr_value / 1024 AS cntr_value_MB
FROM sys.dm_os_performance_counters
WHERE object_name LIKE '%Memory Manager%'
AND counter_name IN ('Total Server Memory (KB)', 'Target Server Memory (KB)');
GO

Wynik: 

object_name                           counter_name              cntr_value_MB
------------------------------------- ------------------------- -------------
MSSQL$SQL2008R2EXPRESS:Memory Manager Target Server Memory (KB) 1410
MSSQL$SQL2008R2EXPRESS:Memory Manager Total Server Memory (KB)  1410

Zatem Perfmon mówi, że instancja zajęła 1410 megabajtów na bufor danych i – co ciekawe – tyle właśnie zamierzała… 

Dla pewności jeszcze: 

DBCC MEMORYSTATUS;
GO

Gdzie w wyniku znalazłem między innymi: 

Buffer Pool Value
----------- -----------
Committed   180480
Target      180480

Kalkulator (T-SQL) w ręce i liczymy ile wychodzi z pomnożenia 180480 przez 8 (tyle kilobajtów ma strona danych) i podzielenia wyniku przez 1024 (żeby wynik wyszedł w megabajtach): 

SELECT 8 * 180480 / 1024 AS [Buffer pool in MB]; -- 1410

I w tym momencie już nie mam wątpliwości, że ta instancja wzięła 1,4GB pamięci RAM na bufor danych! A napisano, że miała wziąć nie więcej niż 1GB… 

Jak jedno się nie zgadza…

… to trzeba sprawdzić, czy inny limit opisany w reklamowych folderach jest prawdziwy (do osiągnięcia i nie do przekroczenia). Tym limitem jest 10GB na pliki danych w pojedynczej bazie danych w SQL Server 2008 R2 Express Edition. 

Pierwszy ruch – spróbujmy zwiększyć wielkość pliku do 1MB powyżej owych 10GB: 

USE master;
GO
ALTER DATABASE Test
MODIFY FILE (
  NAME = N'Test',
  SIZE = 10486784KB
);
GO

Odpowiedź serwera: 

Msg 1827, Level 16, State 2, Line 1
CREATE DATABASE or ALTER DATABASE failed because the resulting cumulative database size would exceed your licensed limit of 10240 MB per database.
  

Ok, tak się nie da :-) 

Test kolejny – co będzie, jak wstawimy tyle danych, że plik sam będzie zmuszony urosnąć ponad wymienione 10240MB. Tu po prostu wielokrotnie kopiuję moją dużą tabelę dbo.t z testu bufora danych: 

SELECT * INTO dbo.t1 FROM dbo.t; -- tu jedynie zmieniam numerki na kolejne
GO

W końcu serwer odpowiada: 

Msg 1101, Level 17, State 12, Line 1
Could not allocate a new page for database ‘Test’ because of insufficient disk space in filegroup ‘PRIMARY’. Create the necessary space by dropping objects in the filegroup, adding additional files to the filegroup, or setting autogrowth on for existing files in the filegroup.
 

Z tego wynika, że 10GB jest prawdziwym ograniczeniem rozmiaru danych w pojedynczej bazie danych w SQL Server 2008 R2 Express Edition. 

To jak to w końcu jest?

Jest tak, że jak sami nie sprawdzimy, to może się okazać, że informacje przekazywane drogą poczty pantoflowej mają się nijak do szarej rzeczywistości (i ja przyłożyłem w tym przypadku rękę do powielania, jak się okazuje niekoniecznie prawdziwej, informacji). Jest też tak, że nawet sam Microsoft nie zna ograniczeń swojego produktu, bo ludzie z Redmond proszeni o komentarze do zademonstrowanego tu testu nie bardzo wiedzą, jak sensownie to wytłumaczyć (próbują wymyślać wersje zeznań mówiące, że niby jakieś tam systemowe obiekty i tajemnicze pule systemowe zajmują dodatkowe megabajty w buforze), ale obstają przy wersji, że limit dla bufora nadal wynosi 1GB! Może by nie było o co robić szumu, ale… Ale w edycji Express liczy się każdy megabajt, a tu najpewniej mamy nieścisłość rzędu 40% :-) A to już podstawa do zadania sobie pytania – czy mogę wykorzystać te oddane do dyspozycji “za darmo” 400MB bufora? Skoro w Microsoft o nich nie wiedzą, to pewnie mogę, bo skąd będą wiedzieli, że wykorzystuję coś, o istnieniu czego nie wiedzą ;-) 

Acha, i ciekawostka na koniec – dla SQL Server 2008 R2 Express Edition można śmiało ustawić ‘max server memory (MB)’ na 2048 ;-) Ani SQL Server, ani Management Studio ani pisną, choć taki limit jest bez sensu, bo instancja nie użyje więcej niż 1GB… tfu, wróć… 1,4GB pamięci na bufor danych :-) 

PS. Za chwilę instaluję SQL Server 2008 Express Edition i powtarzam test. Dopiszę obserwacje do tego wpisu. Kto wie, może i na poprzedniej wersji rzeczony limit nie wynosi 1GB ;-) 

[EDYCJA: 2010-08-24]  

Ponieważ Łukasz Grala już zrobił test na SQL Server 2005 Express Edition (patrz jego komentarz do tego wpisu) i wygląda na to, że sytuacja jest analogiczna, nie będę testował, jak to jest na SQL Server 2008 Express Edition. 

[/EDYCJA]

[PL] SQL Server – Czy optymalizator może policzyć wiersze w zmiennej tabelarycznej?

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Przyjęło się i przekazywane jest w środowisku stwierdzenie, że w przypadku, gdy w zapytaniu użyta jest zmienna tabelaryczna (zwana przez niektórych “tabelą małpkową”), optymalizator zawsze zakłada/widzi w zmiennej tabelarycznej dokładnie jeden wiersz. Z tego powodu plany wykonania są dalekie od oczekiwanych, przez co musimy sterować planami (np. jawnie określając kolejność tabel w złączeniach).

Postanowiłem troche poszperać w temacie.

Spróbujmy uruchomić taki kawałek kodu T-SQL:

DECLARE @t TABLE (c1 int PRIMARY KEY);
DECLARE @x TABLE (c2 int PRIMARY KEY);
INSERT INTO @t SELECT DISTINCT number FROM master.dbo.spt_values;
INSERT INTO @x SELECT TOP 1 number FROM master.dbo.spt_values;
SELECT * FROM @t t INNER JOIN @x x ON t.c1 = x.c2 OPTION (RECOMPILE); --(1)
SELECT * FROM @t t INNER JOIN @x x ON t.c1 = x.c2;                    --(2)

Zapytania (1) i (2) są złączeniami dwóch zmiennych tabelarycznych – @x – zawierającej 1 wiersz oraz @t – zawierającej ponad 2000 wierszy.

Dla zapytań (1) i (2) oglądamy dwa różne plany wykonania (ale nie plany estymowane, a plany rzeczywiste) o tym samym koszcie:

imagePierwsza obserwacja - w obu przypadkach kolejność zmiennych tabelarycznych w złączeniu była różna (w przypadku zapytania (1) z opcją RECOMPILE kolejność była “prawidłowa” – skanowanie zmiennej zawierającej mniejszą ilość wierszy i wyszukanie rekordów w zmiennej “większej”).

Zobaczmy w planach wykonania szczegóły operatorów użytych w stosunku do zmiennej @t.

image

W zapytaniu (2) w operatorze Clustered Index Scan liczba przy pozycji Actual Number of Rows (rzeczywista liczba wierszy)  zgadza się z rzeczywistością (2164), zaś w przypadku zapytania (1) używającego opcji RECOMPILE (“rekompiluj zapytanie i nie używaj planu z cache’u”) w operatorze Clustered Index Seek liczba obok Acutal Number of Rows wynosi 1. Skąd w drugim planie wzięła się poprawna informacja o rzeczywistej liczbie wierszy? Odpowiedź przychodzi, gdy spojrzymy na całość planu wykonania. W takich sytuacjach liczba wierszy z tabeli “zewnętrznej” (Outer) jest równa ilości iteracji pętli Nested Loops – 2164 to właśnie ilość “obrotów pętli”.  Nadal jednak estymowana ilość wierszy dla tych zmiennych wynosi 1 (słownie: jeden). A z tego wynika, że – z klauzulą OPTION (RECOMPILE) czy bez niej – optymalizator zakłada, że w zmiennej tabelarycznej jest zawsze 1 rekord. Swego rodzaju ciekawostką są różne plany wykonania obu przedstawionych zapytań, ale równie dobrze może to być przypadek lub bug (dość powtarzalny).

W tym miejscu przyznaję, że gdyby nie Maciek Pilecki, chodząca księga wiedzy o zagadnieniach optymalizacji SQL Servera, nie wpadłbym na oczywistą pomyłkę, którą popełniłem pisząc ten wpis w jego pierwotnej postaci. Co się mianowicie stało? Ano, jakimś cudem pomyliłem plany wykonania obu zapytań i tak się tym zasugerowałem, że pomyślałem, iż klauzula OPTION (RECOMPILE) umożliwia dokładne policzenie rekordów w zmiennej tabelarycznej SQL Serverowi. Oczywiście mój błąd. Dobrze, że Maciek był na posterunku i mogłem dzięki niemu poprawić ten wpis oraz swój krzywy światopogląd, a przy okazji jeszcze czegoś się nauczyć (dzięki, Maćku).

[PL] Skryptowanie w SQL Server 2008 – Proste typy danych użytkownika

VN:F [1.7.9_1023]
Rating: 5.0/5 (1 vote cast)

Intro

Typy danych użytkownika tworzymy zazwyczaj z myślą, by ewentualne zmiany długości, precyzji czy skali przeprowadzać w sposób prostszy, bo scentralizowany. Niestety, Microsoft nie daje nam w SQL Server polecenia ALTER TYPE, ale i tak używamy własnych typów, a w razie potrzeby jakiejkolwiek zmiany przeprowadzamy operację opisaną tutaj: [EN] Call for voting – ALTER TYPE in SQL Server. W tej notce pokażę, jak można skryptować proste typy danych stworzone przez użytkowników w bazie danych. Nie będę opisywał skryptowania typów tabelarycznych i typów stworzonych w CLR.

Skryptowanie typów prostych

Zadanie: zeskryptować wszystkie proste typy danych użytkownika w bazie danych.

Dla każdego typu trzeba stworzyć kod sprawdzający, czy typ nie istnieje (trzeba zajrzeć do widoku sys.types) oraz wygenerować kod polecenia CREATE TYPE o składni jak poniżej (na podstawie Books Online):

CREATE TYPE [ schema_name. ] type_name
FROM base_type   [ ( precision [ , scale ] ) ] [ NULL | NOT NULL ];

Rozwiązanie:

USE AdventureWorks2008R2;
SET NOCOUNT ON;
SELECT
  'IF NOT EXISTS (
  SELECT * FROM sys.types
  WHERE [schema_id] = SCHEMA_ID(' +
  QUOTENAME(SCHEMA_NAME([schema_id]), '''') + ') AND name = N' +
  QUOTENAME(name, '''') + '
)
  CREATE TYPE ' + QUOTENAME(SCHEMA_NAME([schema_id])) + '.' + QUOTENAME(name) + '
  FROM ' + TYPE_NAME(system_type_id) +
  CASE
    WHEN system_type_id IN (
      165, --varbinary
      167, --varchar
      173, --binary
      175, --char
      231, --nvarchar
      239  --nchar
    )
      THEN '(' +
        CONVERT(
          varchar(4),
          CASE
            WHEN max_length = -1
              THEN 'max'
            ELSE max_length
          END
        ) + ')'
    WHEN system_type_id IN (
      41, --time
      42, --datetime2
      43  --datetimeoffset
    )
      THEN '(' + CONVERT(char(1), scale) + ')'
    WHEN system_type_id IN (
      106, --decimal
      108  --numeric
    )
      THEN '(' +
        CONVERT(varchar(2), precision) + ',' +
        CONVERT(varchar(2), scale) + ')'
    WHEN system_type_id = 62 --float
      THEN '(' + CONVERT(varchar(2), precision) + ')'
    ELSE ''
  END + ' ' +
  CASE
    WHEN is_nullable = 0
      THEN 'NOT NULL'
    ELSE 'NULL'
  END + ';
GO'
FROM sys.types
WHERE is_user_defined = 1 --typ użytkownika
AND is_table_type = 0     --nie typ tabelaryczny
AND is_assembly_type = 0; --nie typ CLR
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • metadane typów zwraca widok sys.types (ale metadane typów tabelarycznych są też w sys.table_types),
  • typy też mają dwuczłonowe nazwy (znajdują się w schematach) – stąd użycie funkcji SCHEMA_NAME,
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi i apostrofami,
  • do uzyskania nazwy typu systemowego użytego na potrzeby danego typu użytkownika wykorzystałem funkcję TYPE_NAME wywołaną na identyfikatorze typu systemowego trzymanym w kolumnie system_type_id w widoku sys.types,
  • przy niektórych typach systemowych niezbędne jest podanie maksymalnej długości (typy binarne i tekstowe),  dokładności (data i czas), precyzji (float) albo precyzji i skali (decimal/numeric),
  • dzięki kolumnie is_nullable z widoku sys.types mogłem określić, czy typ pozwala domyślnie na przechowywanie wartości NULL, czy nie,
  • w klauzuli WHERE odfiltrowałem typy systemowe, typy tabelaryczne i typy CLR.

Słowo podsumowania

Skryptowanie typów prostych jest stosunkowo łatwe. W zasadzie wszystkie niezbędne metadane są do odczytania z jednego widoku systemowego (sys.types). A przy okazji można się choćby oswoić z niekoniecznie oczywistym faktem – typy też są trzymane w schematach. Skryptowanie pozostałych typów użytkownika (tabelarycznych i CLR) jest już nieco trudniejsze. Ale to temat na być może kolejny wpis na moim blogu :-)

[EDYCJA] Dziękuję Krzysiowi Stachyrze, który wypatrzył w tym wpisie czeski błąd – oczywiście SQL Server nie daje polecenia ALTER TYPE. ALTER TABLE na szczęście mamy :-) [/EDYCJA]

[PL] Skryptowanie w SQL Server 2008 – Obiekty proceduralne

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Pilot

Umiejętność korzystania z metadanych w celu skryptowania obiektów w bazach danych (tabel, procedur, itd.) przydaje się w życiu administratora i programisty baz danych. Sam wykorzystuję skryptowanie do cyklicznego tworzenia kopii kodu T-SQL obiektów w bazach danych (na wypadek, gdyby akurat okazało się, że w bazie uszkodzeniu uległ któryś z obiektów przechowujących metadane). Zapytasz, czemu nie użyć do tego mechanizmu skryptowania oferowanego przez Management Studio? Otóż skryptowanie z użyciem własnego kodu T-SQL daje mi więcej swobody, a ponadto skryptowanie w Management Studio ma swoje wady (patrz tutaj: [EN] SQL Server – Don’t (sp_)rename your database objects) i na dodatek często występują spore problemy w przypadku, gdy baza danych zawiera sporo obiektów (jakoś listowanie pokaźnej listy obiektów nie wychodzi narzędziu Management Studio – często aplikacja się zawiesza albo odpowiada wyjątkiem klasy OutOfMemoryException – patrz tutaj: Dziennik pokładowy MSSQL 2008 (wpis 0×0011) – Problem z Generate Script…).

Dlatego postanowiłem napocząć na blogu nową serię wpisów poświęconych tworzeniu własnego kodu T-SQL do skryptowania. Zaczynam od skryptowania obiektów proceduralnych – funkcji, procedur, widoków i wyzwalaczy. Potem spróbuję pokazać, jak można skryptować typy danych użytkownika, więzy, obiekty CLR i tabele. Tam, gdzie ilość kodu nie będzie wykraczała poza możliwości percepcji, będę się starał pokazać pełne rozwiązanie, ale na przykład w przypadku tabel już wiem, że nie będę podawał pełnego rozwiązania, a tylko podsunę kawałek kodu i parę pomysłów na dobry początek :-)

Kod będę tworzył z myślą o skryptowaniu w SQL Server 2008 i SQL Server 2008 R2. Przy odrobinie wysiłku kod będzie do zaadoptowania na potrzeby SQL Server 2005.

Funkcje, procedury, widoki

Zadanie: zeskryptować wszystkie funkcje, procedury i widoki w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @type char(2),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    SCHEMA_NAME(o.[schema_id]),
    o.name,
    o.type_desc COLLATE database_default,
    o.type COLLATE database_default,
    m.definition
  FROM sys.objects AS o
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = o.[object_id]
  WHERE o.type IN ('FN', 'IF', 'P', 'TF', 'V')
  AND o.is_ms_shipped = 0
  ORDER BY SCHEMA_NAME(o.[schema_id]), o.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @type,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@schema_name) + '.' +
    QUOTENAME(@object_name), '''', ''''''
  ) + '''';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF OBJECT_ID(N''' +
      REPLACE(
        QUOTENAME(@schema_name) + '.' +
        QUOTENAME(@object_name), '''', ''''''
      ) + ''', N''' + (@type) + ''') IS NOT NULL';
    INSERT INTO @SQL (Line) SELECT '  DROP ' +
      CASE
        WHEN @type = 'P' THEN 'PROCEDURE'
        WHEN @type IN ('FN', 'IF', 'TF') THEN 'FUNCTION'
        ELSE 'VIEW'
      END + ' ' +
      QUOTENAME(@schema_name) + '.' +
      QUOTENAME(@object_name) + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @type,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • definicje obiektów są trzymane w widoku sys.sql_modules w kolumnie definition, wyjątek stanowią obiekty z opcją WITH ENCRYPTION użytą w definicji (takie obiekty są “szyfrowane”),
  • listę obiektów stworzonych przez użytkowników w bazie danych zwraca widok sys.objects (ale jest też widok sys.all_objects, w którym dodatkowo są zawarte metadane obiektów systemowych),
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • zapytanie filtruje obiekty wg typu – wybiera funkcje trzech rodzajów, procedury i widoki, jest też filtr odrzucający ewentualne obiekty systemowe (kolumna is_ms_shipped w widoku sys.objects),
  • przed definicją każdego obiektu dorzucam kod wykrywający obecność obiektu w bazie danych i usuwający obiekt w przypadku, gdy takowy istnieje,
  • kolumny type i type_desc z widoku sys.objects mają na sztywno przypisane collation, stąd użycie funkcji COLLATE (inaczej nie dałoby się sklejać tych kolumn z tekstem, gdy collation bazy jest inne niż collation wspomnianych kolumn),
  • operuję zawsze dwuczłonowymi nazwami obiektów (dzięki temu nazwa jest jednoznaczna w kontekście bazy danych) – do uzyskania nazwy schematu obiektu wykorzystuję funkcję SCHEMA_NAME,
  • funkcja REPLACE jest przeze mnie użyta do “wyescape’owania” apostrofów w nazwach obiektów (swoją drogą, jak ktoś używa niestandardowych identyfikatorów zawierających dziwne znaczki i/lub słowa kluczowe, sam prosi się o kłopoty),
  • funkcja QUOTENAME pozwala otoczyć identyfikatory nawiasami kwadratowymi – zabezpieczam się przed niestandardowymi identyfikatorami.

Wyzwalacze na poziomie bazy

Zadanie: zeskryptować wszystkie wyzwalacze w wybranej bazie danych.

Rozwiązanie:

-- wyzwalacze DML i DDL (ON DATABASE)
USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @parent_class int,
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    OBJECT_SCHEMA_NAME(t.[object_id]),
    t.name,
    t.type_desc COLLATE database_default,
    t.parent_class,
    m.definition
  FROM sys.triggers AS t
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY OBJECT_SCHEMA_NAME(t.[object_id]), t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @parent_class,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    ISNULL(QUOTENAME(@schema_name) + '.', '') +
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.triggers WHERE name = N''' +
      REPLACE(
        ISNULL(QUOTENAME(@schema_name) + '.', '') +
        QUOTENAME(@object_name),
        '''',
        ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      ISNULL(QUOTENAME(@schema_name) + '.', '') +
      QUOTENAME(@object_name) +
      CASE WHEN @parent_class = 0 THEN ' ON DATABASE' ELSE '' END + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @parent_class,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy w bazie danych zwraca widok sys.triggers,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • drobnym wyzwaniem było rozróżnienie wyzwalaczy DML i DDL – każdy z tych typów wyzwalaczy wymaga nieco innego kodu przy usuwaniu, posłużyłem się kolumna parent_class z widoku sys.triggers,
  • ponieważ w widoku sys.triggers nie ma kolumny schema_id, musiałem użyć funkcji OBJECT_SCHEMA_NAME do wyłuskania nazwy schematu,
  • przy okazji taka obserwacja – wyzwalacz DML można utworzyć tylko w tym samym schemacie, w którym znajduje się nadrzędna tabela/widok.

Wyzwalacze na poziomie serwera

Zadanie: zeskryptować wszystkie wyzwalacze DDL na poziomie serwera.

Rozwiązanie:

USE master;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @object_name sysname,
  @type_desc nvarchar(60),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    t.name,
    t.type_desc COLLATE database_default,
    m.definition
  FROM sys.server_triggers AS t
  INNER JOIN sys.server_sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @object_name,
  @type_desc,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.server_triggers WHERE name = N''' +
      REPLACE(
        @object_name, '''', ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      QUOTENAME(@object_name) + ' ON ALL SERVER;'

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @object_name,
    @type_desc,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy na poziomie serwera zwraca widok sys.server_triggers,
  • definicje widoków na poziomie serwera znajdują się w kolumnie definition widoku sys.server_sql_modules,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • tu było bodaj najłatwiej, ponieważ wyzwalacze na poziomie serwera nie mają przypisanego schematu.

Słowo podsumowania

Skryptowanie pojedynczego obiektu proceduralnego to nie problem. SQL Server oferuje procedurę sp_helptext oraz funkcję OBJECT_DEFINITION. Ale jeżeli chcemy hurtowo skryptować obiekty, podany w powyżej kod może okazać się przydatny. Można na jego podstawie zbudować własne procedury do skryptowania i używać ich na codzień. Tu polecam wpis z mojego starego bloga:  [PL][util] Piszemy narzędzia w T-SQL cz. 1 – sp_getcolumns.

Zachęcam do zabawy z metadanymi SQL Servera. Można się dzięki takiej zabawie sporo nauczyć. I oczywiście zachęcam do komentowania tego wpisu. Może masz jakieś uwagi, coś nie gra w podanym tu kodzie albo masz lepszy pomysł na realizację takich zadań.

[EDYCJA] To już druga wersja tego wpisu. W pierwszej kod T-SQL nie bazował na kursorach, a definicja każdego obiektu była zwracana w postaci jednej wartości typu nvarchar(max), co powodowało problem w przypadku, gdy definicja obiektu była dłuższa niż 8192 znaki (a nawet mniej, gdyż Management Studio domyślnie wyświetla w kolumnie bodajże do 255 znaków). Druga wersja powstała dzięki czujności (po raz kolejny) Krzysia Stachyry. Krzysiu, wielki szacunek za cierpliwe testowanie moich wypocin! Tym razem powinno być ciut lepiej :-) [/EDYCJA]

[PL] SQL Server 2008 – Statystyki procedur i wyzwalaczy

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Geneza

Niedawno na forum portalu WSS.pl użytkownik miscu zapytał, w jaki sposób znaleźć datę ostatniego wykonania procedury składowanej na SQL Server 2005. Odpowiedziałem wówczas na jego pytanie, ale potem sprawdziłem, czy w SQL Server 2008 nie dałoby się uzyskać tej informacji nieco łatwiej, bez “grzebania” w kilku widokach i wykonywania podejrzanych złączeń w nadziei, że otrzymany wynik będzie choć trochę przedstawiał prawdę prawdziwą. I owszem, można.

Statystyki procedur

W SQL Server 2008 (i oczywiście w R2 także) dostajemy ciekawe widoki DMV – sys.dm_exec_procedure_stats i sys.dm_exec_trigger_stats. Widoki te pokazują statystyki wykonania odpowiednio procedur składowanych i wyzwalaczy. Oba widoki są widokami globalnymi, tzn. przechowują statystyki dla wszystkich baz danych naraz (nawet dla bazy mssqlresource coś tam się znajdzie). Oba widoki bazują na danych zebranych od ostatniego uruchomienia instancji SQL Server i bazują na tym, co siedzi w cache’u planów wykonania. Napisałem kawałek kodu (pewnie nie ja pierwszy), który wybiera co ciekawsze dane dotyczące procedur składowanych:

SELECT
  DB_NAME(s.database_id) AS [database_name],
  QUOTENAME(OBJECT_SCHEMA_NAME(s.[object_id], s.database_id)) +
  '.' +
  QUOTENAME(OBJECT_NAME(s.[object_id], s.database_id)) AS [procedure_name],
  s.cached_time,
  s.last_execution_time,
  s.execution_count,
  CONVERT(
    decimal(38,2),
    (s.execution_count * 1.0 / t.uptime)
  ) AS executions_per_minute,
  CONVERT(
    decimal(38,2),
    (s.max_elapsed_time * 1.0 / 1000)
  ) AS max_execution_time_miliseconds,
  CONVERT(
    decimal(38,2),
    ((s.total_elapsed_time * 1.0 / 1000) / s.execution_count)
  ) AS avg_execution_time_miliseconds
FROM sys.dm_exec_procedure_stats AS s
CROSS JOIN (
  SELECT
    DATEDIFF(minute, sqlserver_start_time, GETDATE()) AS uptime
  FROM sys.dm_os_sys_info
) AS t
WHERE s.database_id <> 32767;

Krótki komentarz do kodu:

  • wyświetlane dane zawierają:
    • database_name – nazwę bazy danych, w której znajduje się procedura,
    • procedure_name - dwuczłonową nazwę procedury,
    • cached_time – datę zapisania planu procedury w cache’u,
    • last_execution_time – datę ostatniego wykonania procedury,
    • execution_count – ilość wykonań procedury od ostatniego restartu usługi SQL Servera,
    • executions_count_per_minute – ilość wykonań procedury na minutę od ostatniego restartu usługi SQL Servera,
    • max_execution_time_miliseconds – maksymalny czas wykonania procedury wyrażony w milisekundach,
    • avg_execution_time_miliseconds – średni czas wykonania procedury wyrażony w milisekundach.
  • wszystkie czasy w widoku sys.dm_exec_procedure_stats są wyrażone w mikrosekundach, stąd operacje dzielenia przez 1000,
  • datę startu instancji wyciągam z widoku DMV sys.dm_os_sys_info (polecam do niego zajrzeć, bo w jednym wierszu zawiera sporo ciekawych informacji),
  • w klauzuli WHERE odfiltrowuję obiekty z ukrytej bazy systemowej mssqlresource, bo i tak na nic nam informacje o procedurach z tej bazy :-)

Zamiast filtru eliminującego obiekty z mssqlresource, można pokusić się o filtrowanie względem bazy, w której chcemy optymalizować procedury:

… WHERE s.database_id = DB_ID('MojaBazaDanych');

Przykładowy fragment wyniku powyższego zapytania:

image

Znajdź nieużywane procedury

Widok sys.dm_exec_procedure_stats może się też przydać w scenariuszu, w którym w bazie danych zawierającą dużą ilość procedur chcemy odnaleźć te procedury, które nie są wykorzystywane (nie zostały użyte od ostatniego uruchomienia serwera). Do tego celu można użyć na przykład takiego zapytania (tu – szukam procedur nieużywanych w bazie AdventureWorks2008R2):

USE AdventureWorks2008R2; -- tu wstaw nazwę Twojej bazy danych
GO
SELECT
  QUOTENAME(SCHEMA_NAME(p.[schema_id])) +
  '.' +
  QUOTENAME(p.name) AS unused_procedure
FROM sys.procedures AS p
INNER JOIN sys.sql_modules AS m
ON p.[object_id] = m.[object_id]
LEFT JOIN sys.dm_exec_procedure_stats AS s
ON s.database_id = DB_ID() AND p.[object_id] = s.[object_id]
WHERE s.[object_id] IS NULL AND m.is_recompiled = 0;
GO

Jeżeli serwer nie był restartowany przez odpowiednio długi czas i wiesz, że zostały przeprowadzone przypadki biznesowe wykorzystujące procedury, możesz założyć, że procedury wylistowane przez powyższe zapytanie wymagają sprawdzenia, czy są do czegokolwiek potrzebne (od polityki firmy i solidności programistów / administratorów zależy, jak często zdarzy się, że ktoś założy na bazie produkcyjnej tymczasowe obiekty, a następnie zapomni po sobie posprzątać). Celowo w powyższym zapytaniu sięgam do widoku sys.sql_modules, gdyż jest tam informacja o tym, czy procedura została utworzona z opcją WITH RECOMPILE (kolumna is_recompiled). A jeśli tak jest, jej planu nie znajdziesz w cache’u i nie możesz się sugerować jej brakiem w widoku sys.dm_exec_procedure_stats.

Słowo o wyzwalaczach

Analogiczne informacje można uzyskać także dla wyzwalaczy. I to nie tylko dla wyzwalaczy DML, ale także DDL (w tym dla logon triggerów). Do wykorzystania oprócz DMV sys.dm_exec_trigger_stats są widoki:

  • sys.triggers – zwraca metadane wyzwalaczy na poziomie bieżącej bazy danych (DML i DDL),
  • sys.server_triggers – zwraca metadane wyzwalaczy DDL na poziomie serwera (w tym logon triggerów).

[PL] SQL Server – Zabijanie sesji “wiszących” w statusie KILLED/ROLLBACK

VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Zdarza się, że z poziomu SQL Servera wykonujemy operacje w systemie operacyjnym Windows i w pewnym momencie zabijamy poleceniem KILL z poziomu SQL Servera sesję, która owe operacje prowadzi. Nierzadko prowadzi to do sytuacji, w której zabita sesja na długo (a czasem w nieskończoność) pozostaje na liście sesji w master.dbo.sysprocesses (cmd = ‘KILLED/ROLLBACK’) lub w widoku dynamicznym sys.dm_exec_requests (command = ‘KILLED/ROLLBACK’).

Przykład:

Wykonujemy zapytania (analogiczne zapytania do DMV i sysprocesses) i oglądamy “wiszącą” sesję o @@SPID = 52:

SELECT
  r.session_id,
  r.command,
  s.host_process_id
FROM sys.dm_exec_requests AS r
INNER JOIN sys.dm_exec_sessions AS s
ON r.session_id = s.session_id
WHERE r.command = 'KILLED/ROLLBACK';
GO
SELECT
  spid,
  cmd,
  hostprocess
FROM master.dbo.sysprocesses
WHERE cmd = 'KILLED/ROLLBACK';
GO

Wynik:

session_id command          host_name host_process_id
---------- ---------------- --------- ---------------
52         KILLED/ROLLBACK  CLIENT1   3720

spid   cmd              hostname hostprocess
------ ---------------- -------- -----------
52     KILLED/ROLLBACK  CLIENT1  3720

Jak sobie poradzić z taką “wiszącą” sesją? Nie bez przyczyny w zapytaniach wybrałem kolumny host_process_id i hostprocess. Obie pokazują ID procesu (tu – 3720) na maszynie CLIENT1. Aby sesja zniknęła i przestała zatruwać nam życie, należy na wskazanej maszynie zabić proces o wskazanym ID. Jeżeli maszyna (host) to serwer hostujący instancję SQL Server, na której “wisi” sesja, można spróbować użyć procedury rozszerzonej xp_cmdshell (o ile jest włączona i o ile z poziomu SQL Servera da się na tej maszynie zabić proces w systemie Windows – kwestia uprawnień):

EXEC master.dbo.xp_cmdshell 'taskkill /PID 3720';

Bardziej jednak prawdopodobne, że będzie potrzebna interwencja administratora Windows (domeny?). W każdym razie, przynajmniej wiadomo, co trzeba zrobić, by uniknąć operacji, która biednemu DBA wydaje się często jedynym ratunkiem – restartu usługi SQL Servera.