Showing posts with label SQL Server. Show all posts
Showing posts with label SQL Server. Show all posts


Report from the battlefield #9 - async/await + MARS


This post from Report from the battlefield series will be about my own mistake. It is related to async/await and MARS i.e. Multiple Active Result Sets. async/await allows us to use asynchronous programming more easily. MARS is a feature of MSSQL that allows us to have more than one pending request opened per connection at the same time. For example, it may be useful if we have 2 nested loops i.e. internal and external. External loops iterate through one result set and the internal one through another. Ok, but you probably wonder what MARS has in common with async/await.

A few days ago my application started failing due to InvalidOperationException exception with the additional message saying that The connection does not support MultipleActiveResultSets. Well, I used MARS in the past so I simply enabled it in the connection string by setting MultipleActiveResultSets attribute to true.

However, later I realized that my application should not require MARS at all so I started digging into what was wrong. It turned out that the problem was related to my silly mistake in using async/await. Let's look at the following simplified version of the problematic code. We have a trivial Main method:
static void Main()
Start is an async method responsible for opening a connection to DB and executing other async methods:
private static async Task Start()
   using (var c = new SqlConnection(ConnectionString))

      await Func1(c);
      await Func2(c);
      await Func3(c);
Func1, Func2 and Func3 are responsible for reading data and processing them. In our case, for simplification, they all will do the same thing:
private static async Task Func1(SqlConnection c) => await ReadData(c);
private static async Task Func2(SqlConnection c) => ReadData(c);
private static async Task Func3(SqlConnection c) => await ReadData(c);
And here is the ReadData method. It's also simple. It simply reads data from a table:
private static async Task ReadData(SqlConnection c)
   var cmd = c.CreateCommand();

   cmd.CommandText = "SELECT * FROM dbo.Fun";

   using (var reader = await cmd.ExecuteReaderAsync())
      while (await reader.ReadAsync())
         // Process data
If you run this code, the aforementioned InvalidOperationException exception will be thrown in the line with ExecuteReaderAsync. The question is why? Well, in this short code it is rather easy to spot that in Func2 method await is missing before ReadData. But, do you know why it is a problem? If not, don't worry it's a little bit tricky.

Here is an explanation. Without await the simplified flow is as follows:
  • ...
  • Start executes Func2.
  • Func2 executes ReadData.
  • ReadData executes ExecuteReaderAsync.
  • ReadData awaits for the result from ExecuteReaderAsync.
  • The control returns to caller i.e. Func2.
  • However, Func2 does not use await so it returns completed task to Start method.
  • From the point of view of Start processing of Func2 is finished so it executes Func3.
  • Func3 executes ReadData
  • The previous call to ReadData may be still in progress.
  • It also means that ReadData will call ExecuteReaderAsync when another result set is still being processed.
  • The exception is thrown.
Adding missing await fix the problem. Thanks to that the task returned from Func2 will not be completed until call to ReadData is over. And if so Start will not execute Func3 immediately. The final well known conclusion is:

Always async/await all the way down.

*The picture at the beginning of the post comes from own resources and shows Laurel forest on La Gomera.


Do not forget about GO


Source: own resources, Authors: Agnieszka and Michał Komorowscy

Almost 4 years ago, I wrote a short post in Polish about problems that may occur if we forget about GO keyword in our scripts. I decided to write this post again, this time in English, because recently I helped to fixed exactly the same problem again. As a remainder, GO keyword instructs tools like SQL Management Studio, sqlcmd... to send the batch of T-SQL code to the server. Now, let's look at the following code that creates a stored procedure and tell me what is wrong here:

GRANT EXECUTE ON dbo.pr_Fun TO public


Sandbox Database Manager


My colleague Tomasz Moska published very nice tool that makes management of development MSSQL sandbox databases very easy. It is called Sandbox Database Manager and you can download it here or from GitHub.

Why is it worth recommending? Try to imagine yourself situation like this. A tester found a bug in the application. In order to reproduce it you need a copy of his database from a system test environment. With Sandbox Database Manager you can make a copy of this database and restore it on a selected server with just a few clicks. Another click or two and you have a snapshot created. Thanks to that you are be able to revert the database to its original state at any time. Now let's assume that this database contains hundreds of tables and you don't know all of them. To investigate a problem you want to run an application and see which tables (probably dozens of them) will be updated and how. Sandbox Database Manager also supports this scenario because it'll allow you to track data changes at the column level.

These are only a few features of Sandbox Database Manager. It can do much more, for example to run the same query against many databases or compare data between two databases. I can guarantee that Sandbox Database Manager is a really, really helpful tool because I use it in my day to day work. I recommend it without any hesitation. What is the best you can use it completely for free!


Do you know OUTPUT clause?


Today, I'll write about using OUTPUT clause together with INSERT statements. It seems to be that it is not a very well known syntax. However, it is especially useful when use Identity columns to generate keys. Let's start with a simple table:
 Id int Identity (1,1) PRIMARY KEY,
 Code varchar(10),
 UpperCode AS Upper(Code)
The old fashioned approach to retrieve a value of Identity column for a new row is to use SCOPE_IDENTITY(). For example:
INSERT INTO dbo.Main (Code) VALUES ('aaa');
With OUTPUT clause it will look in the following way:
DECLARE @InsertedIdentity TABLE(Id int);
INSERT INTO dbo.Main (Code) OUTPUT INSERTED.Id INTO @InsertedIdentity VALUES ('aaa')
SELECT TOP(1) * FROM @InsertedIdentity
You can say wait a minute. If I want to use OUTPUT I have to declare a table variable first and then use SELECT. It is more complex than just using SCOPE_IDENTITY().

