Posts tagged T-SQL

[PL] Najlepsze praktyki dla programisty T-SQL – collation i "obce znaczki"

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

Na tegorocznej konferencji Microsoft Technology Summit (MTS) przedstawiłem wraz z Markiem Adamczukiem prezentację pt. "Najlepsze praktyki dla programisty Transact-SQL". Po tej sesji sporo osób prosiło mnie o materiały, a głównie demonstracje. Dobra informacja jest taka, że materiały te można już pobrać ze strony konferencji. Najwięcej dyskusji po prezentacji dotyczyło fragmentu poświęconego collation i pracy z danymi tekstowymi pochodzącymi z różnych języków. Dlatego, po rozmowie z Markiem, postanowiłem napisać niniejszy artykuł, w którym omówię wybrane najlepsze praktyki posługując się kodem zaprezentowanym na MTS.

Nvarchar – N ma znaczenie

Typy danych nchar i nvarchar umożliwiają przechowywanie w bazie danych tekstów zawierających znaki charakterystyczne dla różnych języków. Przykładowo, mamy bazę w collation "rosyjskim", a w jednej z kolumn w tabeli w tej bazie chcemy przechować tekst w języku polskim. Wówczas typ danych nvarchar doskonale nadaje się do przechowywania danych (co ciekawe, kolumna tego typu ma nadal przypisane collation "rosyjskie"). Co jest jednak istotne w takim przypadku? Ważne jest, by przy używaniu literałów tekstowych dawać systemowi SQL Server znać, że podany tekst ma być traktowany jako Unicode. Do tego służy prefiks N umieszczany bezpośrednio przed literałem (wielkość litery ma znaczenie). Poniższy przykład dobrze obrazuje tę zasadę. Bez prefiksu N polskie znaki w napisie, który otrzymuje collation bazy danych, po prostu znikają (w niektórych przypadkach zamiast znaków charakterystycznych dla różnych alfabetów pojawiają się znaki zapytania lub inne "krzaczki"). Po dodaniu prefiksu N polskie znaki są wstawiane w kolumnę typu nvarchar bez problemu, mimo że collation bazy i kolumny jest "rosyjskie".

--Tworzymy "rosyjską" bazę
USE master;
GO
IF DB_ID('RussianDB') IS NOT NULL BEGIN
  ALTER DATABASE RussianDB SET SINGLE_USER
  WITH ROLLBACK IMMEDIATE;
  DROP DATABASE RussianDB;
END;
GO
CREATE DATABASE RussianDB COLLATE Cyrillic_General_CI_AS;
GO
 
USE RussianDB;
GO
 
--Tabela z kolumną nvarchar
CREATE TABLE dbo.Tab (c nvarchar(20) NULL);
GO
 
--Wstawiamy dane
INSERT INTO dbo.Tab (c) SELECT 'ąęśćłóżźń';
GO
 
--Test
SELECT * FROM dbo.Tab;
GO
 
--A teraz zróbmy to, jak trzeba
INSERT INTO dbo.Tab (c) SELECT N'ąęśćłóżźń';
GO
 
--Test
SELECT * FROM dbo.Tab;
GO

Długa czy krótka kolacja?

W SQL Server mamy bogactwo możliwych do wyboru collation. Zawsze, gdy tworzymy nową bazę danych, stajemy przed wyborem, jakie collation nadać tworzonej bazie? Pierwsza myśl – niech baza ma takie collation, jak bazy systemowe (czyli takie collation, jakie wybraliśmy przy instalacji SQL Servera). A jeśli ma mieć inne collation? Co wybierać – collation SQL-owe (prefiks SQL_ – zwane dalej "długim collation") czy raczej collation rodem z systemu Windows (np. Polish_CI_AS – zwane dalej "krótkim collation")? Istnieje kilka przesłanek przemawiających na korzyść collation "Windowsowych". Pierwsza przesłanka – niejawne rzutowania w przypadku, gdy do kolumny o "długim collation" spróbujemy wstawić dane typu nvarchar. Wówczas niejawna konwersja kolumny na nvarchar spowoduje niemożliwość wykorzystania ewentualnego indeksu (i odbędzie się skanowanie tabeli). W przypadku kolumny z "krótkim collation" problem znika – dane Unicode są rzutowane do typu varchar i indeks może zostać wykorzystany.

--Tabela z "długim" collation
CREATE TABLE #t1 (
  c1 varchar(128)
    COLLATE SQL_Polish_CP1250_CS_AS NOT NULL
  );
CREATE CLUSTERED INDEX IX_1 ON #t1 (c1);
GO
 
--Tabela z "krótkim" collation
CREATE TABLE #t2 (
   c2 varchar(128) COLLATE Polish_CI_AS NOT NULL
   );
CREATE CLUSTERED INDEX IX_2 ON #t2 (c2);
GO
 
--Dane
INSERT INTO #t1 (c1)
SELECT name FROM sys.all_objects;
GO
INSERT INTO #t2 (c2)
SELECT name FROM sys.all_objects;
GO
 
--Test
SELECT * FROM #t1 WHERE c1 = N'objects';
SELECT * FROM #t2 WHERE c2 = N'objects';
GO

image

Innym zachowaniem "długich collation", które może być powodem do zaniechania ich wykorzystywania, jest sortowanie tekstów, w których pojawiają się myślniki. W zasadzie komentarz jest zbędny – wystarczy rzut oka na wyniki zapytania z sortowaniem, by stwierdzić, że wynik dla "długiego collation" jest dość nieoczekiwany.

--Zmieńmy zawartość
TRUNCATE TABLE #t1;
TRUNCATE TABLE #t2;
INSERT INTO #t1 (c1)
VALUES ('a'), ('a-'), ('a-a');
INSERT INTO #t2 (c2)
VALUES ('a'), ('a-'), ('a-a');
GO
 
--Problem sortowania i myślników
SELECT * FROM #t1 ORDER BY c1;
SELECT * FROM #t2 ORDER BY c2;
GO

image

Collation a tabele tymczasowe

Jeśli już zdecydujemy się na wybór dla bazy danych collation innego niż collation serwera, musimy liczyć się z tym, że nasza nowo utworzona baza będzie miała inne collation niż baza tempdb. Co za tym idzie? Gdy będziemy używali tabel tymczasowych w kodzie proceduralnym w naszej bazie, możemy napotkać na konflikty collation w sytuacjach, gdy będą wykonywane operacje porównywania czy sortowania tekstu.

USE RussianDB;
GO
 
IF OBJECT_ID('dbo.T11', 'U') IS NOT NULL
  DROP TABLE dbo.T11;
GO
CREATE TABLE dbo.T11 (
  c11 varchar(128) NOT NULL
);
GO
INSERT INTO dbo.T11 (c11)
VALUES ('MTS');
GO
 
IF OBJECT_ID('tempdb.dbo.#T11', 'U') IS NOT NULL
  DROP TABLE #T11;
GO
CREATE TABLE #T11 (
  c11 varchar(128) NOT NULL
);
GO
 
-- Test - konflikt collation
SELECT * FROM dbo.T11
UNION
SELECT * FROM #T11;
GO
Msg 468, Level 16, State 9, Line 3
Cannot resolve the collation conflict between "Polish_CI_AS" and "Cyrillic_General_CI_AS" in the UNION operation.

Rozwiązanie połowiczne – użyć składni SELECT INTO do stworzenia tabeli tymczasowej jako kopii istniejącej tabeli. Wówczas collation kolumn tekstowych są identyczne z tymi z tabeli źródłowej.

IF OBJECT_ID('tempdb.dbo.#T11', 'U') IS NOT NULL
  DROP TABLE #T11;
GO
SELECT TOP 0 * INTO #T11 FROM dbo.T11;
GO
 
-- Test: ciut lepiej, bo działa
SELECT * FROM dbo.T11
UNION
SELECT * FROM #T11;
GO

Nie zawsze jednak mamy tabelę wzorcową, na której możemy oprzeć tabelę tymczasową. Dlatego najlepszym rozwiązaniem wydaje się być użycie klauzuli COLLATE DATABASE_DEFAULT. Wówczas kolumna tekstowa otrzymuje collation identyczne z tym, jakie zostało przypisane bazie danych, w kontekście której uruchamiamy kod.

IF OBJECT_ID('tempdb.dbo.#T11', 'U') IS NOT NULL
  DROP TABLE #T11;
GO
CREATE TABLE #T11 (
  c11 varchar(128)
  COLLATE DATABASE_DEFAULT
  NOT NULL
);
GO
 
-- Test: jest dobrze :)
SELECT * FROM dbo.T11
UNION
SELECT * FROM #T11;
GO

Podsumowanie

