SQL Server
[PL] Skryptowanie w SQL Server 2008 – Klucze obce
Sep 6th
Intro
To już trzeci wpis z serii “Skryptowanie w SQL Server 2008″. Dwa poprzednie dostępne są tu:
- [PL] Skryptowanie w SQL Server 2008 – Obiekty proceduralne
- [PL] Skryptowanie w SQL Server 2008 – Proste typy danych użytkownika
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
Sep 4th
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.
Przyjemnej zabawy z danymi geograficznymi :-)
[PL] SQL Server – FOR XML EXPLICIT (nie) dla opornych
Aug 30th
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:
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.
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
Aug 26th
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:
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
Aug 23rd
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:
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:
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:
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?
Aug 23rd
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:
Pierwsza 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.
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] SQL Server 2008 – Statystyki procedur i wyzwalaczy
Aug 2nd
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:
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
Jul 28th
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.