Well, the first benefit is that with OUTPUT clause we can read values from many columns, including these that are computed (as it was shown above). However, the real power of OUTPUT clause can be observed if we want to insert many rows into a table:
DECLARE @ToBeInserted TABLE(Code varchar(10), Name varchar(100));

VALUES ('aaa','1111111111'), ('ddd','2222222222'), ('ccc','3333333333');

DECLARE @Inserted TABLE(Id int, Code varchar(10), UpperCode varchar(10));

INSERT INTO dbo.Main (Code)
FROM @ToBeInserted;

SELECT * FROM @Inserted;
Without OUTPUT we would have to write a nasty loop!

Here is one more example. Let's assume that we have an additional table that references dbo.Main.
 MainId int,
 Name varchar(100),
We want to insert a few rows into dbo.Main and then related rows to dbo.Child. It is quite easy if we use OUTPUT clause.
INSERT INTO dbo.Child (MainId, Name)
SELECT i.Id, tbi.Name
FROM @ToBeInserted tbi
 JOIN @Inserted i ON i.Code = tbi.Code;
Extremely useful thing that you must know!

At the end it is worth mentioning that OUTPUT clause can be also used together with UPDATE, DELETE or MERGE statements.


Jak znaleźć brakujące indeksy w bazie danych?


Optymalizacja bazy danych i zapytań to temat rozległy i szeroki jak morze i nie jedną książka napisano na ten temat. Ja dzisiaj napiszę o dosyć prostej technice pozwalającej znaleźć brakujące indeksy w bazie danych MSSQL. Zapewne każdy korzystający z MSSQL Management Studio wie, że można poprosić o wyświetlenie planu wykonania zapytania (opcje Dispaly Actual Execution Plan oraz Include Actual Execution Plan). Dodatkowo po wykonaniu zapytania MSSQL zasugeruje nam jakich indeksów brakuje.

Fajnie, ale co w sytuacji kiedy widzimy, że nasza aplikacja działa wolno. Mamy podejrzenie, że problem dotyczy bazy danych, ale przecież nie będziemy uruchamiali każdego możliwego zapytania w SSMS. W takiej sytuacji możemy de facto użyć tej samej funkcjonalności co w przypadku uruchamiania zapytania z SSMS. Mam tutaj na myśli Missing Indexes Feature, która jest cechą MSSQL, a nie środowiska SSMS. Informacje o brakujących indeksach silnik bazy danych odkłada mianowicie w kilku widokach systemowych z rodziny sys.dm_db_missing_index_*. Wystarczy więc uruchomić aplikację i zobaczyć jakie indeksy sugeruje nam MSSQL. Ja w tym celu używam zapytania, które znalazłem na blogu SQL Authority.

Przykład z życia. Ostatnio musiałem zoptymalizować pewne obliczenia i postąpiłem dokładnie jak napisałem wyżej. Uruchomiłem w aplikację, zmierzyłem czas obliczeń, zapisałem czas ich uruchomienia i zakończenia, a następnie wyświetliłem listę sugerowanych indeksów do utworzenia. Było ich 6. Na początek odrzuciłem te o niskiej wartości w kolumnie Avg_Esitmated_Impact. Z pozostałych indeksów 2 różniły się tym, że jeden miał klauzulę INCLUDE, a drugi nie. Stwierdziłem, że w pierwszym podejściu skupię się na jednym.

W dalszej kolejności wykonałem testy aby zobaczyć jaki uzysk daje założenie każdego z tych 3 indeksów, a także 2 z nich czy wszystkich 3. Okazało się, że zastosowanie jednego z nich skrócił czas obliczeń o ponad 30%, a pozostałe dwa o małe kilka. Dla rzetelności testy powtórzyłem, a wyniki uśredniłem. Na koniec dokładnie przeanalizowałem proponowany indeks i porównałem go do indeksów już utworzonych na tabeli. Okazało się, że istniał już bardzo podobny indeks. Konkretnie, MSSQL zaproponował coś takiego:
CREATE INDEX IX_Test ON dbo.Table(Col_1, Col_2) INCLUDE (Col_4);
A istniejący indeks wyglądał tak:
CREATE INDEX IX_Test ON dbo.Table(Col_1, Col_2, Col_3);
Wystarczyło, wieć go zmodyfikować w następujący sposób:
CREATE INDEX IX_Test ON dbo.Table(Col_1, Col_2, Col_3) INCLUDE (Col_4);
Na koniec sprawdziłem jak taka modyfikacja wpływa na operacje wstawiania/aktualizacji danych do/w docelowej tabeli. W tym celu napisałem zapytania wstawiające setki tysięcy rekordów do tej tabeli, a także takie, które modyfikuje kolumnę Col_4.. Wyniki pokazały niewielkie spadek wydajności. Był on znacznie mniejszy niż zysk przy odczycie danych, a po drugie wiedziałem, że w praktyce omawiana tabela jest częściej czytana niż modyfikowana.