Powyższe przykłady ilustrują pewne wybrane przeze mnie i Marka problemy dotyczące danych tekstowych w SQL Server. Oczywiście, podobnych problemów jest więcej (np. jak zmienić collation istniejącej bazy?), ale wydawało nam się, że przedstawione dema pozwolą na ominięcie najczęściej występujących pułapek. No i druga sprawa – mieliśmy tylko 60 minut na prezentację :-) W tym miejscu pragnę podziękować wszystkim, którzy byli na naszej sesji na MTS i wypełnili ankietę dotyczącą naszej prezentacji. Jest mi też niezmiernie miło, że nasz przekaz spotkał się z bardzo pozytywnym odbiorem. Gdyby była potrzeba opisania w kolejnym artykule jakiegoś fragmentu kodu z naszych demonstracji, dajcie znać, a chętnie opublikuję kolejny fragment tekstu na blogu.

[PL] SQL Server – Kiedy SELECT INTO nie daje rady

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

Swego czasu na portalu WSS.pl uczestniczyłem w dyskusji na temat tego, czy należy używać składni SELECT…INTO. Wytaczaliśmy różne tezy: od takich, że kiedyś ta składnia powodowała blokowania, aż po stwierdzenia, że nie jest dobrze tworzyć tabele o nieznanym schemacie. Jednak tak na dobrą sprawę nie podaliśmy żadnego sensownego argumentu przeciw wspomnianej składni.

I tym razem nie podam takiego argumentu, a jedynie pokażę przypadek, w którym SELECT…INTO powoduje błąd i nie można go najzwyczajniej w świecie użyć. Gwoli ścisłości dodam, że w bazach danych (głównie hurtownianych), którymi obecnie się zajmuję, SELECT…INTO jest używane notorycznie i z bardzo dobrym skutkiem (bardzo często z warunkiem w stylu WHERE 1=0, by generować puste struktury, z których później generuję dane – najczęściej moją “ofiarą” pada wówczas arkusz Excela, w którym dane zostały zapisane na nagłówkach kolumn…).

A więc, kiedy SELECT…INTO nie działa?

Weźmy taką procedurę:

USE tempdb;
GO
CREATE PROCEDURE dbo.SomeProcedure
  @param int
AS
IF @param = 1
  SELECT database_id AS ID INTO #TMP FROM sys.databases;
ELSE
  SELECT [object_id] AS ID INTO #TMP FROM sys.objects;
SELECT * FROM #TMP;
GO

Wynkiem uruchomienia powyższego kodu na SQL Server 2005 lub nowszym będzie następujący komunikat błędu:

Msg 2714, Level 16, State 1, Procedure SomeProcedure, Line 7
There is already an object named '#TMP' in the database.

A zatem, jeżeli chcemy wielokrotnie użyć SELECT…INTO w jednej procedurze, by warunkowo założyć tabelę tymczasową o strukturze zależnej od zaistniałego przypadku, nie uda nam się to, choć z logicznego punktu widzenia nie ma żadnych przeszkód, by taki kod kompilował się i uruchamiał się z powodzeniem.

Pamiętam, że wiele razy uczestniczyłem już w dyskusjach na temat wykorzystania tabel tymczasowych w procedurach składowanych i na podobne problemy natknęło się sporo osób. Może się zdarzyć, ale coraz częściej dochodzę do wniosku, że w przypadku zaszywania w procedurze co bardziej skomplikowanej logiki wymagającej wykonywania obliczeń, przeliczeń, agregacji itd. tabele tymczasowe są po prostu niezastąpione z uwagi na łatwość śledzenia kolejnych kroków procesu (debugowanie i odnajdywanie problematycznych miejsc jest dużo łatwiejsze niż w przypadku tworzenia np. mocno zagnieżdżonych podzapytań, CTE czy używania zmiennych tabelarycznych). Oczywiście, nadal trzeba pamiętać o tym, że w środowisku mocno transakcyjnym z jednoczesnym dostępem wielu użytkowników do danych, tabele tymczasowe mogą stanowić źródło licznych problemów wydajnościowych (“tempdb contention” i rekompilacje to chyba najczęstsze problemy).

Tak czy siak, chyba powoli nawracam się na tabele tymczasowe ;-)

[PL] SQL Server "Denali" CTP1 – Część 2.: T-SQL i programowanie

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

Po badaniu SQL Server “Denali” CTP1 pod kątem nowości w SQL Server Management Studio przyszedł czas na eksplorację nowinek w T-SQL / programowaniu.

Poniżej to, co wyczytałem w dokumentacji lub samemu “namacałem” bawiąc się CTP1:

  • Nowe DMVs:
    • sys.dm_db_objects_disabled_on_compatibility_level_change
    • sys.dm_db_uncontained_entities
    • sys.dm_exec_describe_first_result_set
    • sys.dm_exec_describe_first_result_set_for_object
    • sys.dm_fts_index_keywords_by_property
    • sys.dm_hadr_availability_group_states
    • sys.dm_hadr_availability_replica_states
    • sys.dm_hadr_database_replica_states
    • sys.dm_hadr_database_synchronization_states
    • sys.dm_hadr_instance_node_map
    • sys.dm_hadr_name_id_map
    • sys.dm_logconsumer_cachebufferrefs
    • sys.dm_logconsumer_privatecachebuffers
    • sys.dm_logpool_consumers
    • sys.dm_logpool_hashentries
    • sys.dm_logpool_sharedcachebuffers
    • sys.dm_logpool_stats
    • sys.dm_logpoolmgr_freepools
    • sys.dm_logpoolmgr_respoolsize
    • sys.dm_logpoolmgr_stats

Dość dobrze opisane są póki co DMVs sys.dm_exec_describe_*, które zwracają metadane dotyczące wyników zapytań / wykonania procedur. Dla przykładu zapytanie:

SELECT *
FROM sys.dm_exec_describe_first_result_set
  (N'SELECT object_id, name, type_desc FROM sys.indexes', null, 0);

zwróci metadane trzech kolumn (nazwy, typy danych, długości, nullability, etc.). Nie trzeba będzie bawić się w tworzenie tabel tymczasowych / rzutowania na sql_variant, by wykryć typy danych dla zwracanych kolumn.

Będzie też można łatwo dowiedzieć się, jak SQL Server typuje literały. Przykład:

SELECT * FROM sys.dm_exec_describe_first_result_set (N'SELECT 1', NULL, 0);

I już wiem, że 1 oznacza dla niego typ int. Bajer :-)

  • Nowe widoki systemowe:
    • sys.availability_groups
    • sys.availability_replicas
    • sys.registered_search_properties
    • sys.registered_search_property_lists
    • sys.sequences
  • EXECUTE … WITH RESULT SETS – ma gwarantować właściwą ilość i postać wyników (typowane tabele). Przykład:
EXEC ('SELECT 1, 2, 3')
WITH RESULT SETS (
    (
        A numeric(4,2) NOT NULL,
        B numeric(4,2) NOT NULL,
        C numeric(4,2) NOT NULL
    )
);

Wynik:

A    B     C
---- ----- ----
1.00 2.00  3.00

Z kolei uruchomienie takiego kodu:

EXEC ('SELECT 1, 2, 3')
WITH RESULT SETS NONE;

powoduje zwrócenie następującego błędu:

Msg 11535, Level 16, State 1, Line 1
EXECUTE statement failed because its WITH RESULT SETS clause specified 0 result set(s), and the statement tried to send more result sets than this.
  • THROW – polecenie, które ma zastąpić RAISERROR. Służy do wywoływania wyjątków. Póki co sprawdziłem, że działa bez parametrów w bloku CATCH (po prostu przekazuje napotkany wyjątek dalej) i sprawdza się jako generator błędów o severity 16:
THROW 50001, 'Error!', 1;

Ciekawa rzecz, że nie wygląda na to, by można było w THROW sterować poziomem błędu (patrz dokumentacja: http://msdn.microsoft.com/en-us/library/ee677615%28v=SQL.110%29.aspx).

  • Stronicowanie w ORDER BY:
SELECT number
FROM master.dbo.spt_values
WHERE number BETWEEN 1 AND 1024
AND type = 'P'
ORDER BY number
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;

Powyższe zapytanie zwróci 10 wierszy (numerki od 11 do 20), a bliższe przyjrzenie się planowi zapytania pokazuje, że operacja wybiera z tabeli 20 wierszy, po czym FETCH działa jak TOP i wybiera 10 wierszy. Oczywiście, OFFSET / FETCH ma sporo ograniczeń, o czym można się przekonać czytając dokumentację: http://msdn.microsoft.com/en-us/library/ms188385%28v=SQL.110%29.aspx.

  • Sekwencje – czyli pseudo-autonumeracja w wielu tabelach jednocześnie. Jako komentarz niech posłuży kod:
USE Test;
GO
CREATE TABLE dbo.T1 (C1 int NOT NULL);
CREATE TABLE dbo.T2 (C2 int NOT NULL);
GO
CREATE SEQUENCE dbo.S1 AS int START WITH 1 INCREMENT BY 1;
GO
INSERT INTO dbo.T1 (C1) VALUES (NEXT VALUE FOR dbo.S1);
INSERT INTO dbo.T2 (C2) VALUES (NEXT VALUE FOR dbo.S1);
GO
SELECT * FROM dbo.T1;
SELECT * FROM dbo.T2;
GO

Wynik? Po jednym rekordzie w obu tabelach – w tabeli T1 wartość C1 = 1, w tabeli T2 wartość C2 = 2. Dobrze, że na świecie istnieje coś takiego, jak konkurencja. Wyraźnie widać bowiem, że Microsoft dodaje funkcjonalności przypominające mechanizmy działające od lat w innych systemach zarządzania bazami danych :-)

