Posts tagged administracja
[PL] SQL Server – Przerwany łańcuch backupów różnicowych
Jun 3rd
Ostatnio po raz pierwszy w życiu spotkałem się z przypadkiem przerwanego łańcucha backupów różnicowych w SQL Server. Zjawisko może i ciekawe, ale potrafi mocno zestresować DBA. Jak to wygląda i w czym problem?
Wyobraźmy sobie scenariusz. Masz hurtownię danych, VLDB o rozmiarze ponad 1TB. Baza pracuje w SIMPLE recovery model. Wykonanie jej pełnego backupu zajmuje jakieś 2-3 godziny. Backup pełen bazy został zrobiony kilka godzin wcześniej. Teraz po dokonaniu pewnych zmian w konfiguracji bazy decydujesz się na wykonanie backupu różnicowego (differential backup). Ale przy próbie wykonania takiego backupu pojawia się komunikat:
Msg 3035, Level 16, State 1, Line 1 Cannot perform a differential backup for database "DataWarehouse", because a current database backup does not exist. Perform a full database backup by reissuing BACKUP DATABASE, omitting the WITH DIFFERENTIAL option. Msg 3013, Level 16, State 1, Line 1 BACKUP DATABASE is terminating abnormally.
Ale jak to? Przecież Ty wiesz, że pełen backup bazy został wykonany i leży sobie na dysku! Co się wydarzyło przez te parę godzin, że SQL Server nie pozwala Ci wykonać kopii różnicowej? Pierwszą podpowiedź znajdziesz w errorlogu SQL Servera. W czasie, gdy wykonywał się pełen backup zapewne pojawiły się w errorlogu komunikaty w stylu:
I/O is frozen on database DataWarehouse. No user action is required. However, if I/O is not resumed promptly, you could cancel the backup.
Powyżej pewnie zobaczysz (pewnie nawet zalogowane w tej samej sekundzie):
I/O was resumed on database DataWarehouse. No user action is required.
Hmmm… Pierwsza myśl – coś z dyskiem :-) Ale to nie to.
Sprawdźmy historię backupów naszej bazy danych (nie pamiętam, skąd wziąłem skrypt, ale w razie czego można posłużyć się fajnym kawałkiem kodu z bloga Roberta Kubalskiego):
SELECT bs.backup_finish_date, DATEDIFF(second, bs.backup_start_date, bs.backup_finish_date) AS time_taken_sec, CASE bs.type WHEN 'D' THEN 'Full' WHEN 'L' THEN 'Log' WHEN 'I' THEN 'Differential' ELSE '' END AS backup_type, CONVERT(decimal(15, 2), bs.backup_size / (1024. * 1024)) AS backup_size_mb, bmf.physical_device_name FROM msdb.dbo.backupmediafamily as bmf INNER JOIN msdb.dbo.backupset as bs ON bmf.media_set_id = bs.media_set_id WHERE (CONVERT(datetime, bs.backup_start_date, 102) >= GETDATE() - 7) AND bs.database_name = N'DataWarehouse' ORDER BY bs.backup_finish_date DESC;
Przykładowy wynik:
Co widzimy? Ano, zapewne wśród wyświetlonych informacji pojawią się backupy, które w kolumnie physical_device_name będą miały dziwaczne “szlaczki” w stylu {CBE47A8D-BF0C-4C9E-B3D4-841E16793FA8}10. Ponadto, owe podejrzane backupy będą miały równie podejrzany maleńki rozmiar (w prezentowanym powyżej przykładowym wyniku pełen backup bazy o rozmiarze 1GB zajął ponoć 0.01MB). O czym świadczą te objawy (komunikaty w errorlogu, dziwne wpisy w historii backupów oraz problem z wykonaniem backupu differential)?
Problem okazał się powszechnie (?) znany i dość wiekowy, bo nawet doczekał się swojego KB: http://support.microsoft.com/kb/903643 (a jak poszukać, to jest pewnie sporo pokrewnych KB, np. http://support.microsoft.com/kb/937683). A więc wszystkiemu winien jest działający spod systemu operacyjnego program do robienia kopii zapasowych plików, który potrafi używać Volume Shadow Copy do backupowania plików baz danych nawet, gdy są one używane przez SQL Server. Niestety, robi to kosztem backupów natywnych SQL Servera. Jeśli zdarzy się, że oba backupy – natywny backup w SQL Serverze i backup z poziomu systemu operacyjnego (niekoniecznie ntbackup, bo okazuje się, że narzędzia firm trzecich też używają tego samego mechanizmu) – wykonują się w tym samym czasie, może zostać przerwany łańcuch backupów różnicowych i nie będziesz w stanie zrobić backupu differential. A to może być czasem fatalne w skutkach. Rozwiązaniem oczywistym jest – bazy danych SQL Servera backupować wyłącznie natywnym mechanizmem (z kodu T-SQL) lub narzędziami dedykowanymi do backupowania baz SQL Servera. Jednak czasem nie da się wytłumaczyć administratorowi sieci korporacyjnej, że nie powinien używać innych narzędzi do backupowania plików baz danych. Wówczas pomocne może okazać się wyłączenie usługi SQL Server VSS Writer, dzięki której możliwe jest właśnie backupowanie plików działających baz przez “windowsowe” narzędzia.
Na koniec wypadałoby napisać, jak wykrywać takie anomalie. Próbowałem przeglądać strony DIFF bazy potraktowanej narzędziem NTBackup, ale nie znalazłem żadnej (nie-)prawidłowości, która sugerowałaby, że łańcuch backupów differential jest przerwany. Jedyne, co przychodzi mi więc do głowy, to sprawdzać w historii backupów danej bazy, czy physical_device_name jednego z ostatnich backupów zawiera coś innego niż ścieżkę do pliku. Jeśli zamiast ścieżki widzisz wspomniane wyżej “krzaki”, to może się okazać, że właśnie dotknął Cię opisany problem. Jeśli ktoś ma lepszy pomysł na detektor zerwanego łańcucha backupów differential, niech się podzieli :-)
[PL] SQL Server dla DBA – Zdań kilka o SQL Trace
Apr 12th
Pomyślałem, że napiszę krótki wpis o SQL Trace, czyli o mechanizmie, który powinien znaleźć się w arsenale każdego DBA pracującego z SQL Serverem.
Namierzyć Profilera
Zakładam, że każdy DBA prędzej czy później musi nauczyć się używać aplikacji SQL Server Profiler. Czasem jednak śledzenie przy użyciu Profilera nie jest mile widziane na serwerze produkcyjnym. Zwłaszcza, jeśli do tego dodać, że śledzenie może odbić się negatywnie na wydajność serwera. Niejednokrotnie zdarzało mi się, że przechwytując Profilerem za dużo zdarzeń (lub zbyt ciężkich zdarzeń – np. plany wykonania), obserwowałem “zamulanie” serwera.
Jak znaleźć trace’y otwarte z użyciem Profilera? Ano tak:
SELECT id, path, file_position, reader_spid FROM sys.traces WHERE is_rowset = 1;
Przykładowy wynik:
W zasadzie po dłuższych obserwacjach doszedłem do wniosku, że można też pytać o reader_spid o wartości innej niż NULL.
Kolumny, które wybrałem w powyższym zapytaniu są w moim odczuciu najbardziej przydatne:
- id – unikalny numer trace’a, dzięki niemu można dowiedzieć się więcej o danej sesji śledzenia (o tym za moment) lub po prostu zakończyć sesję śledzenia odpowiednimi wywołaniami procedury sp_trace_setstatus,
- path - jeśli zapisujemy do pliku .trc (najczęstszy scenariusz poza “zwykłym” śledzeniem za pomocą Profilera), ścieżkę do aktualnego pliku znajdziemy właśnie w tej kolumnie,
- file_position - rozmiar pliku, jeśli trace prowadzi zapis do takowego,
- reader_spid – jeśli Profiler czyta trace, tu znajdziemy identyfikator sesji otwartej przez aplikację (i można dzięki temu poleceniem KILL sprawnie zakończyć sesję Profilera).
Co właściwie śledzi trace?
Jednym z najbardziej niedocenianych źródeł informacji dla DBA jest default trace. Jest to systemowy trace uruchamiany domyślnie wraz ze startem instancji. W widoku katalogowym sys.traces zawsze ma id równe 1. Można go włączać / wyłączać zmieniając opcję ‘default trace’. Poniżej włączanie wspomnianej opcji:
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'default trace enabled', 1; RECONFIGURE;
Co śledzi taki domyślny trace? Przekonajmy się:
SELECT DISTINCT e.trace_event_id, e.name FROM sys.traces AS t CROSS APPLY sys.fn_trace_geteventinfo(t.id) AS i INNER JOIN sys.trace_events AS e ON i.eventid = e.trace_event_id WHERE t.is_default = 1 ORDER BY e.trace_event_id;
Wynik:
trace_event_id name -------------- ------------------------------ 18 Audit Server Starts And Stops 20 Audit Login Failed 22 ErrorLog 46 Object:Created 47 Object:Deleted 55 Hash Warning 69 Sort Warnings 79 Missing Column Statistics 80 Missing Join Predicate 81 Server Memory Change 92 Data File Auto Grow 93 Log File Auto Grow 94 Data File Auto Shrink 95 Log File Auto Shrink 102 Audit Database Scope GDR Event 103 Audit Schema Object GDR Event 104 Audit Addlogin Event 105 Audit Login GDR Event 106 Audit Login Change Property Event 108 Audit Add Login to Server Role Event 109 Audit Add DB User Event 110 Audit Add Member to DB Role Event 111 Audit Add Role Event 115 Audit Backup/Restore Event 116 Audit DBCC Event 117 Audit Change Audit Event 152 Audit Change Database Owner 153 Audit Schema Object Take Ownership Event 155 FT:Crawl Started 156 FT:Crawl Stopped 157 FT:Crawl Aborted 164 Object:Altered 167 Database Mirroring State Change 175 Audit Server Alter Trace Event 218 Plan Guide Unsuccessful
Sporo, bo aż 35 rodzajów zdarzeń. Ale są to na ogół zdarzenia, które nie występują z dużą częstotliwością.
W podobny sposób, a używając jedynie innych funkcji i widoków katalogowych, można dowiedzieć się o tym, jakich kolumn dla każdego zdarzenia używa trace, oraz jakie są w nim ustawione filtry.
Default trace w użytku
Kilka przykładowych zastosowań default trace’a, niektóre podpatrzone w Management Studio ;-)
Zmiany w konfiguracji serwera
SELECT
e.StartTime,
e.SessionLoginName,
e.TextData
FROM sys.traces AS t
CROSS APPLY sys.fn_trace_gettable(
LEFT(t.path, LEN(t.path)-PATINDEX('%\%', REVERSE(t.path))) + '\log.trc',
DEFAULT
) AS e
WHERE t.is_default = 1
AND (
-- Error = 15457 - common sp_configure message
(e.EventClass = 22 AND e.Error = 15457)
OR
-- DBCC event
(e.EventClass = 116 AND e.TextData LIKE '%TRACEO%(%')
)
ORDER BY e.StartTime DESC;
Wynik:
Warte uwagi jest chyba tylko to, że pierwszy plik default trace’a ma nazwę log.trc. Ścieżkę da się odczytać z kolumny path widoku sys.traces. Reszta zapytania bez historii – wybrać dwa rodzaje zdarzeń (Errorlog oraz DBCC) dla trace’a, dla którego w kolumnie is_default w widoku sys.traces widnieje wartość 1.
DDL wykonywany na obiektach w bazach danych
SELECT
e.StartTime,
e.SessionLoginName,
e.DatabaseName,
e.ObjectName,
v.subclass_name AS ObjectType,
CASE e.EventClass
WHEN 46 THEN 'CREATE'
WHEN 47 THEN 'ALTER'
WHEN 164 THEN 'DROP'
END AS SchemaChange
FROM sys.traces AS t
CROSS APPLY sys.fn_trace_gettable(
LEFT(t.path, LEN(t.path)-PATINDEX('%\%', REVERSE(t.path))) + '\log.trc',
DEFAULT
) AS e
INNER JOIN sys.trace_subclass_values AS v
ON v.trace_event_id = e.EventClass
AND v.trace_column_id = 28
AND v.subclass_value = e.ObjectType
WHERE t.is_default = 1
AND e.EventClass IN (46, 47, 164)
AND e.EventSubClass = 0 -- eliminate junk
AND e.DatabaseID <> 2 -- not tempdb
AND e.ObjectType <> 21587 -- not stats
ORDER BY e.StartTime DESC;
Wynik:
Problemem może być odczytanie schematu obiektu, jeśli obiekt skasowano. Komentarz “eliminate junk” dodałem obok filtra, który pozwolił na odfiltrowanie wierszy mówiących o operacjach na statystykach.
Notka: dwa powyższe przykłady wziąłem ze standardowych raportów dostępnych w Management Studio pod prawym przyciskiem na folderze Databases w oknie Object Explorer.
Automatyczne zmiany rozmiaru plików
SELECT
e.StartTime,
CASE e.EventClass
WHEN 92 THEN 'Data File Auto Grow'
WHEN 93 THEN 'Log File Auto Grow'
WHEN 94 THEN 'Data File Auto Shrink'
WHEN 95 THEN 'Log File Auto Shrink'
END AS EventDesc,
e.DatabaseName,
e.FileName
FROM sys.traces AS t
CROSS APPLY sys.fn_trace_gettable(
LEFT(t.path, LEN(t.path)-PATINDEX('%\%', REVERSE(t.path))) + '\log.trc',
DEFAULT
) AS e
WHERE t.is_default = 1
AND e.EventClass IN (92, 93, 94, 95)
ORDER BY e.StartTime DESC;
Wynik:
Czarna skrzynka
SQL Server daje możliwość uruchomienia szybkiego w konfiguracji trace’a – tzw. BlackBox trace’a.
Kod do jego stworzenia i uruchomienia jest naprawdę krótki:
DECLARE @trace_id int; EXEC sp_trace_create @trace_id OUT, 8; EXEC sp_trace_setstatus @trace_id, 1;
Co śledzi taki trace?
SELECT DISTINCT e.trace_event_id, e.name FROM sys.traces AS t CROSS APPLY sys.fn_trace_geteventinfo(t.id) AS ei CROSS APPLY sys.fn_trace_getinfo(t.id) AS i INNER JOIN sys.trace_events AS e ON ei.eventid = e.trace_event_id WHERE i.property = 1 AND i.value = 8 ORDER BY e.trace_event_id;
Zapytanie zwraca raptem cztery rodzaje zdarzeń:
trace_event_id name -------------- ----------------- 11 RPC:Starting 13 SQL:BatchStarting 16 Attention 33 Exception
Do czego można użyć takiego śledzenia? Szybkie uruchomienie, dość zgrubne zdarzenia… Jeśli już, używałbym w sytuacji, gdy chciałbym dowiedzieć się, co stało się tuż przed “reprodukowalnym” problemem :-) Tak czy owak, jeśli ktoś kiedyś tego użył produkcyjnie, chętnie poczytam o innych zastosowaniach.
BlackBox trace’a znajdujemy w widoku sys.traces używając funkcji sys.fn_trace_getinfo (@options = 8). Swoje dane zapisuje on do dwóch plików o łącznym maksymalnym rozmiarze 10 MB (to mniej niż default trace, który zapisuje do 5 plików po maks. 20 MB każdy). Nowe zdarzenia w obu trace’ach – default i BlackBox – nadpisują stare zdarzenia, jeśli osiągnięty został maksymalny rozmiar ostatniego z dostępnych dla danego trace’a plików. Pliki obu trace’ów zapisywane są w folderze Log w ścieżce roboczej SQL Servera (a więc tam, gdzie ERRORLOG).
Podsumowanie
Sądzę, że warto poznać możliwości obu wymienionych trace’ów. Choćby po to, by pobawić się w wyciąganie metadanych trace’ów albo poznać lepiej procedury do ich obsługi. Nie polecam też wyłączania default trace’a, chyba, że polityka bezpieczeństwa firmy lub organizacji wyraźnie mówi o tym, że należy go wyłączyć.
Jeśli masz jakieś ciekawe przemyślenia na temat obu trace’ów albo znasz jakieś ciekawostki dotyczące trace’ów ogólnie, podziel się nimi (mogę je opublikować jako dodatek do tego wpisu).
[PL] SQL Server dla DBA – sys.dm_os_performance_counters
Apr 4th
W komentarzach do pierwszego wpisu z serii SQL Server dla DBA Robert Kubalski przypomniał o istnieniu widoku sys.dm_os_performance_counters, który może być źródłem cennych informacji o instancji SQL Server i bazach danych istniejących w ramach owej instancji. Pomyślałem, że warto by było napisać, w jaki sposób DBA może interpretować wartości zwracane w wyniku zapytania do wspomnianego widoku.
Pierwsze podejście – typy liczników
Widok dynamiczny sys.dm_os_performance_counters umożliwia podejrzenie wartości liczników monitora wydajności (perfmon.exe) dedykowanych konkretnej instancji SQL Server (tej, z której zadajemy zapytanie do widoku). Jednak nie wszystkie wartości liczbowe można bezpośrednio interpretować. Aby dowiedzieć, w jaki sposób należy interpretować wartości zwracane dla poszczególnych liczników, trzeba poznać typy liczników.
Istnieje kilka typów liczników (są to typy liczników WMI), a listę ich kodów można uzyskać w prosty sposób:
SELECT DISTINCT cntr_type FROM sys.dm_os_performance_counters ORDER BY cntr_type;
Oto owe typy liczników i ich interpretacja:
- 65792 - PERF_COUNTER_LARGE_RAWCOUNT – obrazuje ostatnio przechwyconą wartość licznika; tego typu liczniki interpretujemy bezpośrednio – odczytana wartość może być od razu interpretowana (bez przeliczania)
- 272696576 - PERF_COUNTER_BULK_COUNT – oznacza licznik, którego wartości są liczone per zadany interwał czasowy; do wyliczenia sensownej wartości takiego potrzebne są dwa pomiary (a następnie różnicę między pomiarami należy podzielić przez ilość jednostek czasu, np. ilość sekund)
- 537003264 – PERF_LARGE_RAW_FRACTION – taki licznik używany jest do obliczenia ilorazu reprezentującego wskaźnik procentowy (jest podstawiany w liczniku dzielenia, a więc do pełni szczęścia potrzebujemy mianownika – patrz typ PERF_LARGE_RAW_BASE poniżej); niektóre liczniki (np. Buffer Cache Hit Ratio) są reprezentowane jako wartości procentowe i powstają jako wynik dzielenia licznika typu 537003264 przez licznik typu 1073939712
- 1073874176 - PERF_AVERAGE_BULK – ten licznik jest używany do obliczenia ilorazu reprezentującego średnią wartość(podstawiany w liczniku dzielenia – znów potrzebny jest mianownik w postaci licznika typu PERF_LARGE_RAW_BASE); niektóre liczniki (np. Average Wait Time (ms)) reprezentują wartości średnie i powstają jako wynik dzielenia wartości licznika typu 1073874176 przez wartości odpowiedniego licznika typu 1073939712
- 1073939712 - PERF_LARGE_RAW_BASE – licznik używany do obliczenia ilorazu (podstawiany w mianowniku dzielenia); stanowi parę z licznikiem typu 537003264 lub 1073874176.
A zatem, jeśli zobaczysz takie dwa liczniki:
znając typy liczników możesz wywnioskować, że należy podzielić wartość z kolumny cntr_value dla licznika Buffer cache hit ratio przez wartość z tej samej kolumny dla licznika Buffer cache hit ratio base (w tym konkretnym przypadku otrzymujemy wartość 1, co oznacza, że instancja notuje idealnie 100% trafień w bufor danych).
Drugie podejście – licznik, obiekt, instancja
Liczniki są podzielone pomiędzy obiekty. Obiekt można traktować jak kategorię. Oczywiście, są to te same obiekty, które instancja SQL Server dodaje do listy obiektów do użycia w monitorze wydajności. I jednocześnie trzeba pamiętać, że widok sys.dm_os_performance_counters zwraca wyłącznie wartości tych liczników, które pochodzą od danej instancji, na której wykonujemy zapytanie. Dla niektórych liczników, tak jak w perfmon.exe istnieje wiele instancji. Przykładem mogą być liczniki mówiące o rozmiarze dzienników transakcji w bazach danych oraz ilości miejsca zajętego w każdym z dzienników transakcji:
SELECT * FROM sys.dm_os_performance_counters WHERE counter_name LIKE '%Log File%';
Trzecie podejście – przykładowe zapytania i liczniki
O ile zapytanie o wszystkie liczniki nie obciąża praktycznie serwera (liczba odczytów i zużycie procesora są niemal zerowe, a sam widok zwraca co najwyżej kilka tysięcy wierszy – liczba wierszy zależy od ilości baz danych na instancji), o tyle zazwyczaj interesuje nas konkretny licznik lub grupa liczników.
Page Life Expectancy
Page Life Expectancy z obiektu Buffer Manager czyli czas życia stron w pamięci mierzony w sekundach. Ten licznik przydaje się, by zweryfikować, czy przypadkiem strony danych z pamięci nie są zrzucane do pliku stronicowania. Gwałtowny spadek tego licznika zazwyczaj mówi o opróżnieniu bufora danych – czy to celowym, czy wymuszonym przez system (prosty sposób na stwierdzenie braku ustawionego uprawnienia Lock pages in memory).
SELECT object_name, counter_name, cntr_value FROM sys.dm_os_performance_counters WHERE [object_name] LIKE '%Buffer Manager%' AND counter_name = 'Page Life Expectancy';
Przykładowy wynik (niski, bo zmierzony tuż po restarcie usługi):
Przy okazji warto zauważyć, że kolumny tekstowe widoku sys,dm_os_performance_counters są typu nchar(128), a więc nazwy obiektów, liczników oraz instancji są dopełniane do 128 znaków spacjami. Jest to istotna sprawa przy pisaniu zapytań, zwłaszcza z użyciem operatora LIKE (warto dokładać znak % na końcu wzorca, by nie nadziać się na pułapkę związaną z dodatkowymi spacjami w nazwach).
Buffer cache hit ratio
Buffer cache hit ratio z obiektu Buffer Manager czyli procent trafień zapytań w strony znajdujące się w buforze danych. Im bliżej wartości 1 (oznaczającej 100%), tym lepiej.
SELECT
c.object_name,
c.counter_name,
CASE
WHEN b.cntr_value = 0 THEN 0
ELSE CONVERT(numeric(38,2), c.cntr_value * 1.0 / b.cntr_value)
END AS value
FROM sys.dm_os_performance_counters AS c
INNER JOIN sys.dm_os_performance_counters AS b
ON c.object_name = b.object_name
AND b.counter_name LIKE RTRIM(c.counter_name) + '%'
AND c.instance_name = b.instance_name
AND b.cntr_type = 1073939712
WHERE c.[object_name] LIKE '%Buffer Manager%'
AND c.counter_name = 'Buffer cache hit ratio';
Przykładowy wynik:
Powyższą technikę odszukiwania licznika i mianownika do obliczania ilorazu można stosować do większości liczników, których wartości wyliczamy z ilorazu. Są jednak przypadki, kiedy nazwa licznika i nazwa współczynnika używanego w mianowniku ilorazu różnią się nie tylko słowem “Base”. Przykładem może być licznik Avg. Time Between Batches (ms) z obiektu Broker TO Statistics:
Połączenia i aktywne transakcje
Liczniki Active Transactions z obiektu Databases oraz User Connections z obiektu General statistics pozwalają zorientować się, jaka jest aktualna liczba połączeń do instancji SQL Server oraz ile aktywnych transakcji jest otwartych w kontekście każdej bazy danych.
SELECT [object_name], counter_name, instance_name, cntr_value FROM sys.dm_os_performance_counters WHERE counter_name IN ( 'Active Transactions', 'User Connections' );
Przykładowy wynik:
Podsumowanie
Widok sys.dm_os_performance_counters daje DBA możliwość śledzenie pokaźnej ilości liczników mówiących wiele o kondycji instancji SQL Server. Co ważne, administrator nie musi korzystać z żadnych zewnętrznych narzędzi. Może wykorzystać widok do zbierania danych wydajnościowych do celów analizy, budowania tzw. baseline, klasycznego monitorowania, itd.
Widok ten jest szeroko wykorzystywany przez różne narzędzia firm trzecich (np. Quest Spotlight), jak również przez narzędzia dostępne w SQL Server (np. Performance Studio). Warto się nim zainteresować, zwłaszcza, jeśli naszym celem jest zbudowanie własnego rozwiązania do monitorowania serwera (patrz także wpis pt. SQL Server – Interaktywny performance dashboard dla ubogich).
[PL] SQL Server dla DBA – zapomniany widok sys.master_files
Jan 24th
Pomyślałem ostatnio, że fajnie by było, jakbym częściej umieszczał nieco prostsze, acz użyteczne kawałki kodu T-SQL, głównie dedykowane administratorom baz danych. Na dobry początek wykorzystanie często zapominanego i niedocenianego widoku systemowego sys.master_files istniejącego w SQL Server 2005 i nowszych wersjach.
Widok ten jest znakomitym źródłem informacji na temat plików baz danych. Można się dzięki niemu szybko zorientować w takich tematach, jak:
- czy pliki bazy leżą na tym samym dysku logicznym,
- jaki jest rozmiar oraz parametry automatycznego powiększania każdego z plików,
- ile plików jakiego typu wchodzi w skład każdej z baz danych,
- jaki jest status każdego pliku (OFFLINE / ONLINE, READ_ONLY, etc.),
- jaki był ostatni log sequence number (LSN) backupu wykonanego na pliku.
Z mojej perspektywy najbardziej przydatna informacja to (oczywiście?) lokalizacja i rozmiar plików (kolumny physical_name oraz size). Rozmiar w kolumnie size liczony jest w 8-kilobajtowych stronach, więc po drodze trzeba wykonać kilka prostych obliczeń, by uzyskać rozmiar pliku na przykład wyrażony w megabajtach. Poniższe zapytanie jest przykładem, jak z widoku sys.master_files można odczytać łączny rozmiar plików każdego rodzaju (log, dane, etc.) dla każdej z baz danych.
SELECT DB_NAME(database_id) AS database_name, CONVERT( numeric(38,2), SUM( CASE type WHEN 0 THEN size * 8192.0 / (1024 * 1024) ELSE 0 END ) ) AS data_size_mb, CONVERT( numeric(38,2), SUM( CASE type WHEN 1 THEN size * 8192.0 / (1024 * 1024) ELSE 0 END ) ) AS log_size_mb, CONVERT( numeric(38,2), SUM( CASE type WHEN 2 THEN size * 8192.0 / (1024 * 1024) ELSE 0 END ) ) AS filestream_size_mb, CONVERT( numeric(38,2), SUM( CASE type WHEN 4 THEN size * 8192.0 / (1024 * 1024) ELSE 0 END ) ) AS fulltext_size_mb FROM sys.master_files GROUP BY database_id ORDER BY DB_NAME(database_id);
Powyższy skrypt umożliwia natychmiastową odpowiedź na pytania w stylu: “która baza jest największa?”, “która baza używa filestream?”, “gdzie log jest większy niż dane?”.
[EDYCJA 2011-01-26]: Czytelnik o nicku wacio słusznie zwrócił mi uwagę, że dla kontenerów FILESTREAM w widoku sys.master_files kolumna size zawsze przyjmuje wartość 0. Dzięki za czujność. Dla mnie to oczywisty bug, ale na szczęście oczywiste obejście tego problemu jest podane w BOL dla Denali: This field is populated as zero for FILESTREAM containers. Query the sys.database_files catalog view for the actual size of FILESTREAM containers. Mimo to, nadal uważam, że sys.master_files jest ciekawym i przydatnym widokiem :-)
[PL] SQL Server – Logowanie wejść sysadminów na serwer
Sep 27th
Wstęp
Jakiś czas temu napisałem, dlaczego uważam, że logon trigger nie jest dobrym rozwiązaniem w sytuacji, gdy zależy nam na logowaniu udanych prób logowania do instancji SQL Servera członków roli sysadmin. Napisałem wówczas, że mechanizmem, który nadaje się do takiego zadania, są Extended Events. Ale okazało się, że Extended Events nie dysponują zdarzeniem, które takie logowanie by umożliwiło. W międzyczasie wpadłem na pomysł, by zastosować w takim przypadku events notifications (dostępne od SQL Server 2005).
Rozwiązanie
Zadanie: zapisywać do tabeli wszystkie udane próby logowania członków roli sysadmin.
Rozwiązanie:
-- (0) Wlaczamy Service Brokera, jezeli jest wylaczony IF EXISTS (SELECT * FROM sys.databases WHERE name = N'msdb' AND is_broker_enabled = 0) ALTER DATABASE msdb SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE; GO -- (1) Wybieramy baze msdb, bo zawsze jest... USE msdb; GO -- (2) Kasujemy obiekty, ktore zaraz utworzymy IF EXISTS (SELECT * FROM sys.server_event_notifications WHERE name = N'AuditSysadminLoginNotification') DROP EVENT NOTIFICATION AuditSysadminLoginNotification ON SERVER; GO IF EXISTS (SELECT * FROM sys.services WHERE name = N'AuditSysadminLoginService') DROP SERVICE AuditSysadminLoginService; GO IF EXISTS (SELECT * FROM sys.service_queues WHERE name = N'AuditSysadminLoginQueue' AND [schema_id] = 1) DROP QUEUE dbo.AuditSysadminLoginQueue; GO -- (3) Tworzymy obiekty: kolejke, usluge i powiadomienie o zdarzeniu CREATE QUEUE dbo.AuditSysadminLoginQueue WITH STATUS = OFF; GO CREATE SERVICE AuditSysadminLoginService ON QUEUE AuditSysadminLoginQueue ( [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification] ); GO CREATE EVENT NOTIFICATION AuditSysadminLoginNotification ON SERVER FOR AUDIT_LOGIN TO SERVICE 'AuditSysadminLoginService', 'current database'; GO -- (4) Tworzymy tabele na potrzeby logowania IF OBJECT_ID(N'dbo.SysadminLogins', N'U') IS NOT NULL DROP TABLE dbo.SysadminLogins; GO CREATE TABLE dbo.SysadminLogins ( LoginID int NOT NULL IDENTITY(1,1) PRIMARY KEY, LoginName sysname NOT NULL, ApplicationName nvarchar(256) NULL, HostName nvarchar(128), LoginDate datetime NOT NULL DEFAULT(GETDATE()) ); GO -- (5) Tworzymy procedure do logowania IF OBJECT_ID(N'dbo.usp_LogSysadminLogin', N'P') IS NOT NULL DROP PROCEDURE dbo.usp_LogSysadminLogin GO CREATE PROC dbo.usp_LogSysadminLogin AS DECLARE @ErrorMessage nvarchar(4000), @EventData xml, @LoginName sysname, @ApplicationName nvarchar(256), @HostName nvarchar(128), @ConversationHandle uniqueidentifier; WHILE (1=1) BEGIN BEGIN TRAN; BEGIN TRY; RECEIVE TOP (1) @EventData = CAST(message_body AS xml), @ConversationHandle = [conversation_handle] FROM dbo.AuditSysadminLoginQueue; IF @@ROWCOUNT = 0 BEGIN IF @@TRANCOUNT > 0 BEGIN ROLLBACK; END; BREAK; END; SELECT @LoginName = @EventData.value('(/EVENT_INSTANCE/LoginName/text())[1]', 'sysname'), @ApplicationName = @EventData.value('(/EVENT_INSTANCE/ApplicationName/text())[1]', 'nvarchar(256)'), @HostName = @EventData.value('(/EVENT_INSTANCE/HostName/text())[1]', 'nvarchar(128)'); IF IS_SRVROLEMEMBER(N'sysadmin', @LoginName) = 1 BEGIN INSERT INTO dbo.SysadminLogins (LoginName, ApplicationName, HostName) SELECT @LoginName, @ApplicationName, @HostName; END; IF @@TRANCOUNT > 0 COMMIT; END TRY BEGIN CATCH; IF @@TRANCOUNT > 0 BEGIN ROLLBACK; END CONVERSATION @ConversationHandle; BREAK; END; END CATCH; END; GO -- (6) Przypinamy procedure do kolejki i wlaczamy kolejke ALTER QUEUE dbo.AuditSysadminLoginQueue WITH STATUS = ON, RETENTION = OFF, ACTIVATION ( STATUS = ON , PROCEDURE_NAME = msdb.dbo.usp_LogSysadminLogin, MAX_QUEUE_READERS = 2, EXECUTE AS OWNER ); GO -- (7) Testujemy, czy cos sie zalogowalo SELECT * FROM msdb.dbo.SysadminLogins;
Komentarz do kodu:
- jako bazę docelową, w której zapisuję informacje o zdarzeniach, wybieram bazę msdb (zawsze jest na serwerze, domyślnie ma włączonego Service Brokera),
- w kroku (0) włączam na bazie msdb Service Brokera, jeżeli jeszcze nie jest on włączony,
- w kroku (2) dbam o to, by obiekty, które zamierzam stworzyć, nie istniały na serwerze i w bazie msdb,
- w kroku (3) tworzę obiekty niezbędne do asynchronicznego przechwytywania zdarzeń: kolejkę, usługę oraz event notification (na poziomie serwera, przechwytuje zdarzenia AUDIT_LOGIN – logowania do instancji SQL Servera),
- w kroku (4) tworzę tabelę, do której będą zrzucane informacje o logowaniu sysadminów (login, nazwa użytej aplikacji klienckiej, nazwa maszyny klienckiej oraz data i czas logowania),
- w kroku (5) tworzę procedurę składowaną, która zostanie użyta do wybrania informacji z tego, co przechwyci event notification i zwróci za pomocą funkcji EVENTDATA, wybrane informacje zostaną zapisane do tabeli utworzonej w kroku (4), do wychwycenia loginów będących członkami roli sysadmin używam funkcji IS_SRVROLEMEMBER,
- w kroku (6) przypinam procedurę z kroku (5) do kolejki z kroku (3) i włączam kolejkę, od tej pory zdarzenia klasy AUDIT_LOGIN są już przechwytywane, a stosowne informacje zapisywane w tabeli z kroku (4),
- w kroku (7) prezentuję zapytanie, dzięki któremu możesz sprawdzać, czy jakieś zdarzenie zostało już zalogowane w tabeli z kroku (4).
Przykładowe informacje o zdarzeniach logowania sysadminów:
Podsumowanie
Zastosowanie event notification ma kilka zalet: nie ingeruje w proces logowania (nie ma takiej możliwości, by sysadmin nie zalogował się z powodu błędu w procesie logowania zdarzeń, jak to mogło mieć miejsce w przypadku zastosowania logon triggera), asynchroniczność zapewnia niezłą skalowalność i działanie mechanizmu nawet w przypadku dużego natężenia zdarzeń, ewentualne informacje o błędach są do znalezienia w errorlogu instancji SQL Servera (np. błędy wynikające z wykrycia “zatrutej wiadomości” – poison message) lub w widoku systemowym sys.transmission_queue (w kolumnie transmission_status znajdziesz informację, dlaczego komunikat o zdarzeniu utknął w czasie przesyłania). Co ciekawe, wydaje mi się, że można znaleźć naprawdę sporo kategorii zdarzeń, dla przechwytywania których event notifications nadają się idealnie (m.in. DEACLOCK_GRAPH i BLOCKED_PROCESS_REPORT). Używając event notifications możesz zbudować całkiem niezły framework do monitorowania SQL Servera. Polecam zabawę tym mechanizmem.
[PL] SQL Server 2008 – Szybkie tworzenie polis
Jul 22nd
Ostatnio zdarzyło mi się parę razy użyć mechanizmu Policy-Based Management dostępnego w SQL Server 2008 (i 2008 R2). Zacząłem eksperymentować i szukać wszelkich opcji, które umożliwiłyby w miarę szybkie i sprawne tworzenie polis takiemu leniwemu DBA, jak ja :-)
Jedną z takich opcji jest opcja dostępna w menu kontekstowym okna Object Explorer w Management Studio. Klikamy na przykład prawym przyciskiem myszy na nazwie instancji SQL Servera i wybieramy opcję Facets.
Pojawia się okno właściwości obiektu (w tym przypadku serwera) w kontekście wybranego szablonu (pole Facet). A na samym dole okna… Tada! Przycisk Export Current State as Policy :-)
Klikamy, zapisujemy polisę do pliku XML albo od razu na instancję SQL Server, otwieramy świeżutko utworzoną polisę, edytujemy. Czysta przyjemność! A wszystko w oparciu o przygotowany wzorzec serwera, bazy, obiektu w bazie danych (można sprawdzić, że pozycja Facets pojawia się w menu kontekstowym po kliknięciu w zasadzie na cokolwiek w oknie Object Explorer).
Nie powiem, zabawka w sam raz dla takich leniuszków, jak ja. Łatwiej zrobić polisę opartą o nieco bardziej rozbudowany warunek i usuwać zbędne właściwości z warunku, niż tworzyć wszystko rękami w pocie czoła ;-)
[PL] SQL Server – Kontrola dzienników transakcji
Jul 7th
Jednymi z problemów najczęściej poruszanych na forum poświęconym SQL Serverowi na portalu WSS.pl są wszelkiej maści kłopoty wynikające z zaniedbania dziennika transakcji bazy danych. Rekordziści prześcigają się w wielkości plików dziennika transakcji (185 GB dla bazy kilkugigabajtowej to dobry przykład). Często padają pytania, jak to wszystko (recovery model, dziennik transakcji, backup dziennika transakcji) działa. Nie zamierzam się rozpisywać o podstawach logowania operacji DML czy o tym, jak zbudowany jest dziennik transakcji. Pomyślałem za to, że fajnie byłoby napisać skrypt dla DBA lub konsultanta, dzięki któremu będzie można, wchodząc na “cudzy” (lub, o zgrozo, także swój!) serwer, zdiagnozować podstawowe problemy związane z higieną dziennika transakcji (a w zasadzie z brakiem tej higieny). Wspomniany skrypt prezentuje się tak:
SET NOCOUNT ON;
IF OBJECT_ID('tempdb.dbo.#VLFs', 'U') IS NOT NULL
DROP TABLE #VLFs;
CREATE TABLE #VLFs (
FileId int NULL,
FileSize bigint NULL,
StartOffset bigint NULL,
FSeqNo bigint NULL,
Status tinyint NULL,
Parity smallint NULL,
CreateLSN numeric(25,0) NULL
);
IF OBJECT_ID('tempdb.dbo.#NumberOfVLFs', 'U') IS NOT NULL
DROP TABLE #NumberOfVLFs;
CREATE TABLE #NumberOfVLFs (
DatabaseName sysname NOT NULL,
NumberOfVLFs int NOT NULL
);
DECLARE @DBName sysname, @SQL nvarchar(4000);
DECLARE DBCursor CURSOR LOCAL STATIC
FOR
SELECT name FROM sys.databases WHERE state_desc = 'ONLINE';
OPEN DBCursor;
FETCH NEXT FROM DBCursor INTO @DBName;
WHILE @@FETCH_STATUS = 0 BEGIN
DELETE FROM #VLFs;
SET @SQL =
'INSERT INTO #VLFs
EXEC ' + QUOTENAME(@DBName)
+ '.dbo.sp_executesql N''DBCC LOGINFO WITH NO_INFOMSGS'';
INSERT INTO #NumberOfVLFs (DatabaseName, NumberOfVLFs)
SELECT ' + QUOTENAME(@DBName, '''') + ', COUNT(*)
FROM #VLFs;';
EXEC (@SQL);
FETCH NEXT FROM DBCursor INTO @DBName;
END
CLOSE DBCursor;
DEALLOCATE DBCursor;
IF OBJECT_ID('tempdb.dbo.#LogHealth', 'U') IS NOT NULL
DROP TABLE #LogHealth;
CREATE TABLE #LogHealth (
DatabaseName sysname NULL,
LogSizeMB numeric(38,2) NULL,
LogSpaceUsed numeric(4,2) NULL,
Status tinyint NULL
);
INSERT INTO #LogHealth EXEC ('DBCC SQLPERF(LOGSPACE) WITH NO_INFOMSGS');
SELECT
v.DatabaseName AS [Database name],
d.recovery_model_desc AS [Recovery model],
CASE
WHEN d.recovery_model = 1
AND r.last_log_backup_lsn IS NOT NULL
THEN 1
ELSE 0
END AS [Was backup taken],
q.number_of_files AS [Number of log files],
l.LogSizeMB AS [Log size (MB)],
l.LogSpaceUsed AS [Log space used (%)],
v.NumberOfVLFs AS [Number of VLFs],
d.log_reuse_wait_desc AS [Log reuse wait cause]
FROM #NumberOfVLFs AS v
INNER JOIN #LogHealth AS l
ON v.DatabaseName = l.DatabaseName
INNER JOIN sys.databases AS d
ON v.DatabaseName = d.name
INNER JOIN sys.database_recovery_status AS r
ON d.database_id = r.database_id
INNER JOIN (
SELECT
m.database_id,
COUNT(*) AS number_of_files
FROM sys.master_files AS m
WHERE m.type = 1
GROUP BY m.database_id
) AS q
ON d.database_id = q.database_id;
GO
Wynikiem działania powyższego kodu jest coś takiego:
Co oznaczają kolejne kolumny?
- Database name – nazwa bazy danych (powinny pokazać się wszystkie bazy)
- Recovery model – tryb pracy dziennika transakcji (dla produkcyjnych baz zalecany FULL)
- Was backup taken – wskazuje, czy został wykonany backup rozpoczynający działanie bazy w trybie FULL
- Number of log files – ilość plików dziennika transakcji, w zasadzie zawsze powinien być 1 plik (wyjątkiem są sytuacje, gdy musimy dodać drugi lub kolejny plik dziennika transakcji z uwagi na brak miejsca na dysku przechowującym pierwszy plik dziennika)
- Log size (MB) – rozmiar dziennika transakcji w megabajtach (wiadomo – duży może być zwłaszcza, gdy dziennik transakcji jest zaniedbany)
- Log space used (%) – ile procent dziennika transakcji jest zajęte przez zapisane w nim transakcje (tak dowiemy się, ile brakuje do tego, by dziennik transakcji automatycznie się rozrósł)
- Number of VLFs – ilość wirtualnych plików dziennika transakcji (ang. Virtual Log File); zalecane jest poniżej 50, ale rząd wielkości więcej – 300-500 to też nie jest wielka tragedia; ale jeżeli liczba idzie w dziesiątki / setki tysięcy, można spodziewać się problemów przy starcie bazy oraz dłuższego czasu wykonywania backupów i przywracania bazy
- Log reuse wait cause – informacja na temat przyczyny braku możliwości ponownego zapełniania plików dziennika transakcji od pierwszych plików wirtualnych (najczęściej otwarte transakcje, ale przyczyn może być więcej)
Sam kod wykorzystuje kilka widoków i poleceń DBCC:
- sys.databases – widok systemowy zawierający listę baz danych i ich właściwości
- sys.master_files – widok systemowy zawierający listę plików wszystkich baz danych
- sys.database_recovery_status – widok systemowy zawierający informacje na temat procesów recovery baz danych
- DBCC LOGINFO – nieudokumentowane polecenie DBCC pokazujące układ wirtualnych plików dziennika transakcji dla bieżącej bazy danych (tu użyte do zliczenia wirtualnych plików)
- DBCC SQLPERF(LOGSPACE) – polecenie DBCC zwracające listę baz danych wraz z wielkością dzienników transakcji oraz stopnia wypełnienia tych dzienników
Mam nadzieję, że powyższy kod się przyda. Działa na SQL Server 2005 i nowszych. Przy odrobinie wysiłku można kod przerobić, by działał także na wersji 2000.
I na deser kilka odnośników do postów na blogach, które przeczytałem przed przystąpieniem do pisania kodu:
- Mój log jest za duży (mój stary blog)
- New script: is that database REALLY in the FULL recovery mode? (Paul S. Randal)
- 8 Steps to better Transaction Log throughput (Kimberly L. Tripp)
-
Transaction Log VLFs – too many or too few? (Kimberly L. Tripp)
Szczególnie ciekawy jest bug wymieniony na koniec ostatniego z wymienionych postów – polecam do poczytania :-)
[PL] SQL Server 2008 – kopia zapasowa zadań
Apr 28th
Czy kiedykolwiek zdarzyło Ci się, że trzeba było przenosić zadania (ang. jobs) usługi SQL Server Agent? Dość oczywistym sposobem jest wykorzystanie mechanizmu skryptowania dostępnego w SQL Server Management Studio. Po kolei:
- Łączymy się z instancją SQL Server 2008 za pomocą okna Object Explorer (wciśnij F8, by to okno zobaczyć).
- Przechodzimy w drzewie do węzła SQL Server Agent – Jobs.
- Otwieramy okno Object Explorer Details (wciśnij F7, by to okno zobaczyć).
- Zaznaczamy wszystkie zadania w oknie Object Explorer Details, klikamy prawym przyciskiem myszy i wybieramy Script Job as – CREATE To i wybieramy odpowiednią dla nas opcję (do pliku, do nowego okna skryptu, do schowka).
Wszystko dobrze, ale co, gdy taką kopię zapasową zadań musimy wykonywać cyklicznie? Chcemy mieć do tego zadania automat. Pierwsza myśl – użyć T-SQL. Trzeba się jednak nieźle naklepać kodu, by napisać takie narzędzie. Druga myśl – użyć tego, czego używa Management Studio – .NET i bibliotek SMO. Dobry pomysł, ale nadal sporo kodu do napisania. Czy na pewno? Otóż nie, bo z odsieczą przychodzi nam… PowerShell!
A zatem – otwieramy na początek konsolę sqlps.exe (klikamy prawym przyciskiem myszy na nazwie instancji w oknie Object Explorer i wybieramy Start PowerShell).
Na powyższym zrzucie 0809-001 to nazwa mojej maszyny, a DEFAULT oznacza oczywiście instancję domyślną. Gdy nie wiemy, dokąd dalej nawigować w oknie konsoli PowerShell, wystarczy uruchomić komendę dir (jest to tutaj alias na odpowiedni commandlet). Teraz nawigujemy (cd jest kolejnym przydatnym aliasem – działa zupełnie jak w konsolach DOS i umożliwia nawigację po “folderach” SQL Servera) do zbioru zadań usługi SQL Server Agent. U mnie to wyglądało tak:
- cd JobServer
- cd Jobs
- dir
I mamy listę zadań. W konsoli PowerShell wiele klasy SMO są dostępne bez pokrętnego dodawania bibliotek. Praktycznie każdy obiekt dysponuje metodą Script() i ten fakt właśnie można wykorzystać. Gdy w konsoli PowerShell pojawił się prompt:
PS SQLSERVER:\SQL\0809-001\DEFAULT\JobServer\Jobs>
wpisałem coś takiego:
dir | %{$_.Script()}
I dostałem kod T-SQL zawierający definicje zadań. Kod niestety niedoskonały, ale w zasadzie brakowało w nim jedynie odseparowania każdej definicji końcem wsadu (słowem GO).
Czyli skryptować zadania w PowerShell już umiem. Teraz zapis do pliku. Nic prostszego. Lekka modyfikacja dopiero co uruchomionego skryptu:
dir | %{$_.Script()} >> C:\jobs.sql
I mam zapisane definicje zadań w pliku C:\jobs.sql. Oczywiście – warunkiem jest to, by wskazać folder, do którego SQL Server (a w zasadzie konto Windows przydzielone do tej usługi) ma prawo zapisu.
I teraz ostateczny szlif. Jak spowodować, żeby – po pierwsze – taki skrypt PowerShell był uruchamiany cyklicznie i – po drugie – by za każdym razem wynikowy kod T-SQL lądował w nowym pliku (np. oznaczonym datą i godziną)?
Skoro cyklicznie, to pewnie wypada użyć zadania usługi SQL Server Agent. Tworzymy więc nowe zadanie:
- W oknie Object Explorer przechodzimy do węzła SQL Server Agent – Jobs.
- Klikamy prawym przyciskiem myszy na węźle Jobs i wybieramy New Job….
- Wpisujemy nazwę zadania (pole Name) i po lewej stronie okna New Job wybieramy opcję Steps, a następnie klikamy na przycisku New…
- Wpisujemy nazwę nowego kroku zadania (pole Name), a następnie z rozwijanej listy Type wybieramy PowerShell.
- W polu Command wpisujemy skrypt PowerShell. W moim przypadku wyglądał on tak:
$data = Get-Date –Format yyyyMMddHHmm
dir SQLSERVER:\SQL\0809-001\DEFAULT\JobServer\Jobs | %{$_.Script() + “GO”} >> C:\backup_jobs_$data.sql
Pierwsza linijka skryptu ustawia jako obiekt $data bieżącą datę i czas zapisaną w odpowiednim formacie. Druga linijka dla każdego zadania uruchamia metodę Script(), dokleja do treści zadania zamknięcie wsadu (GO), a całość zeskryptowanego kodu T-SQL zapisuje do pliku o zmiennej nazwie, np. C:\backup_jobs_201004282220.sql.
Teraz pozostaje już tylko wybrać harmonogram dla zadania i przetestować (oczywiście, usługa SQL Server Agent musi być uruchomiona). U mnie działa. Wykorzystałem ten mechanizm także u klienta, który miał potrzebę przenoszenia zadań między serwerami zawierającymi bazy sprzężone mechanizmem log shippingu. PowerShell zaoszczędził mi w tym przypadku wiele pracy.
Przy okazji – polecam sesje Łukasza Grali poświęcone wykorzystaniu PowerShell do administracji systemem SQL Server 2008. Sesje te są organizowane przez portal VirtualStudy.pl (gratulacje dla prowadzących portal z okazji rocznicy istnienia portalu – tak trzymać!).





Nazywam się Paweł Potasiński i pracuję w polskim oddziale Microsoft w dziale Small and Midmarket Solutions & Partners (SMS&P) jako Partner Technology Advisor.