Przy pracy z Missing Indexes Feature warto wiedzieć o kilku dodatkowych rzeczach. MSSQL może nam zasugerować wiele brakujących indeksów i nie koniecznie wszystkie muszą dotyczyć zapytać wykonanych przez nas. Aby wyeliminować ten problem sugeruję wykonywanie takich ćwiczeń na dedykowanej bazie danych. Przydatne będą też kolumny last_user_seek oraz last_user_scan z widoku sys.dm_db_missing_index_group_stats. Zawierają one informacje o tym kiedy dany brakujący indeks był potrzebny. Po pierwsze podany czas możemy porównać z czasem uruchomienia/zakończenia obliczeń i odrzucić te indeksy, które nie mieszczą się w tym zakresie. Po drugie te czasy mogą zgrubnie wskazać, w którym momencie działania aplikacji występuje problem. Napisałem, że przy wyborze indeksów do dalszej analizy bazowałem na kolumnie Avg_Esitmated_Impact. Trzeba na to jednak uważać. Ta wartość to tylko pewne przybliżenie i może nas wyprowadzić na manowce. Z 3 indeksów jakie wybrałem do dalszej analizy największy zysk miał ten o najmniej wartości w tej kolumnie.

Końcowa uwaga jest taka, że Missing Indexes Feature to pomocna rzecz, ale nie jest to magiczna formuła, która rozwiąże wszystkie problemy za nas. Ma też swoje ograniczenia, o których należy wiedzieć.

  • MSSQL sugeruje brakujące indeksy.
  • Brakujące indeksy można odczytać z bazy danych.
  • Testy wydajności należy powtórzyć kilka razy.
  • Testy wydajności dobrze wykonywać w dedykowanym do tych celu środowisku.
  • Missing Indexes Feature to nie magiczna formuła i ma swoje ograniczenia.
  • Proponowane brakujące indeksy należy zawsze poddać analizie i porównać do istniejących indeksów.
  • Należy pamiętać, że indeksy spowalniają operacje aktualizacji i wstawiania danych.
  • Wartość w kolumnie Avg_Esitmated_Impact należy traktować ostrożnie.


CTE i wydajność


Ten post będzie o tym jak można zrobić sobie krzywdę stosując skądinąd bardzo fajne narzędzia. W tym przypadku mam na myśli CTE (ang. Common Table Expressions). Moim zdaniem stosowane z umiarem podnoszą czytelność kodu, z drugiej jednak strony użycie CTE może wpłynąć negatywnie na wydajność naszych zapytań.

Należy pamiętać o tym, że CTE nie mają fizycznej reprezentacji w tempdb tak jak tabele tymczasowe czy zmienne tabelaryczne. Na CTE można patrzeć jak na taki tymczasowy, nie zmaterializowany widok. Kiedy MSSQL wykonuje zapytanie i napotka CTE to odwołanie do tego CTE zastępuję jego definicją. W związku z tym jeśli dane CTE jest używane kilkakrotnie w danym zapytaniu to ten sam kod zostanie wykonany kilka razy i MSSQL tego nie optymalizuje. Ten kod pokazuje to zachowanie:

WITH  Test (Id)  

Na ekran zostaną wypisane 2 różne identyfikatory. Gdyby Test było tabelą otrzymalibyśmy ten sam wynik. W skrajnych przypadkach może to spowodować, że zapytanie będzie koszmarnie wolne. Optymalizacja jest natomiast prosta. Wystarczy w pewnym momencie wrzucić dane do tabeli tymczasowej i dalej używać tej tabeli zamiast odwoływać się do CTE.

Jakiś czas temu widziałem przypadek gdzie dzięki temu jakże prostemu zabiegowi zapytanie zamiast wykonywać się kilka minut wykonywało się kilka sekund!


Jeśli nie na tabela tymczasowa to co?


W poście Quiz - coś do poduszki opublikowałem zestaw pytań z Quiz'u jaki zorganizowałem dla kolegów z pracy. Od czasu do czasu wpadają mi do głowy różne zagadki dlatego postanowiłem, że co ciekawsze będę wrzucał ku pamięci na bloga. Oto pierwsza z nich.

Po wykonaniu poniższego kodu na ekran zostanie wypisana wartość 0. Czego należy użyć zamiast tabeli tymczasowej aby aby na ekran została wypisana wartość 2?
Value VARCHAR(10)



Pokaż/Ukryj odpowiedź

Zamiast tabeli tymczasowej należy użyć zmiennej tabelarycznej.
Value VARCHAR(10)




Dlaczego należy jawnie specyfikować czy kolumna ma akceptować wartości puste czy nie?


Załóżmy, że w procedurze składowanej mamy tabelkę tymczasową:
CREATE TABLE #Temp (Column1 Int, Column2 Varchar(15));
Wszystko działa bez zarzutu, może nawet w środowisku produkcyjnym, aż w pewnym momencie ktoś mówi: Wiesz co dzisiaj Twoja procedura wywaliła się i krzyczy, że kolumna Column1 nie pozwala na wartości NULL. Sprawdzasz błąd na swoim środowisku, ale wszystko działa. Sprawdzasz na innym serwerze, na innej bazie danych i też działa. Co u licha?! Magia czy co?

Jak to zwykle bywa w takich sytuacjach nie magia, ale PEBKAC. Jeśli tworzymy tabelkę tymczasową lub inną i nie podamy jawnie czy kolumna ma akceptować wartości NULL czy nie to domyślnie będzie ona... No właśnie tu tkwi problem, a odpowiedź brzmi to zależy.