I to na razie tyle z moich eksploracji. Jeśli ktoś znajdzie więcej “what’s new”, niech da znać. Już w CTP1 sporo jest breaking changes (nawet można je obejrzeć: http://msdn.microsoft.com/en-us/library/ms143179%28SQL.110%29.aspx), więc można się spodziewać, że ich finalna lista będzie dłuższa. Przede mną teraz bliższe spotkanie z nowymi SSIS, bo w CTP1 udokumentowano parę zmian. A zatem, stay tuned! :-)

[PL] Konkurs T-SQL – Wyniki

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

Po długich testach pora na ogłoszenie wyników konkursu T-SQL “Liga piłkarska”. W konkursie wzięło udział 17 osób, które nadesłały ponad 60 rozwiązań. Rekordzista – Leszek Gniadkowski – nadesłał 15 rozwiązań. Ale zdarzały się też osoby, które nadesłały jedno, ale za to całkiem nieźle wypadające w testach zapytanie.

Na początek słowo o teście finałowym skierowane głównie do finalistów. Polegał on na uruchamianiu Waszych skryptów na trzech zestawach danych – 20, 50 i 100 drużyn. Dwa z finałowych rozwiązań musiałem wykluczyć z testów już po pierwszym podejściu do zestawu 20 drużyn, ponieważ nie mogłem doczekać się wyników… Pozostałe zostały poddane testom wielokrotnym (na każdym zestawie puszczałem skrypty w pętli po 10 razy, a same pętle były puszczone po 5 razy – czyli w sumie każdy skrypt był uruchomiony 50 razy na każdym zestawie danych). Przed każdym uruchomieniem skryptu czyściłem bufor danych i plan cache, żeby uniknąć przypadkowości. Całość testu przeprowadziłem na laptopie o parametrach: Core 2 Duo (ale tylko jeden procesor używany przez SQL Server) + 4 GB RAM (1 GB przydzielony na bufor danych). Zapytania były monitorowane przez SQL Trace, z którego następnie wyciągałem średnie wartości czterech kryteriów (duration, CPU, reads, writes) dla poszczególnych testów (“20”, “50” i “100”). Dla każdego testu robiłem osobną tabelę. Oto owe tabele:

Test “20”

Lp Rozwiązanie D C R W RD RC RR RW T
1 Gniadkowski_Leszek_v15.sql 42602 41 22 0 1 1 1 1 4
2 Gniadkowski_Leszek_v10.sql 52062 45,2 25 0 2 2 2 1 7
3 Gniadkowski_Leszek_v12.sql 52642 45,8 25 0 3 3 2 1 9
4 Nowakowski_Marcin_v04.sql 121766 108,4 161 0 4 4 9 1 18
5 Powichrowski_Marek_v12.sql 177409,4 146,8 37 0 7 7 4 1 19
6 Przeliorz_Tomek_v02.sql 156648,2 135,6 325 0 6 6 11 1 24
7 Zmuda_Katarzyna_v01.sql 125126,2 113,6 633 0 5 5 14 1 25
8 Nowakowski_Marcin_v03.sql 255193,6 226,8 314 0 8 11 10 1 30
8 Sliwa_Krzysiek_v02.sql 262054 223,6 126 0 11 10 8 1 30
10 Pater_Rafal_v03.sql 278655,4 251,6 84 0 12 13 5 1 31
11 Poniatowski_Aleksander_v01.sql 282295,2 241,4 120 0 13 12 7 1 33
12 Pater_Rafal_v04.sql 300416,4 257,8 84 0 14 14 5 1 34
13 Powichrowski_Marek_v13.sql 261114 213,2 1123 0 10 9 17 1 37
14 Przeliorz_Tomek_v01.sql 258753,8 212,6 4771 0 9 8 20 1 38
15 Gailard_Pawel_v02.sql 423823,2 389,8 581 0 15 15 13 1 44
16 Pakulski_Maciej_v03.sql 1207088,2 998,2 578 0 17 18 12 1 48
17 Kulczynski_Przemyslaw_v04.sql 1109802,6 942,6 3066 0 16 16 18 1 51
18 Kulczynski_Przemyslaw_v01.sql 1225069,2 983 3120 0 18 17 19 1 55
19 Grabowska_Katarzyna_v04.sql 1680515,2 1327,6 1092 0 20 19 16 1 56
20 Pakulski_Maciej_v02.sql 3367231,6 2823,8 694 0 22 22 15 1 60
21 Sowa_Piotr_v10.sql 1901548 1603 6652 0 21 21 21 1 64
22 Cerekwicki_Cezary_v01.sql 1677855 1425 64485,8 2,8 19 20 22 22 83
23 Waluszko_Bartlomiej_v02.sql - - - - - - - - -
24 Jacewicz_Lukasz_v02.sql - - - - - - - - -

Legenda:

D – średnie duration
C – średnie CPU
R – średnie reads
W – średnie writes
RD – miejsce w kryterium duration
RC – miejsce w kryterium CPU
RR – miejsce w kryterium reads
RW – miejsce w kryterium writes
T – suma miejsc z wszystkich kryteriów (im mniejsza, tym lepiej)

Test “50”

Lp Rozwiązanie D C R W RD RC RR RW T
1 Gniadkowski_Leszek_v15.sql 71823,2 64,2 40 0 1 1 1 1 4
2 Gniadkowski_Leszek_v10.sql 78343,8 69,8 49 0 2 2 2 1 7
3 Gniadkowski_Leszek_v12.sql 78464 73,8 49 0 3 3 2 1 9
4 Nowakowski_Marcin_v04.sql 179449,6 160,4 85 0 4 4 4 1 13
5 Sliwa_Krzysiek_v02.sql 317637,4 293,2 121 0 6 6 6 1 19
6 Poniatowski_Aleksander_v01.sql 332818,2 295,4 103 0 7 7 5 1 20
7 Przeliorz_Tomek_v02.sql 201770,8 181,4 925 0 5 5 13 1 24
8 Zmuda_Katarzyna_v01.sql 414422,8 386,2 135 0 8 8 9 1 26
9 Pater_Rafal_v03.sql 604693,6 544,2 132 0 11 11 7 1 30
10 Nowakowski_Marcin_v03.sql 450784,6 404,4 733 0 9 9 12 1 31
11 Gailard_Pawel_v02.sql 499387,8 459 366 0 10 10 11 1 32
11 Pater_Rafal_v04.sql 605213,6 570,2 132 0 12 12 7 1 32
13 Grabowska_Katarzyna_v04.sql 2113640 1714,6 254,2 0 15 15 10 1 41
13 Powichrowski_Marek_v12.sql 753222 589,4 1597 0 13 13 14 1 41
15 Powichrowski_Marek_v13.sql 1063860 877 5019 0 14 14 15 1 44
16 Przeliorz_Tomek_v01.sql 2205305 1838,8 34441 0 16 16 17 1 50
17 Pakulski_Maciej_v03.sql 7804265,2 6686,8 27196 0 19 19 16 1 55
18 Pakulski_Maciej_v02.sql 39569582,4 36671,4 199326 0 22 22 21 1 66
19 Sowa_Piotr_v10.sql 6398825 5377,8 56028 39,8 17 17 18 20 72
20 Kulczynski_Przemyslaw_v01.sql 11576081,4 10360 133432 29,6 20 20 19 19 78
21 Cerekwicki_Cezary_v01.sql 7629655,6 6561,2 598566,4 409,6 18 18 22 22 80
22 Kulczynski_Przemyslaw_v04.sql 11873518,2 10476 152408 41,8 21 21 20 21 83
23 Waluszko_Bartlomiej_v02.sql - - - - - - - - -
24 Jacewicz_Lukasz_v02.sql - - - - - - - - -

Test “100”

Lp Rozwiązanie D C R W RD RC RR RW T
1 Gniadkowski_Leszek_v15.sql 169068,6 153,4 112 0 1 1 1 1 4
2 Gniadkowski_Leszek_v10.sql 192850,2 158,8 145 0 2 2 2 1 7
3 Gniadkowski_Leszek_v12.sql 202850,6 165,4 145 0 3 3 2 1 9
4 Nowakowski_Marcin_v04.sql 364540 316,8 277 0 4 4 4 1 13
5 Poniatowski_Aleksander_v01.sql 548570,2 446,6 343 0 7 6 7 1 21
6 Sliwa_Krzysiek_v02.sql 497087,6 456,4 409 0 6 7 8 1 22
7 Przeliorz_Tomek_v02.sql 411842,6 368,2 3165 0 5 5 14 1 25
8 Pater_Rafal_v03.sql 1078200,8 928,2 324 0 11 11 5 1 28
9 Gailard_Pawel_v02.sql 834646,8 767,4 673 0 9 9 11 1 30
9 Pater_Rafal_v04.sql 1123663,2 949,6 324 0 12 12 5 1 30
9 Zmuda_Katarzyna_v01.sql 909931 813,6 423 0 10 10 9 1 30
12 Nowakowski_Marcin_v03.sql 804165 530,4 464,2 0,2 8 8 10 13 39
13 Grabowska_Katarzyna_v04.sql 3773755 3151,2 734,4 0 16 16 12 1 45
14 Pakulski_Maciej_v03.sql 1716357,2 1425,4 736,8 0,6 13 13 13 14 53
15 Powichrowski_Marek_v12.sql 1990753 1704,4 23022 32 14 15 15 15 59
16 Powichrowski_Marek_v13.sql 2326372,4 1690,6 23814,6 32 15 14 16 15 60
17 Przeliorz_Tomek_v01.sql 12467972,4 11131,4 266163 78 17 17 18 18 70
18 Sowa_Piotr_v10.sql 27219956 25120,2 304784 411,8 18 18 19 20 75
19 Pakulski_Maciej_v02.sql 72426161,8 68060,4 522223 52 20 20 21 17 78
20 Kulczynski_Przemyslaw_v01.sql 73221107 68773,2 239710,4 308,2 22 22 17 19 80
21 Cerekwicki_Cezary_v01.sql 40562959,2 37802,4 2496637,8 3024 19 19 22 22 82
22 Kulczynski_Przemyslaw_v04.sql 73103680,2 68496,4 322134,4 458,6 21 21 20 21 83
23 Waluszko_Bartlomiej_v02.sql - - - - - - - - -
24 Jacewicz_Lukasz_v02.sql - - - - - - - - -

Dla każdego rozwiązania przy każdym teście uzyskałem sumę pozycji w poszczególnych kryteriach (ostatnia kolumna wg której ustalałem kolejność rozwiązań w powyższych tabelach wynikowych dla poszczególnych testów). Do uzyskania pozycji w końcowej tabeli wynikowej zsumowałem te wartości ze wszystkich trzech testów i tak wyszła mi tabela końcowa konkursu:

Lp Rozwiązanie T20 T50 T100 Total
1 Gniadkowski_Leszek_v15.sql 4 4 4 12
2 Gniadkowski_Leszek_v10.sql 7 7 7 21
3 Gniadkowski_Leszek_v12.sql 9 9 9 27
4 Nowakowski_Marcin_v04.sql 18 13 13 44
5 Sliwa_Krzysiek_v02.sql 30 19 22 71
6 Przeliorz_Tomek_v02.sql 24 24 25 73
7 Poniatowski_Aleksander_v01.sql 33 20 21 74
8 Zmuda_Katarzyna_v01.sql 25 26 30 81
9 Pater_Rafal_v03.sql 31 30 28 89
10 Pater_Rafal_v04.sql 34 32 30 96
11 Nowakowski_Marcin_v03.sql 30 31 39 100
12 Gailard_Pawel_v02.sql 44 32 30 106
13 Powichrowski_Marek_v12.sql 19 41 59 119
14 Powichrowski_Marek_v13.sql 37 44 60 141
15 Grabowska_Katarzyna_v04.sql 56 41 45 142
16 Pakulski_Maciej_v03.sql 48 55 53 156
17 Przeliorz_Tomek_v01.sql 38 50 70 158
18 Pakulski_Maciej_v02.sql 60 66 78 204
19 Sowa_Piotr_v10.sql 64 72 75 211
20 Kulczynski_Przemyslaw_v01.sql 55 78 80 213
21 Kulczynski_Przemyslaw_v04.sql 51 83 83 217
22 Cerekwicki_Cezary_v01.sql 83 80 82 245
23 Waluszko_Bartlomiej_v02.sql - - - -
24 Jacewicz_Lukasz_v02.sql - - - -

Legenda:

T20 – suma miejsc z testu “20”
T50 – suma miejsc z testu “50”
T100 – suma miejsc z testu “100”
Total – suma miejsc ze wszystkich testów

A zatem… Jednogłośnym zwycięzcą konkursu ogłaszam Leszka Gniadkowskiego. Drugie miejsce wśród uczestników (choć Leszek zdominował konkurs totalnie) zajął Marcin Nowakowski, a trzecie – Krzysztof Śliwa. Postanowiłem też nagrodzić miejsca 4. i 5. – a więc Tomka Przeliorza oraz Aleksandra Poniatowskiego. Ze zwycięzcami będę kontaktował się indywidualnie drogą mailową.

Dziękuję wszystkim, którzy wzięli udział w moim konkursie. Wszyscy, którzy zakwalifikowali się do finału oprócz słów uznania otrzymają ode mnie mały prezent (szczegóły niebawem w mailu). Szczególnie gratuluję zwycięzcom. Dobra SQL-owa robota :-)

