Posts tagged T-SQL

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

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

Intro

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

Skryptowanie typów prostych

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

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

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

Rozwiązanie:

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

Fragment wyniku:

image

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

Komentarz do kodu:

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

Słowo podsumowania

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

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

[PL] Skryptowanie w SQL Server 2008 – Obiekty proceduralne

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

Pilot

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

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

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

Funkcje, procedury, widoki

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

Rozwiązanie:

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

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

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

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

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

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

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

Fragment wyniku:

image

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

Komentarz do kodu:

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

Wyzwalacze na poziomie bazy

Zadanie: zeskryptować wszystkie wyzwalacze w wybranej bazie danych.

Rozwiązanie:

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

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

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

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

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

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

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

Fragment wyniku:

image

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

Komentarz do kodu:

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

Wyzwalacze na poziomie serwera

Zadanie: zeskryptować wszystkie wyzwalacze DDL na poziomie serwera.

Rozwiązanie:

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

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

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

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

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

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

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

Fragment wyniku:

image

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

Komentarz do kodu:

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

Słowo podsumowania

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

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

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

[PL] Migracja z SQL Server 2000 do 2008 – cuda i dziwy

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

Właśnie niedawno zakończyłem projekt migracji bazy klienta z SQL Server 2000 do 2008. Tego typu projekty mają to do siebie, że zawsze po ich zakończeniu jest o czym opowiadać. Nie inaczej jest i tym razem.

Compatibility mode 80 – elastyczność czy głupota?

Zawsze wiedziałem, że SQL Server 2000 wybaczał programistom T-SQL więcej, niż obecne najnowsze wersje tego systemu. Ale nie sądziłem, że jest aż tak tolerancyjny. Przypuśćmy, że mamy bazę świeżo przeniesioną z SQL Server 2000 na SQL Server 2008 (lub 2008 R2). Zasymuluję ten przypadek tak:

CREATE DATABASE Test_80;

ALTER DATABASE Test_80 SET COMPATIBILITY_LEVEL = 80;

GO

Teraz, załóżmy, że mam w tej bazie tabelę:

USE Test_80;

GO

CREATE TABLE dbo.TestTable (Column1 int NOT NULL);

GO

W obiektach bazy danych (widokach i procedurach) znalazły się takie oto odwołania do tabeli TestTable:

SELECT a.bc.Column1 FROM dbo.TestTable AS abc;

lub

SELECT a.b.c.Column1 FROM dbo.TestTable AS abc;

Oba zapytania zwracają zawartość tabeli! Hę? Okazuje się, że przy pracy w trybie zgodnym z SQL Server 2000 (lub na samym SQL Server 2000) takie numery przechodzą, bo silnik sam sobie dopowie, że my tych kropek tam wcale nie chcieliśmy wpisać! Nie muszę dodawać, że przejście w tryb kompatybilności 100 powoduje natychmiastowy błąd przy tego typu sytuacjach.

Kolejny kwiatek – w jednym z obiektów bazy danych znalazłem taki kod:

SET;

Najkrótsze polecenie T-SQL? I to też działa w SQL Server 2000! Oczywiście robi nic / tudzież nic nie robi :-) A jak świetnie nadaje się do sesji Marka Adamczuka o odlotach w T-SQL! ;-)

I takich kwiatków zapewne jest więcej, więc z jednej strony można się cieszyć, jaki to silnik był dawniej elastyczny i łaskawy dla programisty, a z drugiej narzekać, bo przez tę “łaskawość” przy migracji trzeba się sporo napocić, by wykryć i naprawić pojawiające się znikąd błędy.

Konkatenacja sortowana

Zapewne niejednemu programiście zdarzyło się budować w T-SQL skalarną wartość dołączając kolejne rekordy z tabeli / widoku w określonej kolejności. Ale uwaga – tu też czyha pułapka! I to nie tylko w trybie kompatybilności 80.

Przypuśćmy, że chcę do zmiennej doklejać kolejne nazwy baz danych i ich collation z widoku sys.databases w kolejności wg nazwy collation. Pierwszy pomysł:

DECLARE @zmienna varchar(8000);

SET @zmienna = '';

SELECT @zmienna = @zmienna + name + '(' + collation_name + '),'

FROM sys.databases

ORDER BY collation_name;

SELECT @zmienna;

Wynik? Okazuje się, że zmienna zawiera dane tylko jednej bazy… Co trzeba zrobić? Dołożyć kauzulę TOP z czymś dużym (nie TOP 100 PERCENT – choć na SQL Server 2000 czasem to pomagało). Na przykład:

DECLARE @zmienna varchar(8000);

SET @zmienna = '';

SELECT TOP 200000000 @zmienna = @zmienna + name + '(' + collation_name + ')'

FROM sys.databases

ORDER BY collation_name;

SELECT @zmienna;

Taki kod już działa. Największa liczba, jaką można wstawić do klauzuli TOP to maksimum typu bigint, czyli liczba 9223372036854775807. Powinno wystarczyć każdemu ;-)

Nie wspominam nawet o tym, że jak ktoś chce mieć sortowany widok (powinni tego zabronić!!!), to zestaw znany z SQL Server 2000: TOP 100 PERCENT + ORDER BY już nie działa od SQL Server 2005. Działa wyłącznie ORDER BY + TOP duuuużo (nie zawsze działa w widokach także sortowanie z użyciem funkcji okienkowych!), ale ja tego zdecydowanie nie polecam. Widoki nie są po to, by sortować dane.

Czemu nie TOP 99.99… PERCENT? O tym można poczytać na blogu Marka Powichrowskiego.

Post mortem

Oczywiście to nie jedyne “cuda i dziwy”, z jakimi można się zetknąć przy okazji migracji z SQL Server 2000 do wyższych wersji. Ale te przypadki wydały mi się na tyle ciekawe, że postanowiłem je ku pamięci opisać, by w przyszłości nikt nie był zdziwiony, kiedy taki “numer” mu się przytrafi (a SQL Server 2000 nadal ma się całkiem dobrze i wiele firm używa go w najlepsze).