Przy standardowych ustawieniach będzie akceptować wartości NULL, ale można to zmienić na poziomie bazy danych, a co gorsza na poziomie sesji użytkownika! Wystarczy, więc mała zmiana i nasz kod przestaje działać. Dlatego dobra praktyka mówi więc aby zawsze jawnie specyfikować czy kolumny mają akceptować puste wartości czy nie.

Co do opcji, które sterują tym zachowanie. Na poziomie bazy danych MSSQL służy do tego komenda:
Natomiast ustawienia sesji użytkownika określamy w SQL Server Management Studio:

Tools->Options->Query Execution->SQL Server->ANSI

Lub przy pomocy jednej z dwóch komend, które aby było łatwiej mają dokładnie przeciwne znaczenie i się wykluczają:


Brakujące konto MSSQLSERVER


Na swoim lokalnym komputerze pliki baz danych trzymam na innym dysku niż systemowy. Raz, że jest to dysk systemowy i chcę go w razie czego w dowolnym momencie sformatować, a dwa, że jest to dysk SSD i na nadmiar miejsca nie narzekam. Na super wydajności mi natomiast nie zależy. Utworzyłem więc katalog db na innym dysku, bazy przestawiłem w tryb offline, skopiowałem pliki mdf oraz ldf i uaktualniłem ścieżki przy pomocy komendy ALTER DATABASE. Na koniec chciałem przełączyć bazy w tryb online, ale MSSQL krzyknął, że nie ma uprawnień do plików mdf/ldf.

Spojrzałem więc jak skonfigurowany jest domyślny katalog używanego przez MSSQL do przechowywania plików baz danych. Okazało się, że właścicielem jest niejaki MSSQLSERVER. Swój katalog db chciałem więc skonfigurować w ten sam sposób. Głupia sprawa ale wyglądało, że takie konto nie istnieje w systemie! Spojrzałem jeszcze raz na domyślny katalog i wszystko się zgadzało. Przejrzałem listę wszystkich kont i nie znalazłem tam nic przypominającego konto MSSQLSERVER.

Znalezienie rozwiązania zajęło mi trochę czasu i w końcu okazało się, że pełna nazwa konta to NTSERVICE\MSSQLSERVER. Aby było łatwiej jest ono niewidoczne w okienku Select Users or Groups ale jeśli wpiszemy je w pole Enter the object names to select: to zostanie znalezione.


Unable to locate trace definition file...


Jakiś czas temu w celu wykrycia przyczyny zakleszczenia (ang. deadlock) musiałem przeanalizować plik Trace File nagrany przez SQL Server Profile. Niestety przy próbie jego otworzenia pojawiły się problemy ponieważ plik został nagrany przy pomocy starszej wersji oprogramowania (10.50.0) niż moja (10.50.2550):

Nie miałem dostępu do maszyny, na której został nagrany log, a więc nie mogłem postąpić zgodnie z radą zawartą w komunikacie. Nie chciałem też tracić czasu i szukać kogoś kto wie gdzie dokładnie ten log został nagrany. Utworzyłem więc kopię pliku Microsoft SQL Server TraceDefinition 10.50.2550.xml i nazwałem ją Microsoft SQL Server TraceDefinition 10.50.0.xml.

Po tej zmianie kolejna próba otwarcia pliku z logiem powiodła się. Nie wiem jakie są różnice pomiędzy wersją 10.50.2550, a 10.50.0 ale ta prosta sztuczka wystarczyła aby odnaleźć przyczynę błędu i ją usunąć. Możliwe, że przy większym rozjechaniu się wersji to podejście nie zadziała ale zawsze warto spróbować.


Indeksy, LIKE oraz =


Prosta zagadka. Rozważmy następującą tabelą z dwoma kolumnami:
   Name CHAR(10)

Teraz na wejściu dostajemy pewien ciąg znaków i przechowujemy go w zmiennej:
DECLARE @Variable CHAR(10);
SET @Variable = '1234567890';
Chcemy znaleźć wszystkie te rekordy, dla których N pierwszych znaków w kolumnie Name jest takie samo jak N pierwszych znaków w zadanym ciągu. Można to zrobić tak (N=3):
SELECT * FROM dbo.Test WHERE  LEFT(Name, 3) = LEFT(@Variable, 3)
Albo tak:
SELECT * FROM dbo.Test WHERE Name LIKE LEFT(@Variable, 3)+'%'
Oba zapytania dadzą ten sam wynik ale jedno z nich będzie zdecydowanie szybsze (dla dużej ilości danych) niż drugie. Które?

W tym przypadku użycie LIKE okazuje się lepszym wyborem. Dlaczego? W pierwszym zapytaniu zostanie użyty INDEX SCAN (czyli de facto odczytane zostaną wszystkie wiersze z tabeli), a w drugim  INDEX SEEK. Dzieje się tak gdyż w pierwszym zapytaniu użyto funkcji LEFT (może to być dowolna inna funkcja) na kolumnie, na której nałożony jest indeks.

Do problemu można też podejść w inny sposób czyli stworzyć indeks na kolumnie wyliczanej tj.:
W takiej sytuacji pierwsze z pokazanych zapytań (te z operatorem =) również użyje operacji INDEX SEEK.