Tym samym konkurs uznaję za zamknięty. Dla wytrwałych i ciekawskich publikuję pełen zestaw rozwiązań finałowych w pliku ZIP.

Pobierz wszystkie rozwiązania finałowe (ZIP, 21 KB)

[PL] SQL Server – Debugowanie z użyciem RAISERROR

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

Ostatnio na forum portalu CodeGuru.pl brałem udział w dyskusji na temat metod prostego debugowania kodu T-SQL z wykorzystaniem polecenia PRINT. W wątku użytkownik szogun.krepa podał metodę obejścia problemu z poleceniem PRINT, które wysyła komunikat informacyjny (w przypadku aplikacji SQL Server Management Studio komunikat ten pojawia się w zakładce Messages) z opóźnieniami wynikającymi z długich czasów wykonania poleceń znajdujących się w tym samym wsadzie T-SQL.

Czy jest zatem PRINT działający bez opóźnień? Tak, takim odpowiednikiem jest odpowiednio wywołane polecenie RAISERROR. Odpowiednio to znaczy – z poziomem błędu ustawionym na 10 (poziomy od 1 do 9 też oznaczają informację, ale oprócz komunikatu pojawia się nagłówek błędu i już nie jest to działanie analogiczne z PRINT) oraz z przełącznikiem WITH NOWAIT. Porównajmy dwa wsady:

PRINT 'test 1';
WAITFOR DELAY '00:00:05';
GO
RAISERROR('test 2',10,1) WITH NOWAIT;
WAITFOR DELAY '00:00:05';
GO

W pierwszym przypadku komunikat “test 1” pojawia się po 5 sekundach i nie jest to efekt pożądany. W drugim przypadku komunikat “test 2” pojawia się natychmiast.

Ok, wszystko pięknie. Czy są zatem jakiekolwiek problemy, które możemy napotkać używając RAISERROR(…, 10, …) WITH NOWAIT? Zobaczmy taki kod:

DECLARE @zmienna varchar(10);
SET @zmienna = 'jakiś błąd';
RAISERROR('Komunikat: ' + @zmienna,10,1) WITH NOWAIT;
WAITFOR DELAY '00:00:05';
GO

-- Msg 102, Level 15, State 1, Line 3
-- Incorrect syntax near '+'.
-- Msg 319, Level 15, State 1, Line 3
-- Incorrect syntax near the keyword 'with'. If this statement is a common table
-- expression, an xmlnamespaces clause or a change tracking context clause,
-- the previous statement must be terminated with a semicolon.

Tak, w RAISERROR nie można generować komunikatu błędu wyrażeniem (albo literał, albo zmienna, ale nie wyrażenie). A zatem, zamiast powyższego można wykorzystać jeden z poniższych wariantów:

DECLARE
  @zmienna varchar(10),
  @komunikat varchar(255);
SET @zmienna = 'jakiś błąd';
SET @komunikat = 'Komunikat: ' + @zmienna;
RAISERROR(@komunikat,10,1) WITH NOWAIT;
WAITFOR DELAY '00:00:05';
GO

DECLARE @zmienna varchar(10);
SET @zmienna = 'jakiś błąd';
RAISERROR('Komunikat: %s',10,1, @zmienna) WITH NOWAIT;
WAITFOR DELAY '00:00:05';
GO

W pierwszym przypadku budujemy komunikat i zapisujemy go do jednej zmiennej (@komunikat), którą podstawiamy w wywołaniu polecenia RAISERROR jako pierwszy parametr.

