Rozdział 12.
Rozszerzenia języka Transact-SQL
W rozdziale 11. została omówiona modyfikacja danych przy pomocy poleceń INSERT, UPDATE i DELETE. Polecenia te powinny być używane rozważnie, szczególnie polecenie DELETE. Rzadko (jeżeli w ogóle) należy przyznawać użytkownikom uprawnienie DELETE. Bardzo łatwo o przypadkowe usunięcie danych. Zaleca się stosowanie procedur składowanych, które wykonują usuwanie i zapewniają zachowanie integralności danych. Procedury składowane zostaną omówione szerzej w rozdziale 15.
Niniejszy rozdział koncentruje się na własnościach programowania języka Transact-SQL (T-SQL). Na początek zostaną przedstawione pliki wsadowe i skrypty. Następnie transakcje w SQL Serverze oraz elementy języka kontroli przepływu, takie jak bloki IF...ELSE i polecenia WHILE. Kolejnym omawianym zagadnieniem są typy blokad używane w SQL Serverze dla kontroli współbieżności.
Pliki wsadowe (batches)
Plik wsadowy (batch) jest zbiorem poleceń T-SQL interpretowanych przez SQL Server jako całość. Polecenia są zgrupowane razem a polecenie GO oznacza koniec pliku wsadowego. Następujący przykład pokazuje zadanie wsadowe uruchomione z SQL Server Query Analyzera:
USE PUBS
SELECT au_id, au_lname FROM authors
SELECT pub_id, pub_name FROM publishers
INSERT publishers VALUES ('9998','SAMS Publishing', 'Seattle', 'WA','USA')
GO
Zadania wsadowe muszą spełniać następujące zasady:
Wszystkie polecenia SQL są kompilowane jako grupa.
Jeżeli wystąpi błąd w składni w dowolnym miejscu zadania wsadowego, całe zadanie jest usuwane.
Niektóre polecenia mogą być używane w pliku wsadowym, podczas gdy dla innych nie jest to możliwe. Następujące polecenia CREATE mogą być połączone w pojedynczym pliku wsadowym:
CREATE DATABASE
CREATE TABLE
CREATE INDEX
Następujące polecenia nie mogą być łączone z innymi w tym samym pliku wsadowym:
CREATE RULE
CREATE TRIGGER
CREATE PROCEDURE
CREATE VIEW
W przypadku próby ich łączenia, wystąpi błąd 111, przedstawiony poniżej:
Server Msg 111, Level 15, State 1
'CREATE VIEW' must be the first statement in a query batch.
Nie można zmienić tabeli a następnie skorzystać z nowych kolumn w obrębie tego samego zadania wsadowego.
Polecenia SET mają skutek natychmiastowy, z wyjątkiem opcji QUOTED_IDENTIFIER i ANSI_NULLS.
Korzystając z zadań wsadowych należy mieć na uwadze jeszcze kilka innych kwestii. Jak zostało wcześniej wspomniane, gdy zadanie jest poddawane przetwarzaniu, jest poddawane analizie składniowej, optymalizowane, kompilowane a następnie wykonywane. Jeżeli podczas wykonywania, pojedyncze polecenie nie powiedzie się, zadanie wsadowe będzie kontynuowało wykonywanie następnego polecenia aż do ukończenia całego procesu. Przykładowo, jeżeli zadanie wsadowe posiada pięć poleceń CREATE TABLE i polecenie numer 3 powoduje powstanie błędu, SQL Server utworzy tabele 1,2,4 i 5. Tabela numer 3 nie zostanie utworzona.
Skrypty
Skrypt jest zbiorem jednego lub więcej zadań wsadowych, które są zapisywane jako grupa. Skrypty są wykonywane na ogół jako część pewnej jednostki pracy, która ma zostać zrealizowana, jak np.: ładowanie lub konserwacja danych. Wydruk 12.1 pokazuje przykładowy skrypt.
Wydruk 12.1 Przykładowy skrypt
USE PUBS
SELECT au_id, au_lname FROM authors
SELECT pub_id, pub_name FROM publishers
INSERT publishers VALUES ('9997','SAMS Publishing', 'Seattle', 'WA','USA')
GO
SELECT * FROM stores
GO
DELETE publishers WHERE pub_id = '9997'
GO
Zadania wsadowe i skrypty nie mają nic wspólnego z transakcjami, które zostaną omówione w kolejnej sekcji. Firma Microsoft dostarcza różnego rodzaju skryptów w katalogu ...\MSSQL\INSTALL; skrypty te mogą być używane jako przykłady. Należy znaleźć pliki z rozszerzeniem .SQL - są one świetnymi przykładami poprawnych skryptów T-SQL.
Transakcje
Transakcja jest jednostką wykonywania, w której wszystkie polecenia są wykonane poprawnie lub w przeciwnym przypadku nie jest wykonywane żadne z poleceń. Transakcje są stale używane, ale użytkownik może nie być świadomy ich użycia. Przykładowo, kasjer w banku przelewa $50 z rachunku rozliczeniowego na rachunek bieżący. Kasjer może obciążać rachunek na $50 a następnie zapisać oszczędności na $50. Co się stanie, jeżeli kasjer zapomni przełożyć pieniądze na rachunek bieżący? Większość ludzi byłaby bardzo zmartwiona z tego powodu. Spodziewaliby się, że jeżeli pieniądze zostały zdjęte z rachunku rozliczeniowego powinny wpłynąć do oszczędności. Jest to transakcja. Jednostka pracy przetwarza wszystkie polecenia kończąc je sukcesem, lub kończy się niepowodzeniem i wycofuje wszystko co zostało wykonane. Możemy się spodziewać transakcji w codziennym życiu, ale jako programista SQL Servera, użytkownik musi ręcznie zaprogramować transakcje aby działały prawidłowo. Administrator musi rozumieć transakcje, ponieważ mogą one powodować przepełnienie dzienników transakcji jeżeli są używane nieprawidłowo.
Transakcje są tworzone przy zachowaniu następujących czterech własności, które złożone razem zwane są własnościami ACID:
Atomic (Niepodzielność) - Mówi się, że transakcja jest niepodzielna (atomic) gdy zostaje wykonana w całości lub jest w całości usuwana. Jeżeli jedno z poleceń nie powiedzie się, wszystkie polecenia będące częścią transakcji zakończą się niepowodzeniem.
Consistent (Spójność) - Mówi się, że transakcja pozostawia bazę danych w spójnym stanie po udanym lub nieudanym wykonaniu transakcji. Zmiany wykonane przez transakcję są spójne z jednego stanu w drugi.
Isolated (Odosobnienie) - Mówi się, że transakcja jest odosobniona gdy nie zachodzi w reakcję lub nie powoduje konfliktu z innymi transakcjami w bazie danych.
Durable (Trwałość) - Mówi się, że transakcja jest trwała jeżeli gwarantowane jest, że wykonane działania pozostaną kompletne bez względu na to co się stanie z baza po poprawnym zakończeniu transakcji. Jeżeli wystąpi awaria zasilania i serwer bazy danych ulegnie awarii, istnieje gwarancja ze transakcja będzie kompletna po ponownym uruchomieniu serwera.
Transakcje gwarantują, że wykonane działania zakończą się sukcesem lub niepowodzeniem w całości, jak opisano w poprzednim akapicie. Blokady są jedną z gwarancji takiego działania. Podczas wykonywania transakcji, żadne inne transakcje nie mogą modyfikować danych, które są zmieniane przez tę transakcję, aż do czasu gdy użytkownik zdecyduje że zmiany są trwałe. Gdy użytkownik modyfikuje dane zakłada blokady wyłączne (exclusive lock) na danych z którymi pracuje. Odwrotnie, nie można czytać danych innej transakcji, jeżeli wykonuje ona działania modyfikujące dane. Użytkownik żąda w tej sytuacji blokady współdzielonej (shared lock) na tych danych, ale inna transakcja używa blokady wyłącznej na tych samych danych i powoduje że nikt inny nie może czytać tych danych. Blokady zostaną omówione szczegółowo w dalszej części książki.
Typy transakcji
Używane są trzy typy transakcji: jawne (explicit), niejawne (implicit) i automatyczne (automatic -często zwane autocommit).
Transakcje jawne
Transakcje jawne są konfigurowane przez użytkownika ręcznie. Do oznaczenia początku i końca transakcji jawnej używane są zarezerwowane słowa. Są to m.in.: BEGIN TRAN[SACTION], COMMIT TRAN[SACTION], COMMIT WORK, ROLLBACK TRAN[SACTION], ROLLBACK WORK i SAVE TRAN[SACTION].
Aby rozpocząć transakcję jawną należy wpisać BEGIN TRAN (lub BEGIN TRANSACTION). Aby poinformować SQL Server, że transakcja jest wykonana i cała operacja powinna zostać zachowana, należy wpisać COMMIT TRAN (lub COMMIT WORK). Typowa transakcja może wyglądać następująco:
BEGIN TRAN
UPDATE authors
SET city= 'San Jose' WHERE au_lname = 'Smith'
INSERT titles
VALUES ('BU1122', 'Teach Yourself SQL Server 2000 in 21 days', 'business','9998'. $35.00, $1000.00, 10,4501, 'A great book!', '8/1/2000')
SELECT * from titleauthor
COMMIT TRAN
Potrzeba również czasem anulować transakcje. Aby to zrobić, należy użyć polecenia ROLLBACK TRAN (lub ROLLBACK WORK). Następnie należy rozważyć następujący przykład polecenia ROLLBACK TRAN:
BEGIN TRAN
Delete sales where title_id = 'BU1032'
IF @@ERROR >0
ROLLBACK TRAN
ELSE
COMMIT TRAN
Polecenie ROLLBACK TRAN anuluje całkowicie transakcję. Wszelkie operacje, które zostały wykonane do tego momentu zostają wycofane (rollback) lub anulowane. Można tworzyć również w transakcji punkty zwane savepoints (omówione w dalszej części), a następnie sukcesywnie wycofywać do kolejnych takich punktów. Przykład najlepiej ilustruje opisywaną sytuację:
BEGIN TRAN
UPDATE table1 SET col1 = 5 WHERE col2 = 14
SAVE TRAN savepoint1
INSERT table2 values (3,16)
IF @@error > 0
ROLLBACK TRAN savepoint1
DELETE table3 WHERE col1 > 2
IF @@eerror > 0
ROLLBACK TRAN
ELSE
COMMIT TRAN
Warto zauważyć, że w poleceniu SAVE TRAN występuje nazwa savepoint. Poprzez umieszczenie punktu savepoint1 w pierwszym wycofaniu, zaznacza się, że zamiast wycofania całej transakcji od początku, nastąpi wycofanie do określonego nazwanego punktu - w tym przypadku savepoint1. Polecenie INSERT table2 było by wycofane ale transakcja będzie kontynuowana. Zasadniczo, INSERT będzie „usunięte” z transakcji. Ponieważ nie ma nazwy przy drugiej operacji wycofania, ROLLBACK TRAN powróci do polecenia BEGIN TRAN.
Teraz zostaną omówione polecenia TRANSACTION, rozpoczynające się od BEGIN i COMMIT.
BEGIN TRAN [transaction_name]
COMMIT TRAN [transaction_name]
COMMIT [WORK]
Można opcjonalnie przypisać transakcji nazwę (transaction_name). Nazwa transakcji musi być prawidłowym identyfikatorem SQL Servera. Można zastąpić zmienną, wszędzie gdzie występuje transaction_name, dopóki jest ona tłumaczona na poprawną nazwę transakcji. Nie ma potrzeby nazywania transakcji, z wyjątkiem najbardziej zewnętrznego polecenia BEGIN w transakcji. Nie można określić nazwy transakcji w poleceniu COMMIT WORK:
Transakcje mogą być zagnieżdżone, chociaż to zagnieżdżanie jest jedynie własnością składni. Naprawdę transakcje nie mogą być zagnieżdżane. Można wpisać wiele transakcji w skrypcie, ale faktycznie używana jest tylko jedna transakcja.
Składnia polecenia SAVE TRANSACTION wygląda następująco:
SAVE TRAN [savepoint_name]
savepoint_name oznacza nazwę punktu zachowania, do którego następuje wycofanie części operacji w transakcji, bez anulowania całej transakcji. Nazwa savepoint musi być poprawnym identyfikatorem SQL Servera.
Składnia polecenia ROLLBACK TRANSACTION jest następująca:
ROLLBACK TRAN [transaction_name | savepoint_name]
ROLLBACK WORK
Opis opcjonalnych nazw jest podobny jak w przypadku przedstawionych wcześniej poleceń BEGIN/COMMIT TRAN i SAVE TRAN. Również w tym przypadku polecenie ROLLBACK WORK nie pozwala na określenie w tym miejscu nazwy transakcji ani nazwy punktu savepoint.
Zmienna globalna @@trancount Zmienna globalna @@trancount ma zastosowanie bezpośrednio do tych transakcji. Po wpisaniu BEGIN TRAN zmienna @@trancount zwiększa się o jeden. Polecenie SAVE TRAN nie wpływa na tę zmienną. ROLLBACK TRAN może mieć kilka różnych wyników w zależności czy jest podana nazwa transakcji czy też nie. Jeżeli nie jest podana nazwa transakcji, zmienna @@trancount jest ustawiana z powrotem na 0 (całość operacji jest wycofywana). Jeżeli jest określona nazwa ostatniej transakcji, zmienna @@trancount jest obniżana o jeden. Polecenie ROLLBACK WORK zawsze zeruje zmienną @@trancount oraz anuluje całość operacji wykonywanych od pierwszego BEGIN TRAN. Aby przekonać się jak działa ta zmienna należy przeanalizować następujący kod:
SELECT @@TRANCOUNT -- It should return 0.
BEGIN TRAN t1
SELECT @@TRANCOUNT -- It should return 1.
SAVE TRAN savepoint1
SELECT @@TRANCOUNT -- It still is set to 1.
ROLLBACK TRAN savepoint1
SELECT @@TRANCOUNT -- It still is set to 1.
BEGIN TRAN t2
SELECT @@TRANCOUNT -- It should return 2.
ROLLBACK TRAN
SELECT @@TRANCOUNT - It's back to 0.
Dozwolone i niedozwolone polecenia Niektóre polecenia nie są dozwolone jako cześć transakcji jawnej, są to:
ALTER DATABASE RESTORE DATABASE
DROP DATABASE CREATE DATABASE
RECONFIGURE RESTORE LOG
BACKUP LOG UPDATE STATISTICS
Inne polecenia mogą pojawiać się razem w transakcji, chociaż nie zmienia to żadnej z reguł omówionych wcześniej.
Punkty zachowania Nie są podane limity ilości punktów zachowania (savepoints), które mogą występować w pojedynczej transakcji. Gdy transakcja jest zatwierdzona, nie ma możliwości wycofania.
W pojedynczej transakcji nazwy savepoints mogą się powtarzać, jednak tylko ostatnie wystąpienie punktu zachowania jest faktycznie używane przy wycofywaniu do punktu w czasie. Przykładowo, następujące polecenie ROLBACK TRAN transave1powoduje wycofanie jedynie do drugiego wystąpienia punktu transave1. Pierwsze wystąpienie nazwy jest ignorowane, gdyż pojawia się drugie wystąpienie.
BEGIN TRAN
INSERT
UPDATE
SAVE TRAN transave1
DELETE
INSERT
SELECT
SAVE TRAN transave1
INSERT
DELETE
IF @@ERROR <> 0
ROLLBACK TRAN transave1
ELSE
COMMIT TRAN
Gdy polecenie ROLLBACK TRAN jest używane w procedurze wyzwalanej i nie są określone żadne nazwy punktów zachowania, całość transakcji jest wycofywana i pozostała część pliku wsadowego nie jest wykonywana. Jednak, wykonywanie działań może być kontynuowane z następnym zadaniem wsadowym. Należy to przetestować pisząc odpowiedni kod i sprawdzając, czy ten szczególny rodzaj wycofania jest obsługiwany. Rozsądnie jest wywołać powstanie błędu danego typu w procedurze wyzwalanej powodującego powrót do wołającej procedury i oprzeć się na wywoływanym działaniu, aby prawidłowo określić stan transakcji. Podejście to jest znacznie bezpieczniejsze.
Zdalne procedury składowane Wywołania zdalnych procedur nie są normalnie rozważane jako część transakcji:
BEGIN TRANSACTION
UPDATE table1 SET col1= 5 WHERE col1 = 1
DELETE table1 WHERE col1 = 5
EXEC server2.pubs..usp_insertpublisher parm1 parm2 parm3
COMMIT TRAN
Jeżeli procedura składowana pubs..usp_insertpublisher na serwerze 2 miała wywołać polecenie ROLLBACT TRAN nie będzie miała wpływu na lokalną transakcję.
Jednak, opcja konfiguracji serwera REMOTE_PROC_TRANSACTIONS, normalnie ustawiona na 0 (wyłączona), kontroluje czy zdalne procedury składowane są automatycznie wykonywane. Aby włączyć automatyczną rejestrację zdalnych procedur składowanych w transakcjach należy uruchomić następujący kod:
EXEC sp_configure "REMOTE_PROC_TRANSACTIONS", 1
RECONFIGURE WITH OVERRIDE
Przykład ten wpływa na wykonywanie wszelkich zdalnych procedur składowanych na serwerze, więc należy go uruchamiać po odpowiednim zastanowieniu. Transakcje rozproszone zostaną omówione w dalszej części rozdziału w sekcji „transakcje rozproszone”.
Transakcje automatyczne
Nawet jeżeli wydaje się, że transakcje nie są używane, mogą być one wykonywane w tle (niewidoczne dla użytkownika). Wszelkie wykonania polecenia modyfikacji danych w SQL Serverze to transakcje niejawne (implied transactions).
W następującym zadaniu wsadowym każde polecenie SQL jest oddzieloną transakcją. Czyli to poniższe zadanie wsadowe to faktycznie trzy osobne transakcje. Jeżeli jedno z tych wyrażeń nie powiedzie się, nie ma wpływu na inne. Każde polecenie kończy się sukcesem lub niepowodzeniem (jedynie własnym) bez względu na inne polecenia w pliku wsadowym.
INSERT table1 VALUES (1,'abcde')
UPDATE table1 SET col1 = 5 WHERE col1 = 1
DELETE FROM table1 WHERE col1 = 5
GO
Transakcje mogą także prowadzić do poprawy wydajności. SQL Server może znacznie lepiej wykorzystywać dysk ponieważ transakcja nie jest rejestrowania w dzienniku transakcji dopóki nie zostanie ukończona. Dlatego, korzystnie jest grupować polecenia. Jeżeli poprzednie polecenia zostaną zgrupowane w jedną transakcję, zamiast dziewięciu, następuje jedynie pięć wpisów do dziennika transakcji.
BEGIN TRAN
INSERT table1 VALUES (1,'abcde')
UPDATE table1 SET col1 = 5 WHERE col1 = 1
DELETE FROM table1 WHERE col1 = 5
COMMIT TRAN
Jeżeli zwiększy się grupowanie poleceń, transakcje mogą znacząco podnieść wydajność poleceń modyfikacji danych.
Transakcje niejawne
Transakcje niejawne są tworzone aby zachować zgodność ze standardem American National Standards Institute (ANSI). Gdy włączona jest opcja transakcji niejawnych, wybrane polecenia T-SQL automatycznie uruchamiają polecenie BEGIN TRAN. Należy natomiast jawnie zatwierdzić lub wycofać te polecenia przy pomocy polecenia COMMIT lub ROLLBACK TRAN.
Transakcje niejawne można włączyć z poziomu sesji następującym poleceniem:
SET IMPLICIT_TRANSACTIONS ON
Do końca sesji, następujące polecenia będą wymagały bezpośredniego zatwierdzenia (lub wycofania):
ALTER TABLE GRANT REVOKE
CREATE DROP DELETE
SELECT INSERT UPDATE
TRUNCATE TABLE FETCH OPEN
Aby wyłączyć transakcje niejawne należy wpisać polecenie:
SET IMPLICIT_TRANSACTIONS OFF
Przykładowo, następujące dwa fragmenty kodu źródłowego są identyczne (w znaczeniu transakcji). Warto zauważyć, że można zweryfikować poniższe polecenia przy pomocy zmiennej @@trancount.
CREATE TABLE table1 (col1 int not null)
BEGIN TRAN
INSERT table1 VALUES (1)
SELECT @@trancount
COMMIT TRAN
SET IMPLICIT_TRANSACTIONS ON
INSERT table1 values (2)
SELECT @@trancount
COMMIT TRAN
Włączenie opcji IMPLICIT_TRANSACTIONS wymaga pamiętania o zatwierdzaniu lub wycofywaniu każdej transakcji. Łatwo o tym zapomnieć - jeżeli to nastąpi, transakcje pozostaną otwarte i spowodują powstanie blokad (omówionych później) na dłuższy okres, niż normalnie. Zaleca się nie włączanie tej opcji; chyba, że użytkownik jest pewny, że będzie pamiętał o zatwierdzaniu transakcji.
Działanie transakcji
Teraz, po zapoznaniu się z transakcjami jawnymi, niejawnymi i automatycznymi należy prześledzić krok po kroku opis działań SQL Servera podczas wykonywania transakcji. Działania te zostaną omówione na podstawie przykładowego zbioru poleceń SQL przedstawionych na wydruku 12.2.
Wydruk 12.2. Wykonywanie zbioru transakcji krok po kroku
BEGIN TRAN
INSERT table1 VALUES (1,'abcde')
UPDATE table1 SET col1 = 5 WHERE col1 = 1
DELETE FROM table1 WHERE col1 = 5
COMMIT TRAN
Działania wykonywane w powyższym przykładzie:
Gdy do bazy danych zostaje wysłane polecenie BEGIN TRAN, analizator składni (parser) SQL Servera wykrywa żądanie rozpoczęcia jawnej transakcji. Jednak, SQL Server 2000 nie alokuje rekordu dziennika w pamięci dopóki nie jest wykonane jakieś konkretne działanie, czyli technicznie transakcja jeszcze się nie rozpoczęła.
Uruchamiane jest polecenie INSERT. SQL Server tworzy rekord dziennika w pamięci i alokuje identyfikator transakcji (ID) dla nowej transakcji. W dzienniku transakcji rejestrowany jest nowy wiersz a następnie modyfikowana jest w pamięci strona danych dla table1 (zobacz rysunek 12.1). Jeżeli wymagana strona nie znajduje się w pamięci, jest pobierana z dysku.
Kolejne polecenia są uruchamiane podobnie. Polecenie UPDATE jest rejestrowane w dzienniku transakcji a następnie w pamięci modyfikowana jest strona danych (zobacz rysunek 12.2).
--> Rysunek 12.1[Author:A.K.] . Krok 2 procesu wykonywania transakcji. |
|
--> Rysunek 12.2[Author:A.K.] . Krok 3 procesu wykonywania transakcji. |
|
W tym przykładzie, pojedynczy wiersz jest rejestrowany w dzienniku do aktualizacji. W większości przypadków jest prawdą, że dziennik pokazuje na bieżąco usunięcie, a następnie wstawienie wiersza a nie pojedynczy modyfikowany rekord. Pobranie pojedynczego modyfikowanego rekordu do zapisania w dzienniku wymagałoby uaktualnienia „w miejsce poprzedniego”. Różne warunki, których spełnienie jest wymagane do uaktualnienia w miejscu są omówione szczegółowo w książce Microsoft SQL Server 2000 Unleashed opublikowanej przez Sams Publishing.
Kiedy SQL Server otrzyma polecenie COMMIT TRAN, w dzienniku transakcji jest zapisywany rekord dla odpowiedniej bazy danych (zobacz rysunek 12.3). Jest to gwarancja, że transakcja może być odtworzona. Ponieważ zmiany w dzienniku są zapisywane na dysku, gwarantuje to, ze transakcje mogą być odtworzone - nawet w przypadku, gdy wystąpi awaria zasilania lub uszkodzenie bazy danych zanim strona danych zostanie zapisana na dysk.
--> Rysunek 12.3[Author:A.K.] . Krok 4 procesu wykonywania transakcji. |
|
W przypadku SQL Servera 2000 rekordy dziennika są zapisywane w osobnym pliku (lub zestawie plików) i nie są dostępne przy pomocy T-SQL. Firma Microsoft nie dostarczyła narzędzia zapewniającego dostęp do dziennika transakcji (innego niż zaawansowane polecenie DBCC LOG, które nie jest objęte zakresem tej książki). Jedynie wewnętrzne procesy SQL Servera, takie jak tworzenie kopii bezpieczeństwa i odtwarzanie potrzebują mieć dostęp do dziennika transakcji.
Proces CHECKPOINT
Po omówieniu transakcji, powstaje pytanie, kiedy dane są zapisywane na dysk. Wpisy dziennika zostają zapisane przy uruchomieniu polecenia COMMIT TRAN. Natomiast zapisywanie stron danych na dysk jest związane z procesem CHECKPOINT, wewnętrznym procesem SQL Servera, używanym do kopiowania (flush) danych z pamięci na dysk.
Checkpoint (punkt kontrolny) zapewnia, że odtwarzanie zatwierdzonych transakcji nie będzie zajmowało nadmiernej ilości czasu. Po wystąpieniu punktu kontrolnego, w dzienniku rejestrowany jest wpis, określający, że wszystkie modyfikowane dane zostały zapisane na dysk. Wpis ten określa punkt w dzienniku transakcji, do którego proces odtwarzania SQL Servera może odtwarzać dane, oraz zapewnia, że nie trzeba sprawdzać żadnych wcześniej zatwierdzonych transakcji aby zapewnić całkowite odtworzenie.
W SQL Serverze używane są dwa rodzaje punktów kontrolnych: automatyczny i ręczny. Automatyczny proces checkpoint występuje w zależności od wewnętrznych kalkulacji SQL Servera. Przy pomocy opcji konfiguracji RECOVERY INTERNAL, konfiguruje się jak często będzie występował proces checkpoint. Opcja ta określa w minutach maksymalną ilość czasu wymaganą do odtworzenia każdej bazy danych w systemie. Jeżeli SQL Server stwierdzi, że odtworzenie bazy zajmie przewidzianą ilość czasu lub więcej, ustawia automatyczny punkt kontrolny (checkpoint). Gdy to się stanie, wszystkie modyfikowane dane (dla tej bazy danych) są zapisywane z pamięci na dysk, i wszystkie rekordy dziennika tej bazy danych są również zapisywane na dysk. Automatyczny proces chekpoint przegląda co 60 sekund wszystkie bazy danych, określając czy dana baza nie potrzebuje ustawienia punktu kontrolnego. Domyślnym ustawieniem dla opcji RECOVERY INTERNAL jest 0, co oznacza, że SQL Server sam decyduje kiedy jest potrzebny checkpoint.
Automatyczny punkt kontrolny występuje również w dwóch innych sytuacjach. Po pierwsze, występuje gdy zostaje uruchomiona procedura składowana sp_dboption do zmiany opcji bazy danych. Po drugie występuje przy wyłączaniu SQL Servera. SQL Server można wyłączyć poleceniem T-SQL SHUTDOWN lub poprzez zamknięcie usługi MSSQLServer. W takich sytuacjach SQL Server automatycznie ustawia punkt kontrolny.
Aby wymusić w dowolnym momencie ręczny punkt kontrolny, należy wpisać polecenie T-SQL CHECKPOINT. Aby uruchomić to polecenie użytkownik musi należeć do roli bazy danych db_owner. Gdy ustawiony jest ręczny punkt kontrolny, wszystkie modyfikowane strony z pamięci są kopiowane na dysk, w ten sam sposób jak w przypadku automatycznego procesu checkpoint.
W przypadku baz danych z ustawioną opcją Truncate Log on checkpoint, automatyczny proces checkpoint wycina z dziennika transakcji zatwierdzone transakcje w regularnych odstępach czasu (lub gdy dziennik się przepełni). Ręczny proces checkpoint nie ma wpływu na dziennik transakcji (poza kopiowaniem wpisów dziennika na dysk), bez względu na ustawienie w bazie danych opcji Truncate Log on checkpoint.
Transakcje rozproszone
Wszystkie omawiane do tej pory transakcje dotyczyły jednego serwera. SQL Server 2000 może obsługiwać transakcje związane z więcej niż jednym serwerem. Możliwość ta jest obsługiwana przy pomocy usługi Microsoft Distributed Transaction Coordinator (MSDTC).
Można korzystać z transakcji rozproszonych na różne sposoby:
Można programować transakcje rozproszone przy pomocy DB-Lib, open database connectivity (ODBC) lub interfejs programowy aplikacji (API) typu object linking and embedding (OLE) DB. ActiveX Data Objects (ADO) również zezwala na korzystanie z transakcji rozproszonych. Opcje te są poza zakresem niniejszej książki.
Można korzystać ze składni T-SQL: BEGIN DISTRIBUTED TRANSACTION.
Można ustawić opcję SET REMOTE_PROC_TRANSACTION aby umożliwić korzystanie z transakcji rozproszonych w pojedynczej sesji.
Teraz zostaną omówione dwie ostatnie metody. Aby umieścić usługę MSDTC w transakcji i zapewnić koordynację tej usługi na wielu serwerach, należy uruchomić polecenie BEGIN DISTRIBUTED TRANSACTION w dokładnie ten sam sposób jak polecenie BEGIN TRAN. Wcześniejsza sekcja dotycząca transakcji zaznaczała, że zdalne procedury składowane działają poza bieżącą transakcją, z której zostały wywołane. W przypadku transakcji rozproszonych, zdalne procedury składowane są włączone do transakcji. Błąd na zdalnym serwerze wpływa również na wywołujący serwer lub serwery. W następującym przykładzie kodu, jeżeli w procedurze składowanej wywoływanej z serwera 2 wystąpi błąd, będzie miał on wpływ na transakcję na bieżącym serwerze:
BEGIN DISTRIBUTED TRANSACTION
UPDATE table1 SET col1= 5 WHERE col1 = 1
DELETE table1 WHERE col1 = 5
EXEC server2.pubs..usp_insertpublisher parm1 parm2 parm3
COMMIT TRAN
SQL Server 2000 może również kierować zapytania do zdalnych serwerów jako cześć polecenia T-SQL. Dlatego, poniższy kod jest również prawidłową transakcją rozproszoną w SQL Serverze 2000:
BEGIN DISTRIBUTED TRANSACTION
INSERT remoteserver.pubs..remotetable(col1, col2) VALUES (1,2)
UPDATE localtab SET col2= 5 WHERE col1 = 1
COMMIT TRAN
Trzecią metodą jest użycie polecenia SET REMOTE_PROC_TRANSACTIONS. Jeżeli ta opcja jest ustawiona w sesji, wszystkie zdalne procedury składowane wywoływane podczas sesji są rozważane jako cześć transakcji. Można również ustawić tę samą opcję konfiguracyjną przy pomocy procedury sp_configure, opcja jest ustawiona od tego momentu dla wszystkich sesji. Przed ustawieniem tej opcji dla całego serwera (wszystkich sesji), należy się upewnić czy zostały przetestowane wszelkie konsekwencje wynikające z używania dowolnej istniejącej zdalnej procedury składowanej.
SQL Server 2000 pozwala na łączenie się z dwoma typami zdalnych baz danych: serwerami zdalnymi i serwerami sprzężonymi. Opcja zdalnego serwera jest dostarczana dla zachowania zgodności wstecz, a opcja serwera sprzężonego została ulepszona w SQL Serverze 2000.
Serwery zdalne
Serwery zdalne umożliwiają uruchamianie procedur składowanych w zdalnych bazach danych SQL Servera. W zależności od użytego polecenia Transact-SQL: BEGIN DISTRIBUTED TRANSACTION lub SET REMOTE_PROC_TRANSACTIONS ON, zdalne procedury składowane mogą być (lub nie) częścią transakcji. Aby zezwolić na uruchamianie zdalnych procedur, należy skonfigurować zdalny serwer:
Należy dodać nazwę zdalnego serwera do tabeli systemowej sysservers na SQL Serverze. Można to zrobić, uruchamiając polecenie: EXEC sp_addserver remoteserver.
Należy dodać własny serwer do tabeli sysservers w zdalnej bazie danych SQL Servera poprzez polecenie: EXEC sp_addserver yourserver.
Na serwerze lokalnym należy uruchomić następujące polecenia T- SQL:
EXEC sp_addremotelogin remoteserver, sa, sa
EXEC sp_remoteoption remoteserver, sa, sa, trusted, true
Na zdalnym serwerze, należy uruchomić kod z punktu 3 , ale podając nazwę serwera lokalnego.
Teraz, logując się jako sa można już uruchamiać zdalne procedury składowane. Aby ustawić inne konta logowania, potrzeba uruchomić poprzednie procedury składowane. Aby odwołać się do zdalnych procedur składowanych, należy użyć nazwy z czterech części - servername.dbname.owner.procname - z wykonywanym poleceniem. Przykładowo, odwołując się do procedury składowanej byroyalty w bazie danych pubs na serwerze gizmo, należy uruchomić następujący kod:
EXEC gizmo.pubs.dbo.byroyalty 10
To podejście jest bardzo pożyteczne, ale nie jest tak elastyczne jak używanie serwerów sprzężonych.
Serwery sprzężone (linked servers)
Firma Microsoft usprawniła obsługę serwerów sprzężonych w SQL Serverze 2000. Serwery sprzężone umożliwiają otworzenie OLE DB rowset na zdalnym serwerze. Inaczej niż w omawianych już zdalnych serwerach, serwery sprzężone umożliwiają pobieranie danych ze źródeł innych niż SQL Server. Można tym sposobem uzyskać dostęp do dowolnego źródła danych OLE DB. Czyli, można mieć dostęp do danych z Oracle, Microsoft Access, Microsoft Excel lub każdego innego źródła danych OLE DB.
Często wykorzystanie serwerów sprzężonych jest korzystniejsze niż serwerów zdalnych. Można nadal wykonywać procedury składowane, ale można również stosować złączenia zdalnych tabel. Aby ustawić serwer sprzężony, należy użyć systemowych procedur składowanych sp_addlinkedserver i sp_addlinkedsrvlogin:
Exec sp_addlinkedserver 'remoteserver', 'SQL Server'
Exec sp_addlinkedsrvlogin 'remoteserver', 'TRUE'
Jeżeli mają być uruchamiane zapytania rozproszone lub zdalne procedury składowane z opcjami serwera przyłączonego, należy uruchomić skrypt instcat.sql z katalogu instalacyjnego SQL Servera 2000 dla wszystkich serwerów SQL Server 6.5.
Wcześniejszy kod dodaje do SQL Servera zdalny serwer i określa, że w przypadku próby połączenia ze zdalnym serwerem, należy logować się ze wszystkimi danymi logowania, które są używane na serwerze lokalnym.
Dla przykładu, można uruchomić zapytania, które łączą tabelę titles z serwera lokalnego z tabelą publishers z serwera zdalnego:
Select t1.title, t2.pub_name
From pubs..titles t1 -This is the local table
Inner Join remoteserver.pubs..publishers t2 -This is the remote table
ON t1.pub_id = t2.pub_id
Jak można się domyśleć, potrzeba jeszcze wiele dodatkowych informacji aby zaimplementować serwery sprzężone - szczególniej jeżeli decyduje się na łączenie tabel SQL Servera z danymi ze źródła innego niż SQL Server używając możliwości heterogenicznych OLE DB. Tematy te są poza zakresem tej książki i zaleca się odwołanie do książki Microsoft SQL Server 2000 Unleashed opublikowanej przez Sams publishing, aby znaleźć szczegółowe informacje o serwerach sprzężonych.
Elementy języka kontroli przepływu
Jeżeli zadanie wsadowe z wieloma poleceniami jest wysyłane do SQL Servera do wykonania, polecenia są wykonywane w kolejności, w jakiej zostały podane. Język T-SQL dostarcza kilku poleceń, które pozwalają na zmianę kolejności wykonywania poleceń w pliku wsadowym. Są one szczególnie przydatne w transakcjach, procedurach wyzwalanych, funkcjach i procedurach składowanych.
Blok BEGIN...END
Niektóre z poleceń kontroli przepływu jakie zostaną omówione będą wymagały pojedynczego polecenia jako części swojej składni. Wszędzie gdzie oczekiwane jest pojedyncze polecenie, można używać tego pojedynczego polecenia. Jeżeli zajdzie potrzeba, aby wiele poleceń było wykonywanych razem, należy zamknąć je pomiędzy słowami kluczowymi BEGIN i END. Konstrukcja ta zostanie wykorzystana w kolejnych przykładach.
Polecenie PRINT
Do tego momentu, jedynym sposobem zwrócenia informacji z SQL Servera do programu klienta było korzystanie z polecenia SELECT. SQL Server obsługuje również polecenie PRINT, ale jest ono ograniczone:
PRINT {'Any ASCII text' | @local_variable | @@global_variable}
Wszystko, co można drukować to łańcuch ASCII (stała łańcuchowa) lub zmienna typu znakowego (stała lub o zmiennej długości), jak w tym przykładzie:
PRINT "Hello"
PRINT @@version
Jeżeli potrzeba wydrukować coś bardziej złożonego, należy zbudować łańcuch w zmiennej znakowej a następnie wydrukować tę zmienną.
Następujący przykład używa @msg do zbudowania jednego dużego łańcucha do wydrukowania. Nie można używać operacji konkatenacji ani zmiennych liczbowych w poleceniu PRINT, ale można używać ich w ustawieniu SET i SELECT, a następnie wydrukować wyniki.
USE pubs
DECLARE @msg varchar(50),
@numWA tinyint
SELECT @numWA = COUNT(*) FROM stores
WHERE state = 'WA'
SELECT @msg = 'There are ' + convert(varchar(3), @numWA) +
' stores in Washington'
PRINT @msg
(1 row(s) affected)
(1 row(s) affected)
There are 2 stores in Washington.
Block IF...ELSE
Blok IF..ELSE pozwala aby polecenie było wykonywane warunkowo. Po słowie IF występuje wyrażenie, które musi być warunkiem typu prawda lub fałsz. Jeżeli wyrażenie jest prawdziwe, to polecenie jest wykonywane. Opcjonalne słowo kluczowe ELSE wprowadza polecenie alternatywne, które może być wykonywane gdy wyrażenie występujące po IF jest fałszywe.
IF Boolen_expression
{sql_statement | statement_block}
ELSE {Boolean_expression}
{sql_statement | statement_block}
Przykładowo, poniższe, polecenie wywołuje procedurę uspWeeklyReport jeżeli dniem tygodnia jest piątek; w innym przypadku wywołuje procedurę upsDailyReport:
IF (datename(dw, getdate()) = 'Friday')
BEGIN
PRINT 'Weekly raport'
EXEC uspWeeklyReport
END
ELSE
BEGIN
PRINT 'Daily Report'
EXEC uspDailyReport
END
Warto zauważyć, że nie ma określonego słowa kluczowego, które oznacza koniec bloku IF..ELSE. Wyrażenie Boolean, które następuje po słowie IF może zawierać polecenie SELECT. Jeżeli to polecenie SELECT zwraca pojedynczą wartość, wartość ta może być porównywana z inną wartością aby było możliwe utworzenie wyrażenia Boolean.
W następującym przykładzie, jeżeli średnia cena książki jest większa niż$15, należy wydrukować jeden komunikat, jeżeli średnia cena książki jest mniejsza lub równa $15, należy wydrukować inny komunikat:
IF (SELECT avg(price) FROM titles) > $15
PRINT 'Hold a big sale'
ELSE
PRINT 'Time to raise prices'
Jeżeli polecenie SELECT następujące po słowie IF zwraca więcej niż jedną wartość, można skorzystać ze specjalnej formy składni IF - IF EXISTS:
IF EXISTS (SELECT statement)
{sql_statement | statement_block}
[ELSE
{sql_statement | statement_block}]
IF EXISTS zwraca wartość „prawda” jeżeli dane polecenie SELECT zwraca jakikolwiek wiersz lub zwraca wartość „fałsz” w przypadku, gdy polecenie SELECT nie zwróciło żadnego wiersza. Następujący przykład zwraca wszystkie informacje o książce publikowanej przez wydawcę o ID 9933:
IF EXISTS
(SELECT * FROM titles WHERE pub_id = '9933')
BEGIN
PRINT 'Here are the books: '
SELECT * FROM titles WHERE pub_id = '9933'
END
ELSE
PRINT 'No books from that publisher'
Poniższe zapytanie daje takie same wyniki ale przez zadanie przeciwnego warunku zapytania:
IF NOT EXISTS
(SELECT * FROM titles WHERE pub_id = '9933')
BEGIN
PRINT 'No books from that publisher'
RETURN
END
ELSE
BEGIN
PRINT 'Here are the books'
SELECT * FROM titles WHERE pub_id = '9933'
END
W niektórych przypadkach, IF EXISTS dostarcza lepszej wydajności niż alternatywna metoda ponieważ SQL Server może zatrzymać wykonywanie operacji pobierania wierszy, gdy tylko zostanie znaleziony pierwszy wiersz.
Nie należy używać IF EXISTS z funkcjami agregującymi, ponieważ one zawsze zwracają dane, nawet jeśli wartością danych jest zero. Przykładowo, następujący kod sprawdzający czy wydawca 9933 opublikował jakiekolwiek książki, nie wykona swego działania:
IF EXISTS
(SELECT COUNT(*) FROM titles WHERE PUB_ID = '9933')
To polecenie SELECT zawsze zwróci jeden wiersz. Jeżeli baza danych nie zawiera żadnych książek tego wydawcy, ten jedyny wiersz będzie miał wartość 0, ale IF EXISTS zwróci wartość „prawda”.
Wyrażenia CASE
Programiści często wymagają możliwości stosowania wyrażenia warunkowego w innym poleceniu. Wyrażenie CASE pozwala aby wyrażenia T-SQL były upraszczane dla wartości warunkowych. Wyrażenie to pozwala zwracać różne wartości w zależności od wartości kontrolnej lub warunku.
Składnia prostego polecenia CASE wygląda następująco:
CASE expression
WHEN expression1 THEN expression1
[[WHEN expression2 THEN expression2[ [...]]
[ELSE expressionN]
END
Można używać również znalezionych wyrażeń. Składnia w tym przypadku jest następująca:
CASE
WHEN boolean_expression1 THEN expression1
[[WHEN boolean_expression2 THEN expression2[ [...]]
[ELSE expressionN]
END
Proste wyrażenie CASE porównuje wartość początkową z każdą z wartości na liście i zwraca w wyniku skojarzone z tą wartością wyrażenie. Jeżeli żadne z wyrażeń nie zostanie dopasowane, zwracane jest wyrażenie występujące po słowie kluczowym ELSE.
Teraz warto przeglądnąć wykorzystanie niektórych wyrażeń CASE w praktyce. W następującym przykładzie, dla każdego wiersza w tabeli titles, wartość z kolumny type jest porównywana z każdą wartością z listy CASE. Pierwsze dopasowanie wartości określa, które wyrażenie będzie zwrócone. Wyrażenie wyszukujące CASE zwraca wartość skojarzoną z pierwszym wyrażeniem Boolean z listy, które jest prawdziwe. Jeżeli żadne z wyrażeń nie zwróci prawdy, zwrócone --> zostanie wyrażenie następujące po słowie ELSE[Author:JG] .
SELECT title_id, type = CASE type
WHEN 'business' THEN 'Business Book'
WHEN 'psychology' THEN 'Psychology Book'
WHEN 'mod_cook' THEN 'Modern Cooking Book'
WHEN 'trad_cook' THEN 'Traditional Cooking Book'
WHEN 'popular_comp' THEN 'Popular Computing Book'
WHEN 'undecided' THEN 'No type determined yet'
END
FROM titles
Wyniki powinny wyglądać podobnie jak przedstawione poniżej:
title_id type
-------- ------------------------
BU1032 Business Book
BU1111 Business Book
BU2075 Business Book
BU7832 Business Book
MC2222 Modern Cooking Book
MC3021 Modern Cooking Book
MC3026 No type determined yet
PC1035 Popular Computing Book
PC8888 Popular Computing Book
PC9999 Popular Computing Book
PS1372 Psychology Book
PS2091 Psychology Book
PS2106 Psychology Book
PS3333 Psychology Book
PS7777 Psychology Book
TC3218 Traditional Cooking Book
TC4203 Traditional Cooking Book
TC7777 Traditional Cooking Book
W kolejnym przykładzie, dla każdego z wierszy w tabeli titles, wiele wyrażeń jest wyliczanych. W tym przypadku, wartość price jest porównywana z innymi wartościami. Pierwsze wyrażenie, które jest prawdziwe określa, które wyrażenie będzie zwrócone.
SELECT title_id, cost = CASE
WHEN price < 10 THEN 'Cheap'
WHEN price BETWEEN 10 and 20 then 'Midrange'
ELSE 'Expensive'
END
FROM titles
Wyniki powinny wyglądać podobnie jak przedstawione poniżej:
title_id cost
-------- ---------
BU1032 Midrange
BU1111 Midrange
BU2075 Cheap
BU7832 Midrange
MC2222 Midrange
MC3021 Cheap
MC3026 Expensive
PC1035 Expensive
PC8888 Midrange
PC9999 Expensive
PS1372 Expensive
PS2091 Midrange
PS2106 Cheap
TC3218 Expensive
TC4203 Midrange
TC7777 Midrange
Warto zauważyć, ze pierwszy przykład CASE był jedynie specjalną wersją szukanego wyrażenia CASE. Można przepisać pierwszy przykład następująco i otrzymać te same wyniki:
SELECT title_id, type = case
WHEN type = 'business' THEN 'Business Book'
WHEN type = 'psychology' THEN 'Psychology Book'
WHEN type = 'mod_cook' THEN 'Modern Cooking Book'
WHEN type = 'trad_cook' THEN 'Traditional Cooking Book'
WHEN type = 'popular_comp' THEN 'Popular Computing Book'
ELSE 'No type determined yet'
END
FROM titles
Można używać prostej formy wyrażenia CASE nawet jeśli wszystkie warunki sprawdzają równość. Można również używać dwóch innych funkcji zamiast CASE: COALESCE i NULLIF.
Funkcja COALESCE
Czasem wyrażenie CASE ma następującą formę:
CASE
WHEN expr1 IS NOT NULL THEN expr1
[[WHEN espr2 IS NOT NULL THEN expr2]
[...]]
[ELSE exprN]
END
W tym przypadku, wartość zwracana przez wyrażenie jest pierwszą wartością z listy, która nie jest wartością NULL. Alternatywnym sposobem zapisana tego wyrażenia jest wykorzystanie funkcji COALESCE. W tym przypadku tabela wages (zarobki) posiada trzy kolumny z informacjami o rocznych zarobkach pracownika, płacy za godzinę, wynagrodzenia miesięcznego i prowizji. Jednak, pracownik otrzymuje tylko jeden typ zapłaty i tylko jedna z trzech kolumn posiada wpisaną wartość. Pozostałe dwie kolumny mają wartość NULL. Można uruchomić poniższy kod, aby stworzyć tabelę z wprowadzonymi przykładowymi danymi:
USE pubs
GO
CREATE TABLE tblWages (
intEmpID int NOT NULL,
curHourly money NULL,
curSalary money NULL,
curCommission money NULL,
bigNumSales bigint NULL)
GO
INSERT tblWages VALUES (111, NULL, 52000, NULL, NULL)
INSERT tblWages VALUES (112, 14, NULL, NULL, NULL)
INSERT tblWages VALUES (113, NULL, NULL, 0.15, 50000)
INSERT tblWages VALUES (114, NULL, 73000, NULL, NULL)
INSERT tblWAGES VALUES (115, 4.90, NULL, NULL, NULL)
INSERT tblWages VALUES (116, NULL, 28500, NULL, NULL)
GO
Po utworzeniu tabeli i zapełnieniu danymi, można określić całkowitą kwotę płaconą wszystkim pracownikom korzystając z funkcji COALESCE, aby otrzymać wartości inne od NULL znalezione w kolumnach curHourly, curSalary i curComission:
SELECT intEmpID as "Employee ID", "Total Salary" = Convert(money,
(COALESCE(curHourly * 40 * 52, curSalary, curCommission * bigNumSales)))
FROM tblWages
Employee ID Total Salary
----------- ------------
111 52000.0000
112 29120.0000
113 7500.0000
114 73000.0000
115 10192.0000
116 28500.0000
(6 row(s) affected)
Funkcja NULLIF
Czasem wyrażenie CASE przyjmuje następującą formę:
CASE
WHEN expr1 = expr2 THEN NULL
[ELSE expr1]
END
W tym przypadku wartość zwrócona przez wyrażenie wynosi NULL jeżeli wyrażenia 1 i 2 są równe; w innym przypadku zwracana jest wartość pierwszego wyrażenia.
Załóżmy, że istnieje tabela podobna do tej z poprzedniego przykładu - tabela tblWages, która zawiera dane z poprzedniego przykładu, z wyjątkiem tego, że wartości NULL są w tym przypadku zachowane jako zera. Można uruchomić następujący kod i ustawić wartości na 0 zamiast NULL:
USE pubs
GO
UPDATE tblWages
SET curHourly = ISNULL(curHourly, 0),
curSalary = ISNULL(curSalary, 0),
curCommission = ISNULL(curCommission, 0)
bignumSales = ISNULL(bigNumSales, 0)
Aby używać polecenia NULLIF, potrzeba następującego scenariusza. Jeżeli potrzeba napisać polecenie SELECT które zwraca jak wielu pracowników otrzymuje płacę godzinną, ilu otrzymuje pensję miesięczną a ile zarabia na prowizji, następujący kod nie będzie działał poprawnie:
SELECT Hourly = COUNT(curHourly),
Salary = COUNT(curSalary),
Commission = COUNT(curCommission)
FROM tblWages
Funkcja COUNT zlicza ile wartości pojawiło się w kolumnie, przy czym zera są zliczane jako wartości. Jedyna rzecz nie brana pod uwagę przez funkcję COUNT to wartość NULL. Dlatego, można użyć funkcji NULLIF do zmiany wartości 0 w NULL i pozostawienie bez zmian pozostałych (niezerowych) wartości. Aby otrzymać prawidłowe wyniki z poprzedniego przykładu należy uruchomić kod:
SELECT Hourly = COUNT(NULLIF(curHourly,0)),
Salary = COUNT(NULLIF(curSalary,0)),
Commission = COUNT(NULLIF(curCommission,0))
FROM tblWages
Polecenie WHILE
Język T-SQL posiada konstrukcję WHILE, która pozwala na wykonywanie powtarzane, aż do momentu, gdy jest spełniony odpowiedni warunek. Używanie tej konstrukcji jest ograniczone w wielu przypadkach, ponieważ ze swojej natury, SQL Server działa na zbiorach wierszy. Przykładowo, nie ma potrzeby używania polecenia WHILE, aby przejrzeć wszystkie wiersze w tabeli; polecenie SELECT pozwala przeglądnąć wszystkie wiersze, sprawdzając dla każdego wiersza kryteria zawarte w poleceniu WHERE.
W niektórych sytuacjach potrzebna jest możliwość powtórzenia akcji. Można powtarzać działanie wewnątrz transakcji (nie polecane, ponieważ transakcje powinny być małe i szybkie), w procedurze składowanej lub w funkcji SQL. Polecenia WHILE mogą być użyteczne przy obsłudze kursorów.
Konstrukcja WHILE jest powtarzana, do czasu, aż podany warunek będzie prawdziwy. Podobnie jak w przypadku IF...ELSE, jeżeli warunek zawiera polecenie SELECT, polecenie to w całości musi być ujęte w nawiasy zwykłe.
Następujący przykład wielokrotnie sprawdza średnią cenę wszystkich książek. Dopóki średnia jest mniejsza niż $20, cena każdej z książek jest uaktualniana. Powtarzanie zostaje przerwane gdy średnia cena jest większa lub równa $20.
WHILE (SELECT AVG(price) FROM titles) < $20
BEGIN
UPDATE titles SET price = price * 1.1
PRINT 'all prices have been increased by 10%'
END
Jak w przypadku każdego języka programowania, istnieje zawsze ryzyko, że powtarzanie będzie trwało w nieskończoność. Sprawą programisty jest dopilnowanie, żeby tak się nie stało.
W tym przykładzie, powtarzanie trwa dopóki cena jest mniejsza od określonej kwoty, dlatego wewnątrz pętli potrzebna jest akcja powodująca podniesienie ceny; ostatecznie, cena nie będzie mniejsza niż określona kwota. (Nie będzie to prawdziwe jeżeli wszystkie ceny będą miały wartość $0.00). Jeżeli polecenie UPDATE obniżało by cenę książek (price = price * 0.90), powstałaby nieskończona pętla.
Blokowanie
Blokowanie występuje na ogół w negatywnym kontekście. Blokowanie pomaga utrzymać współbieżność pomiędzy bazami. Często słyszy się, że ktoś wspomina o problemach z blokowaniem, ale rzadko słyszy się o pozytywnych aspektach blokowania; jednak, jest ich wiele. Bez blokowania, SQL Server nie posiadałby mechanizmu do zabronienia wielu użytkownikom modyfikacji określonych danych w tym samym czasie.
Zasadniczo, w SQL Serverze używane są cztery typy blokad:
Można założyć blokadę współdzieloną (shared lock) zwaną też read lock, na dane, które są czytane. Współdzielona blokada nie pozwala innym użytkownikom na zmianę danych, gdy dane są przeglądane. Blokady współdzielone są zgodne z innymi blokadami tego typu.
Można używać blokady wyłącznej (exclusive lock) gdy dane są modyfikowane. Inni użytkownicy nie mogą w tym czasie przeglądać ani modyfikować danych, aż do momentu zwolnienia blokady. Blokady wyłączne nie są zgodne z innymi blokadami.
Blokada update (update lock)jest używana podobnie jak blokada wyłączna. Blokada update nie pozwala innym na modyfikację danych, podczas gdy użytkownik wykonuje proces zmiany danych.
Blokada intent (intent lock)używana jest na obiektach „wyższego” poziomu do określenia, że blokada (opisanego wcześniej typu) została założona na tym obiekcie. Zagadnienie to zostanie krótko omówione.
Blokady update są potrzebne gdy zapytanie przechodzi przez dwie fazy aby zmodyfikować dane: fazę wyszukiwania i fazę modyfikacji. Jeżeli SQL Server używa blokady współdzielonej podczas fazy wyszukiwania, inny użytkownik może również założyć blokadę dzieloną na tym samym obiekcie. Kiedy transakcja wyszukująca próbuje modyfikować dane, wymaga blokady wyłącznej. Inna transakcja mogła już próbować założyć blokadę wyłączną i SQL Server nie przydzieli po raz drugi tej blokady. Dlatego mogą wystąpić sytuacje zablokowania lub zakleszczenia. Aby uniknąć tych sytuacji, należy stosować blokadę update, która chroni przed użyciem blokady wyłącznej przez inne transakcje, na obiektach, które zostały zablokowane do edycji.
Typy blokad
Różne poziomy lub typy obiektów mogą być blokowane:
RID jest innym określeniem dla blokady z poziomu wiersza. RID oznacz Row Identifier. Gdy założona jest blokada RID, w danym czasie blokowany jest jedynie jeden wiersz.
Key jest blokadą na poziomie wiersza która jest zakładana w indeksie. Blokada key blokuje wartość pojedynczego klucza lub wiele wartości klucza (zwana key range lock). Ten typ blokady może pomóc szeregować transakcje w indeksie.
Page jest standardową jednostką 8KB w SQL Serverze. Blokada strony blokuje całą zawartość pojedynczej strony, która może być jednym lub wieloma wierszami.
Blokada wyłączna jest wymagana, gdy dla danego obiektu nie ma więcej dostępnych stron a ma być dodanych więcej danych. Oznacza ona, że potrzeba nowego zestawu ośmiu stron (extent) dla tego obiektu.
Można założyć blokadę tabeli automatycznie lub poprzez proces eskalacji (omówiony później), lub może zażądać blokady bezpośrednio. Wszystkie strony w tabeli są blokowane jako jednostka.
Blokada typu intent jest sposobem określenia na poziomie tabeli, że są założone blokady strony lub wiersza lub na poziomie strony, że ma miejsce blokada wiersza. Przykładowo, jeżeli w tabeli występuje pojedyncza blokada współdzielona strony, na poziomie tabeli jest zakładana współdzielona blokada intent.
W SQL Serverze 2000, blokowanie jest całkowicie dynamiczne. W wersjach SQL Servera przed wersją 7.0, domyślne było blokowanie z poziomu strony. Jednak SQL Server 2000 decyduje, który typ blokady założyć przy optymalizacji zapytań. Dla zapytań, które dotyczą niewielkiej ilości danych lub niewielkich dodawań danych, uaktualnień lub usuwania, zakładane są blokady na poziomie wiersza (i/lub o zasięgu klucza). Dla każdego większego zapytania (takiego jak SELECT * FROM tblLargeTable) używanie blokady na poziomie strony lub nawet tabeli może być bardziej efektywne.
Blokowanie na poziomie wiersza uwzględnia blokowanie pojedynczego wiersza w danym momencie zamiast blokowania strony lub tabeli. Blokowanie na poziomie wiersza może być użyteczne ponieważ pojedyncza strona może zawierać wiele wierszy. Generalnie mówiąc, im mniejsza jednostka do blokowania, tym lepsza współbieżność (zdolność aby wielu użytkowników miało równocześnie dostęp do danych). Kompromisem jest jednak to, że założenie blokad na 5000 wierszy wymaga więcej zasobów, i co za tym idzie więcej czasu, niż założenie blokady na pojedynczej tabeli. Ponieważ blokowanie zakłada pewne kompromisy i nie jest zawsze oczywiste, który typ blokady założyć, lepiej jest zostawić decyzje o wyborze blokady SQL Serverowi.
Kontrola blokowania
Normalnie, nie ma potrzeby rozważać kontrolowania blokad. Dla operacji INSERT, UPDATE i DELETE SQL Server zakłada blokadę wyłączną. Jednak, SQL Server może konfigurować blokady zapytanie po zapytaniu przy pomocy podpowiedzi blokowania dla poleceń SELECT. Można je określić po nazwie tabeli w zapytaniu. Okazjonalnie, można zmienić domyśle zachowanie blokad z powodu problemów z konfliktami transakcji lub blokowaniu ich nawzajem.
Przykładowo, aby wymusić wyłączną blokadę tabeli authors, aby nikt nie mógł modyfikować w tym czasie danych, należy uruchomić zapytanie:
SELECT *
FROM authors (TABLOCKX)
W miejsce TABLOCKX można używać następujących parametrów:
NOLOCK żąda, aby nie były stosowane żadne blokady. (Ten typ odczytu jest również nazywany dirty read). Używanie tej podpowiedzi dla optymalizatora pozwala aby zapytanie czytało dane, które zostały zablokowane do wyłącznego użytku. Używanie tego wprowadza możliwość, że dane, które zostały zmienione, ale nie koniecznie zatwierdzone, mogą być czytane jako część zapytania. Chociaż opcja ta jest pożyteczna w niektórych przypadkach, nie należy jej używać, dopóki nie zrozumie się w pełni zachodzących powiązań.
READUNCOMMITTED jest tym samym co NOLOCK.
READPAST określa, że jeśli niektóre wiersze są zablokowane, a czytane jest kilka wierszy, włączając w to te, które normalnie nie pozwoliłyby na kontynuację (działanie zostałoby „zablokowane” do czasu zwolnienia blokady), wiersze te są opuszczane. Należy ostrożnie używać tej opcji ponieważ może brakować danych w zbiorze wynikowym.
REPEATABLEREAD określa, że blokowanie powinna mieć zastosowanie do poziomu izolacji transakcji REPEATABLE READ. Oznacza to zasadniczo, że podczas czytania danych, założone blokady nie są zdejmowane do czasu zakończenia transakcji; w związku z tym, żaden z wierszy danych, które zostały przeczytane nie może być modyfikowany przez inne transakcje dopóki transakcja nie zakończy się. Dlatego, uaktualnienia i usuwanie danych nie są możliwe, ale dodawanie danych jest dozwolone.
HOLDLOCK żąda, aby założona blokada istniała do zakończenia transakcji. Normalnie, podczas wykonywania polecenia select, blokady współdzielone są zakładane i zwalniane do czasu gdy następny wymagany wiersz lub strona jest pobierana. Z tą opcją, współdzielone blokady nie są zwalniane do czasu zatwierdzenia lub wycofania transakcji. Blokady są również zakładane w ten sposób, że dodawanie danych (insert) również nie jest możliwe. Zasadniczo zachowanie to jest implementowane z blokadami o zasięgu klucza.
SERIALIZABLE jest tym samym co HOLDLOCK.
UPLOCK wymaga raczej blokady update niż blokady współdzielonej. Opcja ta nie jest normalnie używana.
ROWLOCK wymusza użycie blokad na poziomie wiersza
PAGELOCK wymaga blokady współdzielonej strony.
TABLOCK wymaga blokady współdzielonej na poziomie tabeli raczej niż blokowania indywidualnych stron lub wierszy
TABLOCKX wymaga wyłącznej blokady tabeli.
Eskalacja blokad
Eskalacja blokad jest procesem zmiany blokady z niższego poziomu (jak np.: blokada na poziomie wiersza lub strony) na blokadę wyższego poziomu (blokada na poziomie tabeli). SQL Server eskaluje blokady gdy jest zmuszony to zrobić. Użytkownik nie ma kontroli nad tym kiedy występuje ten proces.
Opcja LOCKS
Dla blokad dostępna jest jeszcze jedna opcja konfiguracyjna - opcja LOCKS. Jest to opcja zaawansowana. Domyślna wartość 0 oznacza, że SQL Server dopasowuje liczbę blokad, gdy zmienią się wymagania systemu. Domyślnie, SQL Server przypisuje 2 procent pamięci do używania przez blokady. Można również w razie potrzeby ustawić ręcznie liczbę blokad; jednak, jeżeli zabraknie dostępnych blokad, SQL Server umieści komunikat o błędzie w Windows NT Application Event Log jak i w dzienniku błędów SQL Servera. Wszelkie zdarzenia, które wymagają blokad w systemie zostaną wstrzymane do czasu, aż będzie dostępnych więcej blokad. Każda z blokad wymaga małej ilości pamięci, więc nie należy ustawiać blokad ręcznie ani określać arbitralnie tego parametru jako dużej wartości dopóki nie zrozumie się dobrze wpływu tej zmiany na działanie systemu.
Obserwacja blokad
W celu obserwacji blokad należy uruchomić procedury sp_lock lub sp_processinfo. Następujący przykład pokazuje działanie procedury sp_lock w systemie:
EXEC sp_lock
GO
spid dbid ObjId IndId Type Resource Mode Status
------ ------ ------ ----- ------ -------- ------ -------
1 1 0 0 DB S GRANT
1 1 0 0 DB S GRANT
1 1 0 0 DB S GRANT
1 1 0 0 DB S GRANT
1 1 0 0 DB S GRANT
1 1 0 0 EXT 1:80 X GRANT
1 1 117575457 0 TAB IS GRANT
1 1 0 0 DB S GRANT
(8 row(s) affected)
Jak można zauważyć w wynikach zapytania sp_lock, interpretacja wyników może być trudna. Potrzeba interpretować wyniki przeglądając dbid (identyfikator ID bazy danych) z tabeli sysdatabases w bazie danych master oraz ObjId (ID obiektu) z tabeli sysobjects w odpowiedniej bazie danych. Być może będzie potrzebne przeglądanie IndId (ID indeksu) z tabeli sysindexes w danej bazie. Kolumna type wyjaśnia jaki rodzaj obiektu został zablokowany - baza danych, tabela, extent, klucz, strona czy wiersz (RID). Kolumna Resource informuje co zostało zablokowane w zależności od typu blokady:
EXT (extent) podany jest w formie fileno:Extent. Przykładowo, 1:80 oznacza blokadę zakresu na pliku numer 1 w bazie danych , w zakresie 80.
PAG (page) podany jest w formie fileno:pageno. Przykładowo, 1:2401 oznacza blokadę strony na stronie numer 2401 w pliku numer 1 bazy danych.
RID (row) podany jest w formie fileno:pageno:rowid. Np.: 1:32:16 oznacza plik numer 1, strona 32, wiersz 16 jest zablokowany.
KEY (key range) pokazuje wewnętrzną informacje z SQL Servera, której nie mogą interpretować użytkownicy.
Kolumna Mode oznacza typ blokady ( S - blokada współdzielona, X - wyłączna, I - intent itd.).
Najbardziej istotna jest informacja z kolumny Status. GRANT oznacza, że blokada jest właśnie założona. WAIT oznacza, że blokada nie może być założona ponieważ inny użytkownik założył nie kompatybilną blokadę (zgodność blokad zostanie omówiona w kolejnej sekcji). CNVT pokazuje, że blokada jest konwertowana na inną blokadę (w większości przypadków, jest eskalowana). Z perspektywy blokad można traktować CNVT podobnie jak status WAIT.
Używanie okna SQL Server Enterprise Manager Current Activity może być łatwiejsze. Aby otworzyć te okna, należy rozwinąć nazwę serwera, następnie folder Management i opcję Current Activity. Przy pomocy tej opcji można zobaczyć, które procesy (użytkownicy) są aktywne, jakie blokady zostały założone przez procesy i jakie typy blokad są założone przez obiekty. Rysunek 12.4 pokazuje przykładowe okno.
Rysunek 12.4. Okno Locks/Process ID Current Activity. |
|
Po rozwinięciu opcji Locks/Object, można obserwować w miejsce obiektu blokady (zobacz rysunek 12.5).
Rysunek 12.5. Okno Locks/Object Current Activity. |
|
Zgodność blokad
Blokady mogą być zgodne z innymi blokadami lub nie. Tabela 12.1 pokazuje macierz zgodności blokad.
Tabela 12.1 Zgodność blokad
|
IS |
S |
U |
IX |
SIX |
X |
Intent Shared (IS) |
YES |
YES |
YES |
YES |
YES |
NO |
Shared (S) |
YES |
YES |
YES |
NO |
NO |
NO |
Update (U) |
YES |
YES |
NO |
NO |
NO |
NO |
Intent Exclusive (IX) |
YES |
NO |
NO |
YES |
NO |
NO |
Shared With Intent Exclusive (SIX) |
YES |
NO |
NO |
NO |
NO |
NO |
Exclusive (X) |
NO |
NO |
NO |
NO |
NO |
NO |
Warto zauważyć, że blokady wyłączne (exclusive) nie są kompatybilne z żadnymi blokadami. Po przemyśleniu wydaje się to sensowne. Jeżeli dane są zmieniane, nikt inny nie powinien w tym samym czasie zmieniać tych danych. Z drugiej strony blokady współdzielone są elastyczne z uzasadnionych przyczyn. Jeżeli użytkownik czyta dane, nie ma problemu, żeby te same dane czytał w tym samym czasie inny użytkownik.
Zakleszczenia (deadlocks)
Zakleszczenie jest sytuacją, w której dwie transakcje popadają ze sobą w konflikt i jedynym wyjściem z tej sytuacji jest anulowanie jednej z transakcji. Najlepszym sposobem zrozumienia tego problemu jest analiza przykładu:
Należy utworzyć dwie tabele przy pomocy Query Analyzera i wypełnić je danymi:
USE pubs
GO
CREATE TABLE tblChecking
(intAcctNum INT NOT NULL,
strLastName CHAR(30) NOT NULL,
curBalance MONEY NOT NULL
)
GO
CREATE TABLE tblSavings
(intAcctNum INT NOT NULL,
strLastName CHAR(30) NOT NULL,
curBalance MONEY NOT NULL
)
GO
INSERT tblChecking VALUES (1,'smith', $500.00)
INSERT tblChecking VALUES (2,'Jones', $300.00)
INSERT tblSavings VALUES (1,'smith', $100.00)
INSERT tblSavings VALUES (2,'Jones', $200.00)
GO
Należy otworzyć drugie okno Query Analyzera (z menu plik wybrać Connect). W pierwszym oknie uruchomić kod:
use pubs
GO
BEGIN TRAN
UPDATE tblChecking
SET curBalance = curBalance + $100.00
WHERE intAcctNum = 1
Odpowiedź systemu powinna być następująca:
(1 row(s) affected)
W drugim oknie należy uruchomić kod:
USE pubs
GO
BEGIN TRAN
UPDATE tblSavings
SET curBalance = curBalance - $100.00
WHERE intAcctNum = 2
z tym samym wynikiem.
Następnie, w pierwszym oknie należy uruchomić:
UPDATE tblSavings
SET curBalance = curBalance - $100.00
WHERE intAcctNum = 1
Warto zauważyć, że transakcja nadal jest wykonywana, podczas gdy inne okno ją blokuje. Uruchamiając w trzecim oknie procedurę sp_lock można przeglądnąć następujące wyniki:
spid dbid ObjId IndId Type Resource Mode Status
------ ------ ----------- ------ ---- ---------------- -------- ------
51 5 0 0 DB S GRANT
51 5 773577794 0 TAB IX GRANT
51 5 773577794 0 RID 1:141:0 X GRANT
51 5 789577851 0 RID 1:143:1 U WAIT
51 5 789577851 0 RID 1:143:0 X GRANT
51 5 789577851 0 PAG 1:143 IX GRANT
51 5 773577794 0 PAG 1:141 IX GRANT
51 5 789577851 0 TAB IX GRANT
54 5 789577851 0 PAG 1:143 IX GRANT
54 5 789577851 0 TAB IX GRANT
54 5 789577851 0 RID 1:143:1 X GRANT
54 5 0 0 DB S GRANT
55 1 85575343 0 TAB IS GRANT
Warto zauważyć, że jeden z procesów spid w kolumnie status ma słowo WAIT, oznaczające, że proces czeka aby założyć blokadę i nie może ruszyć, dopóki inny proces (spid) nie zwolni swojej blokady w wymaganych zasobach.
W drugim oknie należy uruchomić kod:
UPDATE tblChecking
SET curBalance = curBalance + $100.00
WHERE intAcctNum = 2
Powinien się pojawić komunikat tego rodzaju:
Server: Msg 1205, Level 13, State 50, Line 1
Your transaction (Proced ID 54) was deadlocked on (lock) resources with anther process and has been chosen as the deadlock victim. Rerun your transaction.
Teraz wykonane jest oryginalne zapytanie. Należy uruchomić następujący „czyszczący” kod w pierwszym oknie:
COMMIT TRAN
GO
DROP TABLE tblChecking
DROP TABLE tblSavings
GO
Unikanie zakleszczeń jest bardzo istotne ponieważ gdy pojawią się zakleszczenia marnowane są czas i zasoby. Jedynym sposobem uniknięcia zakleszczeń jest dostęp do tabel zawsze w tej samej kolejności. W poprzednim przykładzie, jeżeli obydwie transakcje rozpoczęłyby od tej samej tabeli, zakleszczenie by nie wystąpiło. Jedna transakcja czekała na kolejną aby zakończyć przed jej rozpoczęciem. Jeżeli powstanie zakleszczenie, dobrze jest poczekać sekundę lub dwie a następnie powtórzyć transakcję.
Jednym z mitów jest to, że zakleszczenia nie powstaną gdy istnieje blokowanie na poziomie wiersza. Jak można zauważyć z poprzedniego przykładu, zakleszczenia nie mają tak dużo wspólnego z blokowaniem na poziomie wiersza, jak z tym, że do ich wystąpienia potrzeba nieprawidłowych blokad w złym czasie. Ważne jest aby doprowadzić poprzez testy do wyeliminowania z aplikacji tylu zakleszczeń ile jest możliwe.
Poziomy izolacji transakcji
Poziomy izolacji transakcji mają wpływ na domyślne rodzaje i długość trwania blokad założonych podczas polecenia SELECT. Jak zostało wcześniej powiedziane, typy założonych blokad mogą być omijane zapytanie po zapytaniu. Poziom izolacji działa podobnie ale może być ominięty na poziomie sesji (oznacza, że wszystkie zapytania, które są uruchamiane podczas pojedynczej sesji mają ustawioną tę opcję). Aby to zrobić, należy skorzystać z polecenia SET TRANSACTION ISOLATION LEVEL:
SET TRANSACTION ISOLATION LEVEL
{READ COMMITTED | READ UNCOMMITTED | REPEATABLE READ | SERIALIZABLE}
W przypadku opcji READ COMMITTED, która jest wartością domyślną, zapytanie (select) widzi jedynie dane, na których zapytanie może założyć blokadę współdzieloną (nie wykonuje dirty reads).
READ UNCOMMITTED, która jest taka sama jak podpowiedź dla optymalizatora NOLOCK, pozwala na dirty reads we wszystkich zapytaniach w danej sesji. Należy być ostrożnym z tą opcją ponieważ można zobaczyć dane, które nie zostały jeszcze zatwierdzone w bazie danych.
REPEATABLE READ nie zwalnia blokad współdzielonych dopóki transakcja nie została ukończona i jest odpowiednikiem podpowiedzi HOLDLOCK opisanego wcześniej. Nie pozwala na modyfikacje i usuwanie danych, które są odczytywane.
SERIALIZABLE nie tylko nie pozwala na modyfikacje i usuwanie danych, które są odczytywane, ale również nie zezwala na dodawanie danych z określonego zakresie klucza, które są czytane.
Obydwie opcja REPEATABLE READ i SERIALIZABLE mają wyraźny wpływ na współbieżność, dlatego należy dobrze rozumieć konsekwencje używania każdej z opcji zanim się je zaimplementuje.
Aby sprawdzić jaki poziom izolacji jest wymuszony, należy użyć polecenia DBCC USEROPTIONS. Bez uruchomienia polecenia SET TRANSACTION ISOLATION LEVEL, pojawią się wyniki:
DBCC USEROPTIONS
GO
Set Option Value
------------------------------ ------------------
textsize 64512
language us_english
dateformat mdy
datefirst 7
ansi_null_dflt_on SET
(5 row(s) affected)
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
Widać różnicę po włączeniu dirty reads z opcją READ UNCOMMITTED:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
DBCC USEROPTIONS
GO
Set Option Value
---------------------------- ------------------
textsize 64512
language us_english
dateformat mdy
datefirst 7
ansi_null_dflt_on SET
isolation level read uncommitted
(6 row(s) affected)
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\moj dysk\studia\pracamag\materiały\helion\ksiazki\SQL Server 2000 dla kazdego\r12-05.doc
Brak rysunku
Brak rysunku
Brak rysunku
Strona: 18
czy przypadkiem poniższy wydruk nie został błędnie zamieniony w oryginale z występującym póżniej (prawie identycznym) wydrukiem- w którym występuje ELSE?