GO to komenda, która sygnalizuje, że narzędzie takie jak sqlcmd powinno wysłać bieżący batch kodu T-SQL do instancji SQL Server'a. Do tej pory nie przywiązywałem do niej dużej uwagi, skupiając się na właściwym kodzie. Pominięcie GO może jednak doprowadzić do kłopotów. Popatrzmy na poniższy skrypt, który tworzy procedurę składowaną pr_Fun.

      DROP PROCEDURE dbo.pr_Fun


GRANT EXECUTE ON dbo.pr_Fun TO public

Na pierwszy rzut oka wygląda prawidłowo i jeśli spróbujemy go uruchomić wykona się bez żadnych problemów. Niestety ale zawiera błąd, który pojawi się dopiero kiedy z procedury pr_Fun spróbuje skorzystać użytkownik z ograniczonymi uprawnieniami. Otrzyma taki komunikat:

Msg 229, Level 14, State 5, Procedure pr_Fun, Line 1
The EXECUTE permission was denied on the object 'pr_Fun', database 'Test', schema 'dbo'.

Dlaczego? Stanie się tak ponieważ wbrew pozorom powyższy skrypt nie nada uprawnień do wykonywania procedury pr_Fun użytkownikom z rolą public. Dzieje się tak z powodu braku komendy GO w odpowiednim miejscu.

Jeśli uruchomimy powyższy skrypt, a potem podejrzymy kod procedury (np.: sp_helptext pr_Fun) to okaże się, że polecenie GRANT EXECUTE zostało dołączone do kodu procedury, zamiast zostać wykonane. Prawidłowa wersja powyższego skryptu powinna wyglądać tak:


GRANT EXECUTE ON dbo.pr_Fun TO public

Niestety ale uruchamiając skrypt z takim błędem nie zostaniemy o tym poinformowani, ani ostrzeżeni. Jest to dla mnie o tyle dziwne, że słowo kluczowe END jawnie wskazuje koniec kodu procedury, a skoro GO jest wymagane to można by o tym poinformować. Po drugie próba uruchomienia skryptu z takim samym błędem, ale tworzącego funkcję nie powiedzie się z powodu takiego błędu:

Msg 156, Level 15, State 1, Procedure fn_Fun, Line 8
Incorrect syntax near the keyword 'GRANT'.

Mała rzecz ale na wszelki wypadek dobrze o tym wiedzieć.


Jeden dziwny znak, a kilka rzeczy do zapamiętania


Niedawno zetknąłem się z raportem, który wypluwał dane do pliku. Przy czym jednym z wymagań było aby trim'ować pola, które zostały dopełnione do wymaganej długości. W tym celu użyto funkcji rtrim. Przeglądając wyprodukowane raporty, zauważyłem jednak, że w niektórych przypadkach zawartość jednego pola zawiera jakieś białe znaki. Na pierszy rzut oka wyglądało to na spacje. Na wszelki wypadek sprawdziłem czy napewno zastosowano rtrim i wszystko się zgadzało.

Rozpocząłem więc poszukiwania czemu rtrim mogło nie zadziałać i od razu nauczyłem się pierwszej rzeczy, do której nie przywiązywałem wcześniej uwagi. rtrim obsługuje tylko spacje i nie poradzi sobie z tabulatorem, znakiem powrotu karetki itd. Poniżej krótki przykład demonstrujący problem (char(9) to tabulator).

DECLARE @string char(10)
SET @string = 'abc' + char(9)

SELECT LEN(@string), LEN(RTRIM(@string))

Rozwiązaniem problemu może być na przykład użycie funckji replace:

SELECT LEN(@string), LEN(RTRIM(@string)), LEN(REPLACE(@string,char(9),SPACE(0)))

W tym przypadku to jednak nie pomogło. W pliku wyjściowym cały czas pojawiały się te "dziwne" spacje. Użyłem więc prostego skryptu aby zobaczyć kody znaków dla pola, które sprawiało problemy. Okazało się, że te dziwne "spacje" to znak o kodzie 255. Teraz w zależności od tego do jakiej tablicy znaków zajrzałem uzyskałem inną informację. Na przykład według ISO 8859-1 to znak łacińska mała litera Y z diarezą/umlautem.

Tutaj dochodzimy do kolejnej ważnej rzeczy, którą łatwo przeoczyć kiedy przez większość czasu pracujemy w dobrze określonym środowisku z takimi, a nie innymi ustawieniami regionalnymi itd. Otóż ten sam znak może wyglądać inaczej kiedy wykonamy zapytanie w Microsoft SQL Server Management Studio ze względu na collation, a inaczej po zapisaniu do pliku i otwarciu w jakimś edytorze ze względu na wybraną w programie stronę kodową itd. Poniższy skrypt pokazuje jak ten sam znak zostanie zaprezentowany dla różnych collation.

DECLARE @string char(10)
SET @string = 'abc' + char(255)
SELECT @string
Collate SQL_Latin1_General_CP1_CI_AS
SELECT @string
Collate SQL_Polish_CP1250_CI_AS

Nie ma w tym nic trudnego i przez większość czasu kwestie kodowania, tablicy znaków... nas nie obchodzą ale warto pamiętać o takich rzeczach bo potem mogą pojawić się cokolwiek zaskakujący efekty, nie zawsze oczywiste do wyjaśnienia.

Post ten dotyczy MSSQL 2008.


Krótka lekcja na temat char i varchar