W drugim przypadku korzystamy z parametryzowanego komunikatu (%s oznacza parametr tekstowy, wartości parametru pojawiają się kolejno jako czwarty i następne parametry polecenia RAISERRROR; więcej o parametryzowaniu RAISERROR do poczytania w Books Online).

Jak widać RAISERROR nadaje się do takiego natychmiastowego zwracania komunikatów, ale pod warunkiem, że będziemy pamiętać o wspomnianym powyżej ograniczeniu.

[PL] Konkurs T-SQL – Lista finalistów

VN:F [1.7.9_1023]
Rating: 5.0/5 (2 votes cast)

Parę minut temu zamknąłem listę finalistów konkursu T-SQL “Liga piłkarska”. Do finału dostały się poniższe rozwiązania:

Cerekwicki_Cezary_v01.sql
Gailard_Pawel_v02.sql
Gniadkowski_Leszek_v10.sql
Gniadkowski_Leszek_v12.sql
Gniadkowski_Leszek_v15.sql
Grabowska_Katarzyna_v04.sql
Jacewicz_Lukasz_v02.sql
Kulczynski_Przemyslaw_v01.sql
Kulczynski_Przemyslaw_v04.sql
Nowakowski_Marcin_v03.sql
Nowakowski_Marcin_v04.sql
Pakulski_Maciej_v02.sql
Pakulski_Maciej_v03.sql
Pater_Rafal_v03.sql
Pater_Rafal_v04.sql
Poniatowski_Aleksander_v01.sql
Powichrowski_Marek_v12.sql
Powichrowski_Marek_v13.sql
Przeliorz_Tomek_v01.sql
Przeliorz_Tomek_v02.sql
Sliwa_Krzysiek_v02.sql
Sowa_Piotr_v10.sql
Waluszko_Bartlomiej_v02.sql
Zmuda_Katarzyna_v01.sql

Gratuluję finalistom i dziękuję wszystkim osobom, które wzięły udział w konkursie. Najdalej w środę wieczór powinny ukazać się wyniki konkursu, o czym poinformuję też uczestników drogą mailową.

[PL] Konkurs T-SQL – liga piłkarska

VN:F [1.7.9_1023]
Rating: 5.0/5 (6 votes cast)

image

Obiecałem, że jak tylko będę miał możliwość, zorganizuję konkurs T-SQL inspirowany cyklicznymi konkursami T-SQL Challenge. I słowa dotrzymuję :-)

Dane wejściowe

Dane są dwie tabele o strukturach, jak poniżej:

Tabela dbo.Teams:

TeamId      TeamName
----------- --------------------
1           Poland
2           San Marino
3           Ivory Coast
4           Luxemburg

Tabela dbo.Games:

HomeId      VisitorId   HomeGoals   VisitorGoals
----------- ----------- ----------- ------------
1           2           0           0
1           3           2           0
1           4           1           0
2           1           1           1
2           3           1           1
2           4           2           1
3           1           2           0
3           2           1           0
3           4           0           1
4           1           0           1
4           2           0           0
4           3           2           0

Tabela dbo.Teams zawiera identyfikatory (TeamId) oraz nazwy (TeamName) drużyn piłkarskich rywalizujących ze sobą na zasadzie klasycznej ligi (każdy z każdym po dwa mecze – mecz u siebie i mecz na wyjeździe). Tabela dbo.Games zawiera wyniki meczów – identyfikator drużyny gospodarzy (HomeId), identyfikator drużyny gości (VisitorId), liczbę bramek strzelonych przez gospodarzy (HomeGoals) oraz liczbę bramek strzelonych przez gości (VisitorGoals).

Opis zadania

  1. Wynikiem działania zapytania stworzonego w ramach konkursu ma być tabela wyniku rywalizacji drużyn zawierająca kolumny:- Position – pozycja drużyny w tabeli – nie ma miejsc ex-aequo,- Team – nazwa drużyny,- P – liczba rozegranych przez drużynę spotkań,- W – liczba zwycięstw drużyny,- D – liczba remisów drużyny,- L – liczba porażek drużyny,- F – liczba bramek zdobytych przez drużynę,- A – liczba bramek straconych przez drużynę,- Points – liczba punktów zdobytych przez drużynę zgodnie z regułą: 3 punkty za zwycięstwo, 1 punkt za remis oraz 0 punktów za porażkę.
  2. O kolejności drużyn w tabeli decydują kolejno:- liczba punktów – im więcej, tym lepiej,- różnica między bramkami strzelonymi a straconymi – im większa, tym lepiej,- liczba bramek strzelonych – im większa, tym lepiej,- liczba bramek straconych – im mniejsza, tym lepiej,- tabela wyników bezpośrednich spotkań – jeśli dwie lub więcej drużyn mają taką samą pozycję w oparciu o powyższe kryteria, o kolejności decyduje tabela wyników zbudowana wyłącznie o wyniki bezpośredniej rywalizacji między zainteresowanymi drużynami (i stosujemy w tej tabeli takie same kryteria, jak powyżej – czyli zaczynamy od punktów, potem bramki etc.), w tabeli tej bramki strzelone na wyjazdach liczymy podwójnie (ale do liczenia punktów oczywiście bramki liczymy normalnie, bez podwajania tych strzelonych na wyjazdach),- jeżeli dwie lub więcej drużyn po zastosowaniu powyższych kryteriów kwalifikują się do zajęcia tego samego miejsca, o kolejności decyduje TeamId – zespół o mniejszym TeamId w tabeli znajdzie się przed zespołem o większym TeamId (załóżmy, że TeamId to miejsce w rankingu federacji piłkarskiej).

Oczekiwany wynik

Position Team        P W D L F A Points
-------- ----------- - - - - - - ------
1        Poland      6 3 2 1 5 3 11
2        San Marino  6 1 4 1 4 4 7
3        Luxemburg   6 2 1 3 4 4 7
4        Ivory Coast 6 2 1 3 4 6 7

Skypt generujący przykładowe dane

Skrypt do wygenerowania przykładowych danych:

IF OBJECT_ID(N'dbo.Games', 'U') IS NOT NULL
  DROP TABLE dbo.Games;
IF OBJECT_ID(N'dbo.Teams', 'U') IS NOT NULL
  DROP TABLE dbo.Teams;
CREATE TABLE dbo.Teams (
  TeamId int NOT NULL,
  TeamName varchar(20) NOT NULL
);
CREATE TABLE dbo.Games (
  HomeId int NOT NULL,
  VisitorId int NOT NULL,
  HomeGoals int NOT NULL,
  VisitorGoals int NOT NULL
);
INSERT INTO dbo.Teams (TeamId, TeamName)
VALUES (1, 'Poland');
INSERT INTO dbo.Teams (TeamId, TeamName)
VALUES (2, 'San Marino');
INSERT INTO dbo.Teams (TeamId, TeamName)
VALUES (3, 'Ivory Coast');
INSERT INTO dbo.Teams (TeamId, TeamName)
VALUES (4, 'Luxemburg');

INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (1, 2, 0, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (1, 3, 2, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (1, 4, 1, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (2, 1, 1, 1);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (2, 3, 1, 1);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (2, 4, 2, 1);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (3, 1, 2, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (3, 2, 1, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (3, 4, 0, 1);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (4, 1, 0, 1);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (4, 2, 0, 0);
INSERT INTO dbo.Games (HomeId, VisitorId, HomeGoals, VisitorGoals)
VALUES (4, 3, 2, 0);

Zasady konkursu

  1. Rozwiązaniem ma być pojedyncze zapytanie T-SQL rozpoczynające się od słowa “SELECT”, “WITH” lub “;WITH”.
  2. W zapytaniu można używać podzapytań i wszystkich elementów języka T-SQL dostępnych w SQL Server 2008 R2.
  3. W zapytaniu można użyć tabeli liczb, której definicja jest podana tutaj. W skrypcie nie umieszczaj definicji tabeli liczb (załóż, że będzie ona dostępna w bazie testowej).
  4. Zapytania, które przejdą próbę poprawności zwracanego wyniku (testy będą prowadzone na nieco większej ilości danych), będą klasyfikowane wg następujących kryteriów: czas wykonania (duration), zużycie procesora (CPU), ilość odczytów (reads) oraz ilość zapisów (writes). Zwycięży rozwiązanie, które łącznie zdobędzie najwięcej punktów we wszystkich czterech kryteriach.
  5. Rozwiązanie (możesz ich wysłać więcej niż jedno) wyślij mailem w postaci skryptu o rozszerzeniu .sql na adres: pawel.potasinski[małpa]sqlpass.org. W tytule maila wpisz “Konkurs T-SQL”. Otrzymasz ode mnie potwierdzenie (e-mail), jeżeli otrzymam od Ciebie rozwiązanie.
  6. W razie pytań lub uwag użyj podanego powyżej adresu e-mail lub dodaj komentarz do tego wpisu.
  7. Na rozwiązania czekam do 3 października 2010 do końca dnia. Ogłoszenie wyników przewiduję najdalej tydzień później.
  8. Konkurs w całości organizuję sam (sam wymyśliłem zadanie, sam będę testował rozwiązania, sam przyznawał nagrody etc.), a co za tym idzieTwoje ewentualne dane osobowe oglądam wyłącznie ja – Paweł Potasiński (konkurs nie ma sponsorów).

Nagrody

Nagrody to prawdopodobnie najmniej istotna część konkursu, ale chyba dobrze wiedzieć, o co walczysz :-)

1. miejsce – subskrypcja MSDN + dysk przenośny 2,5 cala o pojemności 500 GB

2. miejsce – subskrypcja MSDN

3. miejsce – książka pt. “Serwer SQL 2008. Usługi biznesowe. Analiza i eksploracja danych” z autografem Marcina Szeligi

PS. Zastrzegam sobie prawo do zmiany zestawu nagród bez uprzedzenia – np. w sytuacji, gdy uznam, że należy wyróżnić więcej niż 3 uczestników konursu :-)

Jest o co walczyć? Myślę, że tak! A i tak nie ma to jak satysfakcja z samego działającego rozwiązania! ;-)

Powodzenia i czekam na Wasze rozwiązania!

[EDYCJA: 2010-09-20]

Na prośbę uczestników i osób postronnych załączam bardziej rzeczywiste dane testowe: Primera Division 2009/2010. Visca el Barca!

[/EDYCJA]

[EDYCJA: 2010-09-21]

Udostępniam zestaw danych, który powinien pomóc Wam w przetestowaniu Waszych rozwiązań pod kątem poprawności algorytmu sortowania tabeli wynikowej. Grupa śmierci normalnie :-)

Pobierz skrypt “Tricky data”

[/EDYCJA]

[EDYCJA: 2010-09-22]

Zmyłka ;-) Dzisiejszy alarm podniesiony przeze mnie po interwencji Marka Powichrowskiego (ech, mąciwoda!) okazał się fałszywy. Oczekiwany wynik we wpisie jest OK. W tym miejscu dziękuję Leszkowi Gniadkowskiemu za to, że nie dał się zasugerować ani Markowi, ani mnie, i podjął własne śledztwo :-) Walczymy dalej! Póki co zanotowałem ledwie 4 uczestników. Czyżby wszyscy czytelnicy mojego bloga mieli już własne subskrypcje MSDN i przenośne dyski 500 GB??? ;-)

[/EDYCJA]


[EDYCJA: 2010-09-26 {BASIC TESTING}]

Przeprowadziłem  test basic na wszystkich rozwiązaniach, które dotąd otrzymałem. Poniżej rozwiązania, które przeszły test, a zarazem zwróciły poprawne wyniki dla kilku prostych zestawów danych (darujcie brak polskich liter w nazwiskach):

Gniadkowski_Leszek_v05.sql
Gniadkowski_Leszek_v06.sql
Gniadkowski_Leszek_v07.sql
Gniadkowski_Leszek_v09.sql
Gniadkowski_Leszek_v10.sql
Gniadkowski_Leszek_v12.sql
Grabowska_Katarzyna_v02.sql
Jacewicz_Lukasz_v01.sql
Pakulski_Maciej_v01.sql
Poniatowski_Aleksander_v01.sql
Powichrowski_Marek_v07.sql
Powichrowski_Marek_v10.sql
Przeliorz_Tomek_v01.sql
Waluszko_Bartlomiej_v01.sql

Każdy pewnie wie, ile wersji wysyłał, więc nie powinniście mieć problemów z odnalezieniem tych właściwych :-) Gratuluję tym sześciu osobom, którym się udało, a tych, którzy nie dali rady lub jeszcze nie nadesłali swoich rozwiązań (?), zachęcam do działania. Czasu jeszcze jest sporo ;-) Wszelkich odpowiedzi na Wasze pytania udzielam indywidualnie na maila. Test na “tricky data” już niebawem. Stay tuned!

[/EDYCJA]

[EDYCJA: 2010-09-28 {BASIC TESTING UPDATE}]

Poniżej lista rozwiązań, które pomyślnie przeszły basic testing do dzisiaj (28.09):

Cerekwicki_Cezary_v01.sql
Gniadkowski_Leszek_v05.sql
Gniadkowski_Leszek_v06.sql
Gniadkowski_Leszek_v07.sql
Gniadkowski_Leszek_v09.sql
Gniadkowski_Leszek_v10.sql
Gniadkowski_Leszek_v12.sql
Gniadkowski_Leszek_v13.sql
Grabowska_Katarzyna_v02.sql
Jacewicz_Lukasz_v01.sql
Nowakowski_Marcin_v01.sql
Pakulski_Maciej_v01.sql
Pater_Rafal_v03.sql
Poniatowski_Aleksander_v01.sql
Powichrowski_Marek_v07.sql
Powichrowski_Marek_v10.sql
Powichrowski_Marek_v11.sql
Przeliorz_Tomek_v01.sql
Rozycki_Grzegorz_v01.sql
Sowa_Piotr_v05.sql
Sowa_Piotr_v06.sql
Waluszko_Bartlomiej_v01.sql

[/EDYCJA]


[EDYCJA: 2010-09-29 {TRICKY DATA}]

Panie i Panowie, zaczynamy tricky data testing! Poniżej oddaję w Wasze ręce kolejny zestaw spreparowanych wyników. Tym razem ex-aequo jest sporo i trzeba mieć w kodzie odpowiedzi na wiele pułapek. Sprawdziłem nadesłane do dziś rozwiązania i z niekrytą satysfakcją oznajmiam, że 17 rozwiązań stworzonych przez 9 zawodników spełniło rygorystyczne wymogi tego testu :-) Poniżej lista szczęśliwców. Niewykluczone, że wymyślę jeszcze jakiś zestaw testowy, by dokładnie sprawdzić, czy mnie nie nabieracie w swoich “dziełach sztuki SQL-owej” ;-)

Cerekwicki_Cezary_v01.sql
Gniadkowski_Leszek_v07.sql
Gniadkowski_Leszek_v09.sql
Gniadkowski_Leszek_v10.sql
Gniadkowski_Leszek_v12.sql
Gniadkowski_Leszek_v13.sql
Gniadkowski_Leszek_v15.sql
Grabowska_Katarzyna_v02.sql
Nowakowski_Marcin_v01.sql
Pater_Rafal_v03.sql
Powichrowski_Marek_v07.sql
Powichrowski_Marek_v10.sql
Powichrowski_Marek_v11.sql
Przeliorz_Tomek_v01.sql
Sowa_Piotr_v05.sql
Sowa_Piotr_v06.sql
Waluszko_Bartlomiej_v01.sql

*** Pobierz zestaw danych “tricky data” ***

[/EDYCJA]

[EDYCJA: 2010-09-29 {TRICKY DATA} ]

WAŻNE!!! W tabeli dbo.Teams mogą znaleźć się drużyny, które nie rozegrały żadnego spotkania (ich identyfikatory nie występują w dbo.Games). W takiej sytuacji drużyny te mają pojawić się w tabeli wynikowej z zerami w odpowiednich kolumnach i z zachowaniem kolejności zgodnie z podanym algorytmem (czyli po prostu decyduje ranking). A jeśli takich drużyn bez rozegranego meczu jest więcej niż jedna, o ich kolejności decyduje pozycja w rankingu (TeamId).

Na razie żadne (!) z nadesłanych przez Was rozwiązań nie spełnia tej reguły, więc proszę o szybkie poprawienie kodu!

Zmieniłem w związku z tym ostatnio zamieszczony zestaw TRICKY DATA.

Poniżej będę publikował aktualną listę rozwiązań, które pomyślnie przeszły basic + tricky data test.

Cerekwicki_Cezary_v01.sql
Gailard_Pawel_v02.sql
Gniadkowski_Leszek_v10.sql
Gniadkowski_Leszek_v12.sql
Gniadkowski_Leszek_v15.sql
Grabowska_Katarzyna_v04.sql
Jacewicz_Lukasz_v02.sql
Kulczynski_Przemyslaw_v01.sql
Kulczynski_Przemyslaw_v04.sql
Nowakowski_Marcin_v03.sql
Nowakowski_Marcin_v04.sql
Pakulski_Maciej_v02.sql
Pakulski_Maciej_v03.sql
Pater_Rafal_v03.sql
Pater_Rafal_v04.sql
Poniatowski_Aleksander_v01.sql
Powichrowski_Marek_v12.sql
Powichrowski_Marek_v13.sql
Przeliorz_Tomek_v01.sql
Przeliorz_Tomek_v02.sql
Sliwa_Krzysiek_v02.sql
Sowa_Piotr_v10.sql
Waluszko_Bartlomiej_v02.sql
Zmuda_Katarzyna_v01.sql