Na początek prosty kawałek kodu w T-SQL, w którym sprawdzane jest, czy zadany ciąg znaków pasuje do podanego wzorca tj. czy zaczyna się dwoma cyframi:

declare @input char(10)
declare @pattern char(100)

SET @input = '12aaabbb'
SET @pattern = '[0-9][0-9]%'

if @input like @pattern
 print 'OK'
 print 'Fail'

Pomimo, że ciąg znaków pasuje do wzorca to warunek dopasowania nie jest spełniony i na ekran zostanie wypisany napis Fail. Dzieje się tak ponieważ kiedy przypisujemy wzorzec do zmiennej @pattern, która jest typu char(100) to zostanie on dopełniony spacjami. A więc przy testowaniu warunku tak naprawdę sprawdzamy czy zadany ciąg znaków zaczyna się dwoma cyframi i kończy przynajmniej 89 spacjami (100 - długość wzorca).

Można to naprawić przechowując wzorzec w zmiennej typu varchar albo stosując funkcję rtrim. Wszystko zależy od konkretnej sytuacji ale przy wykonywaniu różnych operacji na ciągach znaków należy zawsze pamiętać o różnicy pomiędzy char i varchar. Sprawa wydaje się prosta ale kiedy pracujemy z dużą ilością kodu bazodanowego łatwo można coś przeoczyć, a znalezienie takiego błędu może nie być wbrew pozorom proste.


Podglądanie tabel tymczasowych


Czasami zdarza się, że pracujemy z aplikacją, która tworzy tabelę tymczasową, a następnie woła serię procedur składowanych, które: wypełniają tę tabelę, modyfikują ją itd. W takim wypadku dość często np.: przy debugowaniu błędu, potrzeba podejrzeć zawartość takiej tabeli. Pół biedy kiedy jest to globalna tabela tymczasowa np.: ##SomeTempTable. Wtedy wystarczy zatrzymać aplikację na pułapce i wykonać zapytanie w SQL Server Management Studio np.: select * from ##SomeTempTable.

Niestety, jeśli globalna tabela tymczasowa została utworzona w transakcji, to już nie zadziała. Jeszcze gorzej jest w przypadku lokalnych tabel tymczasowych np.: #SomeTempTable, których nie podejrzymy nawet jeśli zostały utworzone poza transakcją. Co w takim wypadku?

Ja w takiej sytuacji korzystam z możliwości Visual Studio, a dokładniej z potęgi okna Immediate (Debug -> Windows -> Immediate Ctrl + D, I). Zakładam, że wykonując poniższe kroki aplikacja zatrzymana jest na jakiejś pułapce np.: przed uruchomieniem kolejnej procedury robiącej coś z tabelą tymczasową.
  • W oknie Immediate wpisuje DataTable dt = new DataTable();
  • Również w oknie Immediate, wpisuję dt = DataProvider.Instance.ExecuteDataTable("select * from #SomeTempTable");.
  • Przechodzę np.: do okna Watch, podaję nazwę utworzonej zmiennej czyli dt i cieszę się wbudowanym w VS wizualizatorem dla klasy DataTable.

Kilka słów wyjaśnienia w sprawie DataProvider.Instance.ExecuteDataTable. Zakładam tutaj, że jeśli ktoś pisze aplikację bazodanową i nie korzysta z ORM, to ma napisaną jakąś pomocnicza klaskę, która: zarządza połączeniem do bazy danych, pozwala łatwo wykonać zapytania bez potrzeby każdorazowego ręcznego tworzenia DbCommand itd. W tym przypadku jest to klasa DataProvider, która jest singleton'em. Jest to o tyle ważne, że kiedy odwołuję się do niej w oknie Immediate to korzystam z tego samego połączenia co aplikacja, a więc mogę podejrzeć tabele tymczasowe. Pod spodem wykonywane jest zwykłe zapytanie w stylu ADO.NET.

Ostatnio odkryłem również fajny projekt sp_select składający się z dwóch procedur składowanych, które w "magiczny" sposób pozwalają podejrzeć lokalną tabelę tymczasową korzystając z innego połączenia niż to, w którym tabela została utworzona.


Float(1) = Float(24) ???


Jakiś czas temu pracowałem nad komponentem, który odpowiadał za generacją tabel w bazie danych na podstawie zadanej definicji. Dodatkowo kod ten potrafił wykryć różnicę pomiędzy definicją, a faktyczną strukturą tabeli w bazie danych i poinformować o tym użytkownika. Przynajmniej w teorii bo w praktyce czasami twierdził, że definicja nie jest spójna ze stanem faktycznym w bazie danych chociaż na pierwszy rzut oka wyglądała, że jest.

Wspomniany kod zawierał oczywiście kilka błędów, które udało mi się szybko poprawić ale jeden zapadł mi w pamięci. Pewnie dlatego, że jego znalezienie było trochę trudniejsze. Błąd ten pojawiał się kiedy definicja tabeli zawierała kolumnę typu float. W takim wypadku komponent zawsze twierdził, że definicja i strukturą tabeli w bazie danych są inne.

Diagnozę błędu rozpocząłem od przygotowania definicji trywialnej tabeli z jedną kolumną typu float o zadanej precyzji 10. Komponent wygenerował tabelę przy użyciu poniższej komendy DDL i oczywiście stwierdził, że coś jest nie tak.
 SomeNumber float(10) NULL
Do wykrywaniu różnic pomiędzy definicją, a stanem bazy danych skorzystałem z widoków systemowych, a w szczególności z widoku sys.all_columns. Postanowiłem, więc zobaczyć jak wygląda wiersz odpowiadający kolumnie SomeNumber i otrzymałem taki wynik:

object_id name column_id system_type_id user_type_id max_length precision scale ...
2137058649 SomeNumber 1 59 59 4 24 0 ...

Moją uwagę od razu zwróciła zawartość kolumny precision równa 24. Zgodnie z definicja powinno być natomiast 10. Chwila konsternacji, szybkie spojrzenie do dokumentacji i wszystko okazało się jasne. SQL Server ze względu na zgodność ze standardem ISO pozwala napisać float(10), float(20) itp. ale wewnętrznie wartości od 1 do 24 traktuje jak 24, a wartości od 25 do 53 jako 53. Po uwzględnieniu tej informacji komponent zaczął działać prawidłowo.


Błąd przy dodawaniu przystawki do konsoli zarządzania


Przystawka certyfikatów (ang. Certificates Snap-in) pozwala na przeglądanie magazynu certyfikatów dla użytkownika, usługi czy też komputera. Aby uruchomić to narzędzie wystarczy w wierszu poleceń lub do okienka Uruchom wpisać polecenie certmgr.msc. Problem polega na tym, że tak uruchomiona przystawka certyfikatów pokaże nam tylko magazyn dla bieżącego użytkownika.

Jeśli chcemy zobaczyć magazyn certyfikatów komputera, tak było w moim przypadku, czeka nas trochę więcej pracy. Zaczynamy od uruchomienia konsoli zarządzania (ang. Microsoft Management Console) wpisując w wierszu poleceń lub do okienka Uruchom komendę mmc.exe. Następnie wybieramy Plik -> Dodaj/Usuń przystawkę. Przystawka certyfikatów znajduje się na początku listy i po jej wybraniu zostaniemy poproszeni o zaznaczenie jakimi certyfikatami chcemy zarządzać: użytkownika, usługi czy komputera. Na koniec klikamy Ok.

Tak to wygląda w teorii, w praktyce po naciśnięciu przycisku Ok konsola zarządzania raportowała błąd (pokazany poniżej) i kończyła pracę, bez względu na to jaka przystawkę wybrałem.
  Nazwa zdarzenia problemu: APPCRASH
  Nazwa aplikacji: mmc.exe
  Wersja aplikacji: 6.0.6002.18005
  Sygnatura czasowa aplikacji: 49e02760
  Nazwa modułu z błędem: StackHash_7ae8
  Wersja modułu z błędem: 6.0.6002.18327
  Sygnatura czasowa modułu z błędem: 4cb74dd3
  Kod wyjątku: c0000374
  Przesunięcie wyjątku: 00000000000aca57
  Wersja systemu operacyjnego: 6.0.6002.
  Identyfikator ustawień regionalnych: 1045
  Dodatkowe informacje 1: 7ae8
  Dodatkowe informacje 2: fab1f7793b8a08e05290bb8ef1ca5c9e
  Dodatkowe informacje 3: 1607
  Dodatkowe informacje 4: 3b4ea5c6cc4724ebe1b8e0ae80fae1cf
Pan Google nie był zbyt pomocny. Radził aby zainstalować SP2 dla Visty, który już mam zainstalowany. Na innej stronie ktoś twierdził, że problem pojawia się jeśli na jednej maszynie zainstalowane są dwie wersje MSSQL i że trzeba jedną z nich odinstalować. Ja mam akurat zainstalowane dwie wersje MSSQL ale nie miałem najmniejszej ochoty usuwać z dysku żadnej z nich. Trzecia osoba radziła aby na komputerze o zbliżonej do problematycznego konfiguracji, na którym mmc.exe działa, wyeksportować klucze rejestru dotyczące konsoli i zaimportować je na komputerze, na którym występuje problem. Ta rada też nie przypadła mi do gustu ponieważ nie miałem takiego komputera pod ręką. Nie wiedziałem także, na ile ta konfiguracja powinna być "zbliżona" aby było dobrze.

Postanowiłem jednak pójść tropem zawartości rejestru i konfliktu pomiędzy różnymi wersjami MSSQL. Na początek zauważyłem, że na liście dostępnych przystawek znajdują się dwie przystawki o takiej samej nazwie SQL Server Configuration Manager. Zapewne dedykowane dla różnych wersji MSSQL. Następnie postanowiłem zajrzeć do rejestru do klucza, który przechowuje listę wszystkich dostępnych przystawek:


Jest ich tam kilkadziesiąt i mają niewiele mówiące nazwy np.: d52e5f54-75d9-4a93-91b7-2215ea5cbed2 ale szybko udało mi się znaleźć klucze odpowiadające przystawce SQL Server Configuration Manager. Pomyślałem sobie "raz kozie śmierć", zobaczę co się stanie jak usunę jeden z nich. Oczywiście najpierw wyeksportowałem kopię klucza, a dopiero potem go usunąłem. Okazało się to strzałem w dziesiątkę. Po tej operacji dodanie nowej przystawki w końcu zadziałało. Co ciekawe nie ma znaczenia czy usuniemy klucz przystawki dla MSSQL 2005, czy dla MSSQL 2008.