[/EDYCJA]

[PL] Skryptowanie w SQL Server 2008 – Więzy DEFAULT i CHECK

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

Intro

Kontynuuję serię poświęconą skryptowaniu obiektów w SQL Server 2008. Zapoznaj się także z poprzednimi wpisami z tej serii:

Dziś pokażę, jak można skryptować więzy wartości domyślnych (DEFAULT) i ograniczeń wartości (CHECK). Dla przypomnienia – więzy DEFAULT służą do narzucania kolumnom wartości domyślnych (gdy użytkownik nie wstawia jawnie danych w kolumnę, system wstawia wartość domyślną), zaś więzy CHECK służą do narzucenia ograniczeń wartości, jakie mogą przyjmować dane w kolumnie (stąd więzy te służą m.in. do tworzenia odpowiedników enumeracji w kolumnach w SQL Server).

Skryptowanie więzów DEFAULT

Zadanie: zeskryptować wszystkie więzy DEFAULT w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
SELECT
   'IF OBJECT_ID(N''' +
    REPLACE(
      QUOTENAME(SCHEMA_NAME(d.[schema_id])) + '.' + QUOTENAME(d.name),
      '''', ''''''
    ) + ''', N''D'') IS NULL
  ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(d.parent_object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(d.parent_object_id)) + '
  ADD CONSTRAINT ' + QUOTENAME(d.name) + '
  DEFAULT ' + d.definition + ' FOR ' + QUOTENAME(c.name) + ';
GO'
FROM sys.default_constraints AS d
INNER JOIN sys.columns AS c
ON d.parent_object_id = c.[object_id]
AND d.parent_column_id = c.column_id
WHERE is_ms_shipped = 0;
GO

Fragment wyniku:

image

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

Komentarz do kodu:

  • metadane więzów DEFAULT zwraca widok systemowy sys.default_constraints,
  • nazwy kolumn zwraca widok systemowy sys.columns (jest też widok sys.all_columns, który zawiera także kolumny obiektów systemowych),
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi,
  • skryptuję tylko obiekty, które zostały stworzone przez użytkowników (filtr na kolumnie is_ms_shipped).

Skryptowanie więzów CHECK

Zadanie: zeskryptować wszystkie więzy CHECK w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
SELECT
  'IF OBJECT_ID(N''' +
    REPLACE(
      QUOTENAME(SCHEMA_NAME([schema_id])) + '.' + QUOTENAME(name),
      '''', ''''''
    ) + ''', N''C'') IS NULL
  ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' +
  QUOTENAME(OBJECT_NAME(parent_object_id)) + '
  ADD CONSTRAINT ' + QUOTENAME(name) + '
  CHECK ' + CASE is_not_for_replication WHEN 1 THEN 'NOT FOR REPLICATION ' ELSE '' END +
  definition + ';
GO'
FROM sys.check_constraints
WHERE is_ms_shipped = 0;
GO

Fragment wyniku:

image

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

Komentarz do kodu:

  • metadane więzów DEFAULT zwraca widok systemowy sys.check_constraints,
  • funkcja QUOTENAME jak zwykle służy mi do otaczania identyfikatorów nawiasami kwadratowymi,
  • skryptuję tylko obiekty, które zostały stworzone przez użytkowników (filtr na kolumnie is_ms_shipped).

Podsumowanie

Skryptowanie więzów DEFAULT i CHECK jest moim zdaniem stosunkowo proste. Definicje wyrażeń determinujących działanie tych więzów są zapisane w pojedynczych kolumnach. W zasadzie sprawa sprowadza się do zbudowania odpowiedniej składni T-SQL, a to jest bodaj najprostsze. Należy pamiętać, że przed utworzeniem więzów DEFAULT i CHECK musimy zadbać, by w bazie danych istniały wszystkie funkcje skalarne, jakie owe więzy wykorzystują.

[PL] Wywiad – nasi na TSQL Challenges

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

image Jakiś czas temu wysłałem rozwiązanie jednej z zagadek Itzika Ben-Gana z serii “T-SQL Puzzle” (zagadki były publikowane na witrynie SQL Server Magazine). Wtedy pomyślałem, że byłoby fajnie, gdyby takie zagadki / konkursy, w których uczestnicy mogliby poćwiczyć szare komórki i rozwijać umiejętności programowania w T-SQL, były organizowane częściej. I oto, za jakiś czas trafiłem na witrynę ByeondRelational.com prowadzoną przez Jacoba Sebastiana (SQL Server MVP) i kilku innych SQL-owych zapaleńców.

Jednym z działów BeyondRelational.com (oprócz licznych blogów i forum) jest dział TSQL Challenges. Czym są TSQL Challenges (dalej używam skrótu TC)? Są to konkursy polegające na rozwiązywaniu problemów za pomocą zapytań w języku T-SQL (nowe zadanie jest ogłaszane raz na dwa tygodnie). Udział może wziąć każdy, kto ma na to ochotę (i zarejestruje się na portalu BeyondRelational.com). Zazwyczaj zadania wymagają rozwiązania postawionego problemu jednym zapytaniem SELECT (choć na ogół dopuszczalne jest używanie podzapytań i CTE), bez użycia tabel tymczasowych i dynamicznego kodu T-SQL. Zwycięża ten uczestnik, który podeśle rozwiązanie generujące poprawny wynik, spełniające podane wymagania i o najlepszych statystykach (liczy się nie tylko czas wykonania, ale także zużycie CPU oraz ilość odczytów i zapisów). Więcej o samych TSQL Challenges i o możliwości nadsyłania własnych propozycji zadań konkursowych czytaj tu: Submit a TSQL Challenge Idea.

Zabawa jest przednia. Sam spróbowałem. Wysłałem nawet ze trzy rozwiązania zadań, ale z uwagi na to, że jak dotąd nie dałem się wciągnąć na dobre w zabawę (co by to było, jakbym jeszcze w TC dał się wciągnąć :-P), moje próby kończą się w przedbiegach po wysłaniu pierwszego rozwiązania, które uznam za “good enough” (a to jest zdecydowanie za mało na takich zawodników, jacy wysyłają swoje rozwiązania na TC) :-) Co lepsi zawodnicy nadsyłają po kilka rozwiązań wymyślając takie rozwiązania, z których można się uczyć wszystkiego, co potrzebne programiście SQL Servera: od algorytmów, przez optymalizację, aż po wymyślne chwyty do zastosowania w kodzie T-SQL. Co ciekawe, jest też wersja “light” konkursu przeznaczona dla początkujących adeptów sztuki programowania w T-SQL – TSQL Beginners Challenges.

Wśród najlepszych uczestników zabawy w “dorosłe” TC znaleźć można kilku Polaków. Dwóch z nich zaprosiłem do rozmowy o TC. Zgodzili się (dzięki, chłopaki!), a zapis wywiadu znajdziesz poniżej.

Moimi rozmówcami są Leszek Gniadkowski (w wywiadzie jako LG) oraz Marek Powichrowski (w wywiadzie jako MP).

Leszek zawodowo pracuje jako członek zespołu administrującego dosyć dużą domeną Active Directory. Specjalizuje się w systemach opartych na MS Windows, choć ma do czynienia również z innymi OS przy okazji integracji AD z usługami opartymi na *NIX/Linux.

Marek zawodowo pracuje jako programista i konsultant systemów ERP. Specjalizuje się w projektowaniu baz danych, głównie SQL Server. Pisze aplikacje w .NET, nie unika ASP.NET. Lubi projektować sałatki śledziowe i piec chleb na własnym zakwasie.

PP: Witajcie. Na początek gratulacje na okazję Waszych sukcesów w konkursach TSQL Challenges!

LG: Witam, dziękujemy, szczęśliwie trwa passa (przyp. PP – Leszek jest ex aequo pierwszy w rankingu TC!) , największym sukcesem jest jednak sama wiedza nabyta przy udziale w TC.

MP: Dzięki, to miłe. Na razie mam 13 SQL Stars i mogę już sobie zrobić T-Shirt’a z logo „TSQL Challenges Winner” :-) A i jeszcze mogę sobie wydrukować certyfikat potwierdzający ten fakt. To takie drobiazgi, ale miłe.

PP: Skąd dowiedzieliście się o konkursach TSQL Challenges i witrynie beyondrelational.com?

LG: W moim przypadku z forum portalu WSS. Dokładniej stąd. Od czasu umieszczenia tego wątku (trwał wtedy TC20), rozpocząłem naukę TSQL i próbowałem sił w następnych TC.