Reasumując. Jeśli operacja dodania nowej przystawki konsoli zarządzania kończy sie błędem, a masz na komputerze zainstalowane dwie (lub więcej) wersji MSSQL to z dużym prawdopodobieństwem problem spowodowany jest konfliktem pomiędzy przystawkami SQL Server Configuration Manager dla różnych wersji MSSQL. Można go rozwiązać usuwając odpowiedni wpis z rejestru. Nie jest to idealne rozwiązanie, ale z braku laku dobry kit.




W poście Problem z domyślnym schematem wspomniałem, że w celu wyjaśnienia problemu posłużyłem się funkcją CURRENT_USER, która zwraca nazwę bieżącego użytkownika. Czym jednak funkcja ta różni się od funkcji SYSTEM_USER, SESSION_USER czy innych o podobnych nazwach. Sprawa jest prosta. Część z tych funkcji zwraca login, który służy do uwierzytelnienie się względem serwera, a część nazwę użytkownika, która określa co możemy zrobić w ramach poszczególnych baz danych (autoryzacja). Każdy login jest skojarzony (zmapowany) z jednym użytkownikiem dla danej instancji bazy danych. Nie jest to nic skomplikowanego ale można się zgubić w gąszczu nazw. Dlatego ku pamięci, głównie dla siebie, zebrałem te informacje w postaci poniższej tabelki.

CURRENT_USERZwracają nazwę użytkownika

SYSTEM_USERZwracają login

Więcej szczegółów np.: parametry wywołania, wyjaśnienie skąd wzięło się tyle funkcji robiących to samo, można znaleźć tutaj.


Problem z domyślnym schematem


Kilka dni temu chciałem zmodyfikowałem domyślny schemat dla jednego z użytkowników, niech nazywa się SomeUser. W tym celu otworzyłem Management Studio, wybrałem interesującą mnie bazę danych i przeszedłem do listy użytkowników. Dalej wybrałem interesującego mnie użytkownika i wyświetliłem dla niego okno właściwość i w polu Default schema: wpisałem nazwę schematu, dla ustalenia uwagi niech nazywa się testSchema.

Żeby być pewnym, że wszystko jest w porządku nawiązałem połączenie z serwerem korzystając z wspomnianego użytkownika i spróbowałem wykonać trywialne zapytanie pobierające dane z tabeli znajdującej się w schemacie testSchema:
select *
from TestTable
Ku swojemu zdziwieniu otrzymałem komunikat o treści Invalid object name 'TestTable'. Sprawdziłem, więc zapytanie zawierające pełną nazwę tabeli (razem ze schematem) i zadziałało:
select *
from testSchema.TestTable
Aby upewnić się, że pracuję w kontekście właściwego użytkownika posłużyłem się poleceniem select current_user, które ponownie ku mojemu zdziwieniu wypisało na ekran dbo zamiast SomeUser. No cóż może się pomyliłem. Ponownie połączyłem się z serwerem upewniając się, że korzystam z dobrego użytkownika ale nic się nie zmieniło.

Problem pomógł mi rozwiązać kolega, który zwrócił uwagę, że SomeUser ma przypisaną rolę sysadmin, a więc w rzeczywistości był widziany jako użytkownik dbo (dla którego domyślny schemat to dbo). Po zabraniu użytkownikowi roli sysadmin, która nie była mu zresztą potrzebna, wszystko zaczęło działać jak trzeba.


Pobranie fabryki klasy COM...


Niedawno zostałem poproszony o pomoc w rozwiązaniu problemu z aplikacją ASP.Net do zarządzania bazą danych, który polegał na pojawianiu się poniższego błędu przy jej uruchamianiu:

"Pobranie fabryki klasy COM dla składnika o identyfikatorze CLSID {10020200-E260-11CF-AE68-00AA004A34D5} nie powiodło się z powodu następującego błędu: 80040154."

Błąd ten oznacza tyle, że w systemie nie ma zarejestrowanej klasy COM o podanym identyfikatorze. Ponieważ miałem pod ręką drugi komputer postanowiłem sprawdzić co kryje się za tajemniczym numerem CLSID. W tym celu uruchomiłem edytor rejestru (regedit) i przeszukałem gałąź HKEY_CLASSES_ROOT\CLSID w poszukiwaniu pasującego identyfikatora. Szczęście uśmiechnęło się do mnie i szybko znalazłem odpowiedni klucz z ścieżką do biblioteki dll:

C:\Program Files\Microsoft SQL Server\80\Tools\binn\SQLDMO.DLL

Dalej poszło już prosto. SQLDMO czyli SQL Distributed Management Objects to zbiór obiektów umożliwiających wykonywanie różnych czynności administracyjnych, które standardowo instalowane są razem SQL Server Client Utilities. Ponieważ nie miałem możliwości zainstalowania tego zestawu narzędzi postanowiłem po prostu skopiować wspomnianą bibliotekę i zarejestrować ją przy pomocy polecenia regsvr32. W rozwiązaniu problemu przydatny okazał sie również ten artykuł pomocy technicznej Microsoft. Dzięki niemu dowiedziałem się, że oprócz biblioteki Sqldmo.dll potrzebny jest też plik zasobów Sqldmo.rll. W artykule wymieniono również listę innych bibliotek związanych z modelem obiektów SQL-DMO. Okazało się jednak, że na problematycznym komputerze brakuje tylko tej jednej biblioteki. Po jej zarejestrowaniu aplikacja ASP.Net zaczęła działać.