MP: W moim przypadku było dokładnie tak samo. Tamto zadanie zelektryzowało całe forum SQL Server na WSS.pl. Interesujące było to, że trzeba to rozwiązać w jednym zapytaniu. Pamiętam, że podobny problem rzucił kiedyś Maciej Pilecki. Piękne wyzwanie. Wysłałem swoje rozwiązanie i niejakim szokiem było to, że trzeba długo czekać na wyniki rywalizacji. Ale przeszedłem wszystkie fazy (basic test, tricky data i load test) i ostatecznie byłem sklasyfikowany poza pierwszą 10-tką, ale jak na początek to i tak było coś. Potem była przerwa i Leszek odezwał się do mnie przy zadaniu TC28, które miało bardzo mało zgłoszonych rozwiązań. Mi poszło gładko i przeszło dwie pierwsze fazy testów i kiedy już byłem w „ogródku i już witałem się z gąską” przepadłem w load test. To sparzenie się nakazuje mi teraz przyglądać się baczniej wydajności. Ale co ciekawe, Leszek który wygrał tę odsłonę był tym kompletnie zaskoczony bo wstępne porównania statystyk na naszych danych testowych dawały mi przewagę. Ale dane do load test odwróciły tę relację. Także emocje są do samego końca. Od zadania TC28 wciągnąłem się. Wykonałem jeszcze dwa zadania w tył i byłem wśród zwycięzców i od tej pory nie odpuszczam żadnego zadania.

PP: Co takiego jest w TSQL Challenges, że poświęcacie czas na podejmowanie kolejnych „wyzwań”?

MP: To głód tego pięknego momentu olśnienia, gdy znajduje się rozwiązanie proste, z pięknymi statystykami. To głód tych endorfin, które zalewają człowieka powodując, że wszystko wokoło nagle staje się piękne i nic nie jest w stanie tego zakłócić. A poza tym to świetne zajęcie dla umysłu aby przypadkiem nie zachciałoby mu się spuścić z tonu.

LG: Dobra rozrywka umysłowa, sprawdzanie się, rywalizacja. TC w pewnym stopniu uzależniają. Mix samego języka TSQL i zakręconych zadań to mieszanka gwarantująca intelektualną ucztę. Co do samego czasu, jego większość nie jest spędzona bezpośrednio przy komputerze. Nad zadaniem często myślę przy okazji, w czasie wykonywania dowolnej czynności: jazdy samochodem, posiłku, przed snem itd. Samo kodowanie zajmuje już zwykle mniej czasu.

PP: Jak dużo czasu poświęcacie średnio na stworzenie rozwiązania gotowego do zgłoszenia w konkursie i czy zdarza się, że wysyłacie więcej niż jedno rozwiązanie?

LG: Różnie, od 30 minut do dwóch tygodni na stworzenie rozwiązania, które „działa”. Najczęściej parę godzin. Później pracuje się nad ulepszeniem bądź innym podejściem do problemu podkręcającym statystyki. Zdarzało się, że były TC, do których z braku czasu podchodziłem w ostatni dzień przed terminem zakończenia, poświęcając niedzielę. Czasami wysyłam jedną wersję rozwiązania, czasami dwie i więcej. Natomiast prawie zawsze wysyłam poprawione rozwiązania spamując trochę upload.

MP: Różnie z tym bywa. Niektóre pozornie trudne rozwiązują się błyskawicznie w kilku wersjach. Inne kosztują sporo czasu. Jeżeli mam kilka rozwiązań to oczywiście je wysyłam. Trzeba pamiętać, że każde rozwiązanie zadania przechodzi przez trzy fazy weryfikacji: basic test – czyli weryfikacja rozwiązania danymi podanymi przy zadaniu, tricky data – weryfikacja danymi „pokręconymi” w celu testowania odporności rozwiązania na zakłócenia no i na koniec load test, który może wyeliminować z gry. Więc każda dodatkowa wersja rozwiązania to szansa na „przyżycie” w turnieju.

PP: Który TSQLChallenge z dotychczasowych najbardziej przypadł Wam do gustu i dlaczego?

MP: Bezwzględnie TC37! To zadanie najbardziej mnie zmęczyło ale też dało najwięcej radości gdy się wykluło wreszcie z poszukiwań mego umysłu. W samo rozwiązanie mogę się gapić i podziwiać jakie sprytne i szybkie rzeczy można robić w T-SQL-u (nawet bez użycia indeksów). Mam porównanie bo moja pierwsza wersja rozwiązania tego zadania była tragiczna pod względem wydajnościowym i to o kilka rzędów wielkości. Obserwując rozwiązania konkursowe widać, jaki power ma T-SQL, jak bardzo elastycznym jest językiem.

LG: Trudno wybrać, ale wybrałbym TC28 (przyp. PP – Leszek wygrał ten TC!). Za kompletną pustkę w głowie po zapoznaniu się z zadaniem i wiele prób wypełnienia tej pustki kierunkiem zmierzającym do utworzenia sensownej solucji.

PP: Wspominaliście mi, że wzajemnie się „nakręcacie” w czasie konkursów. W jaki sposób?

LG: Wymieniamy się statystykami, uwagami na temat TC, możliwymi „tricky data”. Jeżeli wiem, że Marek zadanie zrobił z lepszymi statystykami niż uzyskałem, mam powód do szukania innego rozwiązania. Korzystamy też z forum, jako źródła statystyk, jak i wielu dodatkowych uwag, czy omawiania samych założeń do TC.

MP: Tak to prawda. Wymieniamy się ze sobą statystykami i powoduje to sportową żyłkę rywalizacyjna między nami. A poza tym przez te kilka miesięcy kontaktów mogliśmy się lepiej poznać nie tylko na polu T-SQL’a ale również zainteresowań w innych technologiach, na przykład w robieniu własnego chleba na własnym zakwasie, piwa, jogurtu, przyrządzaniu sałatek ze śledzi. I to jest ta wartość dodana uczestnictwa w tych zawodach.

PP: Gdybyście mieli mnie zachęcić do udziału w konkursach TSQLChallenge, co byście mi powiedzieli?

MP: Poza zdobywaniem doświadczenia w trudnych wyzwaniach oraz podglądaniem i uczeniem się efektownych technik innych uczestników nie mniej istotna jest żyłka sportowa towarzysząca turniejowi. Co prawda same emocje są rozłożone w czasie tak bardzo, że można je porównać do emocji towarzyszących turniejowi szachowemu, ale jeżeli masz żyłkę sportową, która drga w Tobie, to niewątpliwie Twoje miejsce jest na TC!

LG: TC to dobre miejsce do nauki i rozwijania umiejętności w kodowaniu TSQL. W odróżnieniu od kursów i książek, które często przekazują wiedzę w sposób nudny, schematyczny, TC ukazują problem do rozwiązania, nie podają wiedzy na tacy, tylko zmuszają do samodzielnego jej poszukiwania, intensyfikują proces myślenia i umożliwiają dużo szybsze wchłanianie wiedzy. TC są również znakomitym workiem treningowym dla szarych komórek (ostatni raz ten specyficzny poziom endorfin uzyskałem w szkole średniej rozwiązując trudniejsze całki ;-) ).

PP: Dziękuję za wywiad i powodzenia w kolejnych „wyzwaniach”!

LG: A ja dziękuję za zaproszenie, miłą rozmowę i poparcie w dalszych zmaganiach z tsql’owym puzzle.

MP: Dzięki i do zobaczenia przy okazji kolejnych wyzwań T-SQL z również Twoim udziałem.

A zatem, jeżeli kręci Cię zdrowe współzawodnictwo, chcesz poćwiczyć szare komórki i przy okazji potrenować pisanie kodu T-SQL, spróbuj swoich sił w TSQL Challenges. Obserwując Marka i Leszka muszę Cię jedynie ostrzec, że to wciąga jak diabli :-) Ale jeśli jeszcze nie masz swojego ulubionego uzależnienia… ;-)

I na koniec – chcę wyrazić szacunek dla osób prowadzących portal BeyondRelational.com i konkursy TC. Publikować co dwa tygodnie nowe zadanie, czuwać nad rozwojem sytuacji w każdym konkursie, robić testy nadesłanych rozwiązań i panować nad tym wszystkim, to naprawdę duże wyzwanie dla organizatorów. I jak na razie im się to udaje! Stworzyli atmosferę zdrowej rywalizacji i swego rodzaju społeczność uczestników zabawy. Brawo za ideę i jej realizację.

PS. A już zupełnie na koniec pragnę nadmienić, że ten wywiad jest swego rodzaju zapowiedzią mojego własnego, jednorazowego konkursu a la TC, który zorganizuję na blogu. Szczegóły w drugiej połowie września. Stay tuned!

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

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

Wstęp

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

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

Generujemy poligon

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

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

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

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

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

FOR XML EXPLICIT krok po kroku

Postawmy sobie pierwsze zadanie.

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

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

Rozwiązanie:

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

Komentarz (krok po kroku):

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

image

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

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

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

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

image

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

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

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

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

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

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

FOR XML EXPLICIT – tips & tricks

A teraz spróbujmy zmodyfikować nieco zadanie.

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

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

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

Rozwiązanie:

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

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

Podsumowanie

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