ado.net tworzenie elastycznych aplikacji cz. 1

Wiadomo powszechnie że programista jest stworzeniem leniwym. Visual studio udostępnia mnóstwo różnych kreatorów które pozwalają na wyklikanie wielu różnych rzeczy. Niestety minusem tego podejścia jest kompletna niewiedza na temat tego jak pewne rzeczy działają. Dodatkowo często natrafimy na różne ograniczenia, które w pewnym momencie sprawią że albo będzie trzeba mocno się nakombinować, albo wręcz trzeba będzie napisać własne rozwiązanie.

Przykład który prezentuje tym razem jest raczej banalny i jest przeznaczony dla początkujących programistów. Podobnie jak udostępniona klasa jodb, jest mocno spolszczony i dodatkowo jest opisana praktycznie każda linia kodu.

Wyobraźmy sobie sytuację że mamy napisać aplikację do wprowadzania danych (na początku załóżmy że te dane nie muszą być walidowane w jakiś sensowny sposób). By sprawę maksymalnie uprościć załóżmy że będziemy dodawać klientów do bazy danych. Ale jak wiadomo wymagania mogą z czasem się zmienić i chcielibyśmy by nasza aplikacja była dość elastyczna.

Stwórzmy na początek bazę danych i tabelę gdzie będziemy trzymać nasze dane:

create database elasticApp;
use elasticApp;
go

create table klienci (
kli_id int not null primary key identity(1,1),
kli_imie varchar(255),
kli_nazwisko varchar(255),
kli_mail varchar(255),
kli_dataDodania datetime default getdate(),
kli_aktywny int default 1
)

Jak widać mamy prostą tabelę. Możemy napisać teraz prostą aplikację która będzie korzystała z jodb,
napiszemy sql z parametrami i można by powiedzieć że jest ok. Jednak jeśli ktoś chciałby byśmy zbierali jeszcze jedno pole np miejsce urodzenia, to musimy dokonać zmiany w bazie danych oraz zmiany w aplikacji (zmiana sql, dodanie kolejnych kontrolek itd).

Możemy też napisać aplikację, która sama zareaguje na zmiany w bazie i nie będziemy musieli nic zmieniać.

Nasza aplikacja może pobierać definicje tabeli z bazy i na tej podstawie generować pola tekstowe i później dynamicznie tworzyć zapytanie SQL by te dane zapisać.


string q = "SELECT top 1 column_name,data_type, character_maximum_length FROM"
+ " information_schema.COLUMNS WHERE table_name= @nazwaTabeli"
+ " ORDER BY ordinal_position";

powyższe zapytanie wklejone w C# i wykonane pozwoli nam na pobranie listy kolumn.

Później na podstawie listy kolumn generujemy textboxy:


//iterowanie po wszystich wierszach w obiektu datatable
// kazdy wiersz odpowiadna jednej kolumnie
//nazwa kolumny jest w column_name
foreach (DataRow dr in opisTabel.AsEnumerable())
{
if (!dr["column_name"].ToString().ToLower().Contains("id") && !dr["column_name"].ToString().ToLower().Contains("datadodania"))
{
Label l = new Label(); // stworzenie nowej labelki (opis tekstowy);
TextBox t = new TextBox(); // stworzenie nowego textboxa (pole tekstowe).
l.Name = "label_" + dr["column_name"].ToString(); //nadanie nazwy labelki
l.Text = dr["column_name"].ToString(); // ustawienie wyswietlanego tekstu
l.Top = 10 + a; // polozenie od gory
l.Left = 10; // polezenie od lewego brzegu formy
l.Show(); // pokazanie labelki*

t.Name = dr["column_name"].ToString(); //nadanie nazwy dla textboxa
t.Top = 10 + a; // polozenie od gory
t.Left = 160; // polezenie od lewego brzegu formy
t.Show();// pokazanie textboxa*

this.Controls.Add(t); // dodanie textboxa do listy kontrolek na formie
this.Controls.Add(l); // dodanie labelki do kontrolki na formie

a += 25; //dodanie 12 do ostatniej wartosci a
}
}
// po petli stworzymy guzik ktory bedzie zapisywał dane do bazy
Button b = new Button(); // stworzenie obiektu guzika
b.Left = 160; // polozenie od lewej
b.Top = 10 + 25 + a; //polozenie od gory
b.Text = "zapisz"; // nadanie etykiety guzika
b.Click += b_Click; //przypisanie eventu klikniecia do metody (ponizej)
b.Show(); //pokazanie guzika*
this.Controls.Add(b); // dodanie go do formy

//* obiekt zostanie pokazany na formie dopiero po tym jak zostanie dodany do kontrolek
// jesli zostala uzyta metoda show, a obiekt nie znajduje sie w kolekcji, nic sie nie pojawi.
}

Mając przygotowaną formatkę musimy jeszcze przygotować event który obsłuży nam zapis do bazy:


//metoda do obslugi zapisu danych
void b_Click(object sender, EventArgs e)
{
//slownik w ktorym bedziemy zbierac dane do zapisania
Dictionary slownik_danych = new Dictionary();
// przeiterowanie sie po textboxach w celu stworzenia slownika z danymi
foreach (Control c in this.Controls)
{
//prawie 'refleksja' sprawdzenie typow do rzutowania
if (c.GetType() == typeof(TextBox))
{
//rzutowanie kontrolki c na textbox
TextBox t = (TextBox)c;

//stworzenie danych w slowniku nazwa kontrolki (nazwa kolumny) oraz wpisana wartosc
slownik_danych.Add(t.Name, t.Text);
}
}

//zapisanie wprowadzonych danych do bazy
int i = jdb.zapisz_dane(slownik_danych, "klienci");
// sorawdzenie czy się udało zapisać dane:
if (i != 1)
{
// jesli nie to pokazujemy ładny komunikat.
MessageBox.Show("Nie udało się zapisać danych, popraw je", "błąd", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}

Oczywiście do tej pory w jdb nie mieliśmy metody która by potrafiła wygenerować odpowiedni SQL do zapisu danych, poniżej metody które pozwolą to osiągnąć:


///

/// wykonuje sql command z zapytaniami insert/update
///

/// sql command /// ilość wierszy
public int zapisz_dane(SqlCommand sqlc)
{
sqlc.Connection = this.polaczenie; // wskazanie polaczenia do bazy danych
return sqlc.ExecuteNonQuery();
}

///

/// pobiera opis wybranej tabeli
///

/// nazwa tabeli /// informacje o kolumnach w kolejnosci w jakiej są wyświetlane w sql server
public DataTable pobierz_opis_tabeli(string nazwaTabeli)
{
//zapytanie do information_schema pobierajace opis tabeli
string q = "SELECT column_name,data_type, character_maximum_length FROM"
+ " information_schema.COLUMNS WHERE table_name=@nazwaTabeli"
+ " ORDER BY ordinal_position";

//zwraca obiekt zwrocony przez metode pobierz dane z parametrem
return this.pobierz_dane(q, "@nazwaTabeli", nazwaTabeli);
}

///

/// metoda do wyznaczenia prefixu w nazwach kolumn
///

/// nazwa tabeli /// pefix uzywany w kolumnach
public string getColPrefix(string nazwaTabeli)
{
//probuje wyznaczyc prefix
try
{
// zapytanie ktore zwroci 1 wiersz z info o kolumnach w tabeli
string q = "SELECT top 1 column_name,data_type, character_maximum_length FROM"
+ " information_schema.COLUMNS WHERE table_name= @nazwaTabeli"
+ " ORDER BY ordinal_position";
//pobieranie danych do datatable
DataTable dt = this.pobierz_dane(q, "@nazwaTabeli", nazwaTabeli);

// stworzenie zmiennej prefix ktora zawiera cala nazwe kolumny z 1 wiersza
string prefix = dt.Rows[0]["column_name"].ToString();
int position = prefix.IndexOf('_'); // wyznaczenie pozycji znaku _ ktory oddziela prefix
prefix = prefix.Substring(0, position); // wyznaczenie napisu przed znakiem _

return prefix; // zwrocenie prefixu
}
// jesli sie nie udalo to zwroci null,
// mozna ustawic breakpoint na e.toString by sprawdzic jaki dokladnie blad wystapil
catch (Exception e)
{
e.ToString();
return null;
}
}

///

/// metoda ktora zwraca liste kolumn w formacie listy stringow
///

/// data table z wynikiem zapytania o opis tabeli /// liste stringow z nazwami kolumn
public List gen_lista_kolumn(DataTable dt)
{
//stworzenie nowej listy
List lista = new List();
//iterowanie po liscie kolumn dostarczonej jako parametr
foreach (DataRow dr in dt.AsEnumerable() ){
//dodaj do listy kolumne column_name z wiersza
lista.Add(dr["column_name"].ToString());
}
//zwroc liste stringow
return lista;
}

///

/// metoda do budowania zapytan typu insert
///

/// slownik z danymi do zapisania w bazie /// nazwa tabeli gdzie dane maja byc zapisane /// obiekt sql command do zapisania
public SqlCommand zbuduj_zapytanie_zapisujace(Dictionary daneWej, string nazwaTabeli)
{
//stworzenie obiektu ktory zostanie zwrocony
SqlCommand komenda_sql = new SqlCommand();
//zmienna kontrolna (wykorzystana pozniej)
int inu = 0;
//wygenerowanie listy kolumn w tabeli
List lista_kolumn = this.gen_lista_kolumn(this.pobierz_opis_tabeli(nazwaTabeli));
//zapytanie sql
string sql = " INSERT INTO " + nazwaTabeli + " (";

//iterowanie po kazdym wystapieniu ze slownika w celu zbudowania listy kolumn
foreach (var c in daneWej)
{
// linq (tutaj troche zbedne poniewaz metoda zostala uproszczona)
var kolatr = from p in lista_kolumn
where p.ToLower() == c.Key.ToLower()
select p;
// powyzsze linq sprawdza czy kolumna (c.key) znajduje sie na liscie kolumn w tabeli
kolatr.ToArray().Length.ToString();

//sprawdzenie czy linq zwrocilo jeden element (po drodze cast na tablice).
if (kolatr.ToArray().Length ==1)
{
//sprawdzenie czy dane do zapisania sa 'sensowne'
if (c.Value != null && c.Value.ToString() != "")
{
//doklejenie odpowiedniego kawalka sql
sql += c.Key + ", ";
//inkrementacja zmiennej kontrolnej
inu++;
}
}
}
// po petli usuwam zbedny przecinek
sql = sql.Substring(0, sql.Length - 2);

//dodaje kolejny kawalek kodu sql
sql += ") VALUES (";

// kolejna iteracja po danych wejsciowych teraz by dodac wartosci do zapytania
foreach (var c in daneWej)
{
// to samo linq co wyzej
var kolatr = from p in lista_kolumn
where p.ToLower() == c.Key.ToLower()
select p;
//sprawdzenie czy linq jest ok | tak jak wyzej
if (kolatr.ToArray().Length == 1)
{
//proba stworzenia odpowiedniego parametru i dodanie go sqlcommand
try
{
//sprawdzenie czy dane do zapisu maja 'sens' (jak wyzej)
if (c.Value != null && c.Value.ToString() != "")
{
//stworzenie nazwy parametru
string paramName = "@" + c.Key;
//dodanie parametru do sql
sql += paramName + ", ";
//stworzenie obiektu parametru
SqlParameter param = new SqlParameter (paramName, c.Value);
//dodanie parametru do sql command
komenda_sql.Parameters.Add(param);
}
}
//jesli sie cos nie powiodlo to mozna ustawic sobie breakpoint by zbadac dlaczego nie dziala.
catch (Exception e)
{
e.ToString();
}
}
}

//usuwam zbedne przecinki
sql = sql.Substring(0, sql.Length - 2);
// zakonczenie zapytania
sql += ")";

//sprawdzenie czy udalo sie dodac chociaz jedna kolumne
//zmienna kontrolna :)
if (inu > 0)
{
// przypisanie sklejonego stringa do komendy
komenda_sql.CommandText = sql;
//przypisanie polaczenia do komendy (teraz komenda jest gotowa do wykonania)
komenda_sql.Connection = this.polaczenie;
return komenda_sql;
}
//jesli nie udalo sie dodac zadnej kolumny to zwracamy null
return null;
}

///

/// zapisuje w bazie korzystajac z dynamicznego tworzenia zapytania
///

/// dane do zapisania /// nazwa tabeli gdzie dane maja byc zapisane /// 1 w przypadku sukcesu, -1 gdy porażka
public int zapisz_dane(Dictionary daneWej, string nazwaTabeli)
{
//proba zapisu
try
{
return this.zapisz_dane(this.zbuduj_zapytanie_zapisujace(daneWej, nazwaTabeli));
}
// jesli sie nie udalo, mozna ustawic break point by zobaczyc co poszlo nie tak
catch (Exception e)
{
e.ToString();
return -1;
}
}

kompletny projekt można ściągnąć tu: ElasticApp1

ADO.NET zapytania parametryzowane – C#

W poprzednim wpisie pokazałem, jak napisać prostą klasę w C#, dzięki której można pobierać wyniki prostych zapytań typu select. W tym wpisie pokażę, jak parametryzować zapytania i jak tego nie robić. Zakładam, że czytając ten post, masz drogi czytelniku jakąś klasę na wzór z poprzedniego posta.

W poprzednim poście aplikacja potrafiła wykonać zapytanie ‘select * from klienci’, a gdybyśmy chcieli pokazać konkretnego klienta? Normalnie w SSMS wystarczy napisać zapytanie

select * from klienci where klient_id = 1

Jeśli powyższe zapytanie jest niejasne, zapraszam do lektury o filtrowaniu danych (kurs sql). Oczywiście można w zapytaniu podstawić jakąkolwiek inną tabelę i kolumnę, przykłady opieram na poprzednich postach (zestawy szkoleniowe).

Powyższe zapytanie zwróci nam jeden wiersz.


Zanim przejdziemy do tak zwanego “kodzenia”, jedna uwaga, zmieniłem komputer, na którym prezentuję kod, więc nazwy aplikacji i niektórych kontrolek i obiektów mogły się zmienić.

Gotową klasę można pobrać tutaj
 

By wykonać przykład, który chcę zaprezentować, musimy mieć projekt, posiadający jedną formę, na której znajduje się guzik oraz datagridview.

Gdy nasze zapytanie wkleimy do kodu, gdzie wykonuje się kod po kliknięciu guzika
private void button1_Click(object sender, EventArgs e)
{
jodb db = new jodb("localhost\\sqlexpress", "zajecia");
dataGridView1.DataSource = db.pobierz_dane("select * from klienci where klient_id=1");
}

Po skompilowaniu, uruchomieniu i kliknięciu guzika pojawi się jeden wiersz w DataGridView:

Oczywiście bez sensu jest zmieniać kod, by zobaczyć inny rekord. Dołóżmy na formę textboxa.

Teraz załóżmy, że użytkownik aplikacji zna numery identyfikacyjne klientów i chce oglądać pojedynczych klientów. Do tego celu będzie w textboxa (pole tekstowe) wpisywał numer klienta.

Najprostszą drogą do osiągnięcia tego celu jest konkatenacja w zapytaniu:
if (textBox1.Text.Length > 0)
{

dataGridView1.DataSource = db.pobierz_dane(“select * from klienci where klient_id=” + textBox1.Text);
}

Powyższy oczywiście zadziała, jeśli użytkownik wpisze liczbę do pola tekstowego. Więc, generalnie nie jest to najlepsze rozwiązanie. Dodatkowo, przy takim rozwiązaniu narażamy się na SQL Injection, użytkownik może dopisać część zapytania, która w skrajnych przypadkach może doprowadzić do katastrofalnych zmian w bazie danych.

Przykład na to, że działa:

Przykład na to, że można popsuć coś w ten sposób:

W między czasie, treść zapytania przeniosłem do zmiennej q, tak by móc zdebuggować program i zobaczyć, jak wygląda zapytanie dostarczane do funkcji. Od razu widać, że taki sposób parametryzacji jest bardzo niebezpieczny.

Aby zrobić to poprowanie, powinniśmy skorzystać z obiektu SqlParameter. Do tego celu najlepiej zmodyfikować naszą klasę do obsługi bazy danych.

Skopiujmy naszą funkcję służącą do pobierania danych i dodajmy argumenty wejściowe:

public DataTable pobierz_dane(string q,string nazwa_p,object wartosc_p)
{
..

}

Teraz musimy dodać Parametr do obiektu SqlCommand, tak aby nasza funkcja wyglądała, np. tak:
public DataTable pobierz_dane(string q,string nazwa_p,object wartosc_p)
{
DataTable dt = new DataTable(); // deklaracja i utworzenie instancji obiektu DataTable o nazwie dt
SqlDataReader dr; // deklaracja obiektu SqlDataReader o nazwie dr
SqlCommand sqlc; // Deklaracja obiektu SqlCOmmand

sqlc = new SqlCommand(q);
// utworzenie instancji SQLCommand ktora ma wykonac zapytanie podane jako parametr
// w zmiennej q

sqlc.Connection = this.polaczenie; // wskazanie polaczenia do bazy danych
SqlParameter par = new SqlParameter(nazwa_p, wartosc_p); // stworzenie nowej instancji parametru
sqlc.Parameters.Add(par); // dodanie utworzonego parametru do sqlCommand

dr = sqlc.ExecuteReader(); //wykonanie zapytanie i utworzenie wskaznika dr
dt.Load(dr); //zaladowanie danych do obiektu DataTAble
return dt; // zwrocenie danych

}

Dodaliśmy te dwie linie:
SqlParameter par = new SqlParameter(nazwa_p, wartosc_p); // stworzenie nowej instancji parametru
sqlc.Parameters.Add(par); // dodanie utworzonego parametru do sqlCommand

Musimy je dodać w miejscu, gdzie już mamy utworzoną instancję obiektu SqlCommand. Kolejność dodawania nie ma znaczenia. Parametry należy dodać do obiektu przed wywołaniem zapytania (metoda ExecuteReader).

Gdy już mamy gotową metodę, możemy zmienić nieco kod w naszej metodzie wywoływanej po kliknięciu guzika:

private void button1_Click(object sender, EventArgs e)
{
jodb db = new jodb("localhost\\sqlexpress", "zajecia");

string q = “select * from klienci where klient_id=@id”;
dataGridView1.DataSource = db.pobierz_dane(q,”@id”,textBox1.Text);
}
}

Jak widać, w zapytaniu pojawił się @id. Jest to wskaźnik parametru i informacja o jego nazwie, w przypadku SQLServer parametry powinny się zaczynać się od @. Teraz pod @id, przy wykonywaniu zapytania zostanie podstawiona wartość z pola tekstowego, jednak środowisko .NET zadba za nas o niezbędne walidacje tak, by wszystko zadziałało jak należy:

Teraz próba sql injection skończy się wyjątkiem:

Nasz kod nie posiada bloków try i catch, więc aplikacja najzwyczajniej w świecie się wywali. Kwestii Obsługi błędów poświecę osobny wpis.

Oczywiście może zdarzyć się sytuacja, że będziemy potrzebowali większej ilości parametrów, do tego celu możemy skorzystać ze słowników zaimplementowanych w języku C#. Potrzebujemy znowu przeciążyć metodę od pobierania danych.

Słownik w C# jest listą par obiektów, Para jest kluczem i wartością (tak samo jak SQLParameter). Oczywiście możemy przekazać od razu listę SQLParameter do naszej funkcji lub gotowy obiekt SQLCommand. Ale pokazana ścieżka wymusza użycie obiektów Sql* w jednym miejscu. Tak więc nasza funkcja może wyglądać tak:

public DataTable pobierz_dane(string q, Dictionary < string,object > lista_parametrow )
{
DataTable dt = new DataTable(); // deklaracja i utworzenie instancji obiektu DataTable o nazwie dt
SqlDataReader dr; // deklaracja obiektu SqlDataReader o nazwie dr
SqlCommand sqlc; // Deklaracja obiektu SqlCOmmand

sqlc = new SqlCommand(q);
// utworzenie instancji SQLCommand ktora ma wykonac zapytanie podane jako parametr
// w zmiennej q

sqlc.Connection = this.polaczenie; // wskazanie polaczenia do bazy danych
SqlParameter par; // deklaracja zmiennej
foreach (KeyValuePair<string, object> parametr in lista_parametrow)
//iteracja po wszystkich elementach slownika
{
par = new SqlParameter(parametr.Key, parametr.Value); // stworzenie nowej instancji parametru
sqlc.Parameters.Add(par); // dodanie utworzonego parametru do sqlCommand
}

dr = sqlc.ExecuteReader(); //wykonanie zapytanie i utworzenie wskaznika dr
dt.Load(dr); //zaladowanie danych do obiektu DataTAble
return dt; // zwrocenie danych

}

Jak widać w pierwszej linii:
public DataTable pobierz_dane(string q, Dictionary< string,object > lista_parametrow )
mamy deklarację zmiennej typu słownikowego: Dictionary< string,object > lista_parametrow

Później dodaliśmy pętlę foreach (co oznacza dla każdego obiektu w liście/słowniku/tablicy etc. wykonaj…)

foreach (KeyValuePair< string, object > parametr in lista_parametrow)
//iteracja po wszystkich elementach slownika
{
par = new SqlParameter(parametr.Key, parametr.Value); // stworzenie nowej instancji parametru
sqlc.Parameters.Add(par); // dodanie utworzonego parametru do sqlCommand
}

Dla każdej pary słownika tworzymy obiekt KeyValuePair, jest to obiekt, potrafiący przechowywać dwie wartości jednocześnie. Jest to po prostu struktura/obiekt, posiadający dwie właściwości (key, value). W pętli powstają nowe instancje obiektu i dodawane są do listy parametrów. Mimo, iż obiekt par za każdym razem – mogłoby się wydawać – będzie nadpisany, o tyle referencje do pamięci będą poprawne i lista parametrów w obiekcie SQLCommand tak naprawdę będzie zawierać pełną listę dostarczonych parametrów. Przeróbmy nasz przykład, by działał ze słownikiem (na razie dodamy tylko jeden parametr).

private void button1_Click(object sender, EventArgs e)
{
jodb db = new jodb("localhost\\sqlexpress", "zajecia");

Dictionary<string, object> lista_par = new Dictionary< string, object >();
lista_par.Add(“@id”, textBox1.Text);
string q = “select * from klienci where klient_id=@id”;
dataGridView1.DataSource = db.pobierz_dane(q,lista_par);
}

Tworzymy obiekt (instancję) słownika i dodajemy do niej jedną parę: klucz i wartość (key,value).
Value jest typem object, co oznacza, że możemy podstawiać tam dowolny typ obiektu, nieważne, czy to będzie data, napis, czy liczba, ten kod zadziała i dane zostaną odpowiednio zwalidowane.

Żeby sprawdzić, czy zadziała to dla większej ilości parametrów, dodajmy jeszcze jedno pole tekstowe. Wyobraźmy sobie, że będziemy porównywać dwóch klientów:

private void button1_Click(object sender, EventArgs e)
{
jodb db = new jodb(“localhost\\sqlexpress”, “zajecia”);

Dictionary<string, object> lista_par = new Dictionary< string, object >();
lista_par.Add(“@id”, textBox1.Text);
lista_par.Add(“@id2”, textBox2.Text);
string q = “select * from klienci where klient_id=@id or klient_id=@id2”;
dataGridView1.DataSource = db.pobierz_dane(q,lista_par);
}
}

Dodajemy jeszcze jeden parametr do zapytania, oraz dodajemy go do słownika, efekt:

Metodyka porównywania wydajności operacji SQL Insert w wybranych RDBMS

Poniżej polska wersja artykułu opublikowanego w PIMR,
oryginalna wersja dostępna tutaj

Wprowadzenie
Kluczową kwestią nabierającą coraz większego znaczenia, braną pod uwagę na etapie projektowania, wytwarzania i eksploatowania bazodanowych systemów informatycznych jest problem wydajności i skalowalności aplikacji[1]. W procesie wieloetapowego projektowania danych generalnie nasza uwaga skupiona jest na ich poprawnym odwzorowaniu zgodnie z rozpoznanymi wymaganiami i z przyjętym architektonicznym modelem danych. Zagadnienie wydajności samo w sobie jest zagadnieniem złożonym albowiem możemy go rozpatrywać w kontekście różnych operacji dokonywanych na bazie produkcyjnej lub też w kontekście procesów tworzenia na jej podstawie baz analitycznych[1][7][8]. Z tego obszaru, dość szerokiego, nakreślonego powyżej autorów zainteresował wpływ istnienia klucza podstawowego oaz sposób jego generowania na wydajność zapisu danych w RDBMS. W podejściu badawczym przyjęto perspektywę, interesującą użytkowników gotowego produktu, jaki jest RDBMS, w związku z tym świadomie pominięto zagadnienia związane z mechanizmami fizycznego rozmieszczenia danych na dysku.
Metodyka

Próba odpowiedzi na pytanie na ile obecność klucza podstawowego oraz sposób jego tworzenia wpływa na zapis danych w relacyjnych wymagało opracowania metodyki i wytworzenia niezbędnych narzędzi programistycznych.
Na etapie początkowym przyjęto szereg założeń, których zachowanie powinno eliminować wpływ czynników zakłócających pomiar. Między innymi zdecydowano się, iż aplikacja powinna działać całkowicie po stronie serwera bazodanowego. Tym samym wyeliminowano by wpływ interfejsów programistycznych dostępu do danych typu ADO, ADO.NET itp. na czas zapisu rekordów. Konsekwencją tej decyzji było zwrócenie naszej uwagi tylko na te RDBMS, których wewnętrzny język jest językiem proceduralnym.
Wytworzona procedura, względnie procedury stanowiące podstawowe narzędzie badawcze, powinny tworzyć strukturę tabeli, generować rekordy, a następnie je zapisywać. Równocześnie należałoby rejestrować czas rozpoczęcia i zakończenia zapisu danych z jednoczesnym odnotowywaniem liczby rekordów osadzonych w bazie. Powyższa druga porcja informacji, potrzebna do analiz, gromadzona byłaby w oddzielnej tabeli.
Skoro podstawowym celem autorów było uzyskanie odpowiedzi na pytanie na ile obecność klucza głównego i sposób jego generowania ma wpływ na szybkość zapisu zdecydowano się na utworzenie szeregu niezależnych procedur, osobno dla każdego przewidywanego wariantu. Wspólnym elementem byłby zapis wyników eksperymentu do tej samej tabeli. Przyjęto następujące przypadki, które wiązały się rodzajem informacjami zapisywanych w bazie:
• dane bez klucza podstawowego,
• dane z kluczem głównym, którego wartości generowane są przez RDBMS,
• dane z kluczem głównym, gdzie wartości są generowane na podstawie algorytmu użytkownika.
Procedury to realizujące powinny być zaimplementowane w co najmniej w dwóch RDBMS. Podjęto decyzję, iż będzie to SQL Server 2008, Oracle XE i MySQL. Podstawą wspomnianej decyzji była popularność wykorzystania tych narzędzi informatycznych. I tak dla przykładu na polskim rynku darmowa wersja MSSQL jest wykorzystywana między innymi w znanych programach: Płatnik, MicroSubiekt. Z kolei jego większy brat (płatny) stosowany jest między innymi w jednym z najpopularniejszych programów typu ERP na naszym rynku tj. CDN XL. Baza Mysql skolei dominuje na rynku aplikacji interneotwych, większość skryptów obsługujących fora to tandem Apache, PHP wraz z MySQL. Oprogramowanie bazodanowe dostarczane przez Oracle ma jedną z najlepszych opinii w środowisku IT, dlatego też darmowa wersja (XE) została poddana analogicznym testom jak MSSQL i MySQL.
W prezentowanej metodyce przyjęto jeszcze jedno założenie, z którym w praktyce bazodanowej bardzo często mamy do czynienia, polegające na tym, iż klucz podstawowy jest jedno elementowy, przyjmujący wartości typu int.
Mimo, że zaproponowana metodyka całościowo nie pokrywa się z operacjami przebiegającymi w rzeczywistości, to zdaniem autorów jest dobrym narzędziem do realizacji badań porównawczych. Świadomie zrezygnowano w rozważaniach i badaniach z specjalistycznych narzędzi służących do ładowania danych (typu ETL) wykorzystywanych w tworzeniu hurtowni danych, skupiając się głównie na rozwiązaniach bazodanowych OLTP.

Narzędzie badawcze – procedury

Sygnalizowane powyżej procedury, stanowiące narzędzie badawcze, wytworzono wpierw w języku T-SQL, który jest językiem wbudowanym w SQL Server 2008R2. Pozwala on nie tylko na manipulowanie danymi (składowe języka DML – Data modyfication Language) ale również na definiowanie struktury (składowe języka DDL – Data definition Language). W zaprezentowanej poniżej procedurze, dotyczące przypadku zapisu danych w tabeli bez zdefiniowanego klucza podstawowego, wykorzystano obie wspomniane możliwości języka T-SQL. Podjęto również decyzję o zapisie wyników cząstkowych eksperymentu w dodatkowej tabeli. Do utworzenia agregatów, które tworzą wyniki cząstkowe, odnotowane w tabeli wykorzystano pytanie grupujące wraz z funkcją DateDiff. Grupę tworzą rekordy, które zostały zapisane do bazy w tym samym przedziale czasowym wynoszącym jedna sekunda. Ten sposób grupowania jest możliwy, albowiem jedno z pól tabeli podstawowej (simple_insert_table) zawiera datę i czas systemowy zwróconą przez funkcję GetDate.

CREATE PROCEDURE [dbo].[simple_insert]
@numerow int=1000;
@numtries int= 100;
AS
BEGIN
SET NOCOUNT ON;
declare @testb datetime;
declare @tests datetime;

create table simple_insert_table (
a int, b varchar(10), c datetime );

declare @numtries1 int=@numtries;
declare @numerow1 int=@numerow;

begin transaction;
set @testb = GETDATE();
while @numtries >0
begin
set @numerow = @numerow1;
while @numerow >0
begin
insert into simple_insert_table values (1,’1234567890’,GETDATE());
set @numerow = @numerow -1;
end;
set @numtries = @numtries -1;
end;
set @tests=GETDATE();

insert into test_results (test_name,start_d,stop_d,param1,param2)
values (‘simple insert test’,@testb,@tests,@numerow1,@numtries1);

set @numerow = @@IDENTITY;
insert into test_subresult (test_id, ins_num, time_agr)
select @numerow,count (DATEDIFF(S, ‘19700101’, c)) e, DATEDIFF(S, ‘19700101’, c) from
simple_insert_table
group by DATEDIFF(S, ‘19700101’, c);
insert into test_results (test_name,param1,param2,param3,param4,param5)
select ‘statistic for simple insert: ‘+ cast(@numerow as varchar),exec_per,max_ins,avg_ins,min_ins,stddev_ins from dbo.simple_insert_stat;
drop table simple_insert_table;
commit transaction;
END

Niewielkie zmiany w prezentowanym kodzie pozwoliły na zbudowanie nowych procedur, które stanowiły narzędzie testujące dla dwóch kolejnych przypadków. W pierwszym wariancie klucz był generowany przez algorytm zawarty w procedurze. Zmienna wykorzystywana do przekazywania wartości generowanego klucza do zapytania, stanowiła równocześnie składową warunku, decydującego o liczbie wykonanych pętli. W drugim przypadku do generowania klucza wykorzystano odpowiednie narzędzia serwera bazodanowego. W przypadku SQL Server 2008R2 wykorzystano mechanizm identity, natomiast jego odpowiednikiem na poziomie MySQL był autoincrement. Z kolei w RDBMS Oracle wykorzystano mechanizm sekwencji. Ingerencja w wspomniane narzędzia dotyczyła tylko ustawień początkowych, obejmujących wartość startową oraz skok, które we wszystkich przypadkach były identyczne. Celem zapewnienia możliwości dokonania uogólnień omawiany algorytm został zaimplementowany w języku PL/SQL oraz w proceduralnym rozszerzeniu języka SQL dla bazy MySQL 5.5.8, co pozwoliło na przeprowadzenie testów z wykorzystaniem innych RDBMS.
W trakcie podjętych przez autorów badań pojawiła się, co prawda nowa wersja SQL Server 2011 wyposażona tym razem w dwa mechanizmy, pozwalające na automatyczne tworzenie klucza podstawowego, lecz nie jest to wersja ostateczna, w związku z powyższym nie stała się ona przedmiotem eksperymentów. Tym dodatkowym narzędziem umożliwiającym automatyczne generowanie klucza na poziomie wspomnianego RDBMS jest mechanizm sekwencji [8].
Badania i wyniki

Badania przeprowadzono w wirtualnym środowisku, opartym o system operacyjny Windows XP Home Edition wraz z najnowszymi aktualizacjami. Instalacja miała charakter standardowy podobnie, jak późniejsza instalacja RDBMS (zaaprobowano wszystkie ustawienia kreatora). Po zakończeniu testów danego RDBMS następowała reinstalacja środowiska badawczego. Każdy z testów został także wykonany na maszynie serwerowej podpiętej do macierzy w środowisku Windows Server 2008 ( z wyłączeniem testów bazy Oracle). Procedury dla bazy Oracle zostały przetestowane na serwerze z systemem operacyjnym Ubuntu Server 11 także podpiętym do wydajnej macierzy. Wyniki z środowisk serwerowych były analogiczne do maszyny badawczej (relacje wyników testów dla danego RDBMS).
Powszechnie uważa się iż wyłączenie ograniczeń (np. Klucza podstawowego) podczas wstawiania dużej ilości danych pozwala zwiększyć wydajność zapisu. Opisywany eksperyment pozwala ocenić czy wskazana operacja jest wstanie przynieść realne zyski.
W celu przeprowadzenia testów potrzebne było utworzenie w każdym badanym RDBMS pustej bazy danych, również zgodnie z ustawieniami sugerowanymi przez kreatora. Dało to podstawy do osadzenia w niej procedur testujących z jednoczesnym utworzeniem struktury relacyjnej do gromadzenia wyników badań.
Test przeprowadzono dla każdego z RDBMS dla wyszczególnianych poniżej wariantów, podczas których wprowadzono do bazy 106 rekordów:
• wstawiano dane bez klucza głównego,
• wstawiano dane tworzące klucz główny z poziomu procedury
• wstawiono dane zawierające klucz główny utworzony przy użyciu mechanizmów RDBMS (autoincrement, identity, sequence).

Efekty przeprowadzonych badań zaprezentowano w formie tabelarycznej tab. 1, 2, 3 i 4. Zawierają one wielkości względne, a punktem ich odniesienia są wyniki uzyskane dla pierwszego wariantu badań. Powyższy sposób postępowania zastosowano do rezultatów uzyskiwanych dla każdego z badanych RDBMS. W przypadku MySQL 5.5.8 badania przeprowadzono dla dwóch różnych silników INNODB oraz MyISAM, dostępnych dla tego RDBMS. Natomiast niecelowym, zdaniem autorów było umieszczenie względnych wyników badań uzyskanych dla przypadku zapisu rekordów pozbawionych klucza podstawowego.

Omówienie wyników

Zaprezentowane wyniki badań zapisu danych zawierających i niezawierających klucz podstawowy niewiele się różnią się między sobą w przypadku SQL Server 2008 R2. Dotyczy to zarówno sytuacji, gdy klucz jest tworzony przez RDBMS, jak i przez algorytm zaszyty w procedurze testującej. Opóźnienie zapisu danych wyposażonych w klucz podstawowy, przy przyjęciu prędkości zapisu 5000 wierszy na sekundę oraz liczbie rekordów 106, wynosi 10 sekund. Ważnym odnotowania jest również fakt, iż w trakcie badań nie zaobserwowano spadku prędkości zapisu wierszy wraz ze wzrostem liczby rekordów zawartych w tabeli. Zbliżoną prawidłowość zauważono w przypadku MySQL 5.5.8 wyposażonego w silnik InnoDB z tym, że okazał się on bardziej wrażliwy na sposób generowania klucza podstawowego. Drugą dość zaskakującą zależnością, wynikającą z otrzymanych wyników dla tego RDBMS jest wzrost względnej prędkości zapisu danych, zawierających klucz podstawowy. Dotyczy to zarówno sytuacji gdy klucz główny jest generowany przez system bazodanowy, jak i procedurę. Równoległym faktem, wymagającym wyjaśnienia są zerowe wartości prędkości minimalnej uzyskiwane w trakcie badań MySQL 5.5.8[5]. Skutkowało to znacznym wzrostem odchylenia standardowego oraz tym, że minimalna prędkość względna przyjmowała wartości nieokreślone.
Tego rodzaju prawidłowości nie stwierdzono przy użyciu wcześniejszego silnika MyISAM MySQL 5.5.8 [4]. Odnotowane w tym przypadku tendencje, będę konsekwencjami pomiarów są zgodne z rezultatami uzyskanymi dla Oracle XE, które potwierdzają dotychczasowe przekonanie, że wprowadzenie klucza spowalnia zapis nowych wierszy do tabeli. Należy jednak przypomnieć o istotnych wadach silnika MyISAM, jakim jest brak możliwość tworzenia transakcji i definiowania więzów integralności referencyjnej [6].
Podsumowanie

Współczesne RDBMS bardzo się różnią pod względem implementacji modelu relacyjnego, co utrudniania formułowanie uogólnień i reguł dotyczących wydajności tych systemów informatycznych. W pewnych wypadkach rozwiązania intuicyjne, czy będące efektem dotychczasowych doświadczeń z RDBMS mogą się okazać nieefektywne przy pracy z nowymi systemami bazodanowymi, dlatego autorzy zalecają testowanie proponowanych rozwiązań zwłaszcza przed rozpoczęciem prac rozwojowych tworzonego oprogramowania (zwłaszcza w przypadku tworzenia struktur OLAP). Przeprowadzone badania wraz z dokonaną analizą wyników, z użyciem trzech różnych RDBMS skłoniły autorów do sformułowania następujących uwag i wniosków:
• Wyposażenie danych w klucz podstawowy ogranicza z reguły wydajność zapisu rekordów w każdym badanym RDBMS z wyłączaniem MySQL 5.5 zawierającego silnik InnoDB. Spadek tej wydajności jest zróżnicowany lecz z perspektywy SQL Server 2008R2 jest on mało znaczący.
• Mechanizm generowania klucza podstawowego wbudowany w RDBMS z perspektywy wydajności zapisu nowych wierszy jest generalnie rozwiązaniem lepszym od własnych rozwiązań programistycznych zaszytych w procedurach.
• Wskazanym wydaje się podjęcie dalszych wysiłków poznawczych, zmierzających do wyjaśnienia nietypowego zachowania się MySQL 5.5.8 z silnikiem InnoDB z perspektywy prędkości zapisu danych pozbawianych i wyposażonych w klucz podstawowy.
• Uzyskane całkowite czasy zapisy danych w analizowanych wariantach i w różnych RDBMS i przy zaproponowanej metodyce badań wskazują, że z perspektywy aplikacji OLTP korzyści wydajnościowe, wynikające z niestosowania jednoelementowego klucza podstawowego są mało znaczące.
Bibliografia

[1] Beynon-Davies – Database Systems 2003
[2] Joe Celko – SQL for Smarties, Advanced SQL Programming, 3 Edition 2005
[3] MySQL Reference Manual – http://dev.mysql.com/doc/refman/5.5/en/mysql-nutshell.html 2011
[4] MySQL MyISAM Storage Engine Manual – http://dev.mysql.com/doc/refman/5.0/en/myisam-storage-engine.html
[5] MySQL InnoDB – http://dev.mysql.com/doc/refman/5.5/en/innodb-storage-engine.html
[6] MySQL refman – http://dev.mysql.com/doc/refman/5.0/en/ansi-diff-foreign-keys.html
[7] Paulraj Ponniah – Data Warehousing Fundamentals for IT Professionals 2010
[8] SQL Server perfomance – http://www.sql-server-performance.com/articles/dev/sequence_sql_server_2011_p1.aspx 2011
[9] Chris Todman – Designing a data warehouse: supporting customer relationship management 2003

Tworzenie baz danych w SQL SERVER

W RDBMS które obsługują wiele baz danych na jednej instancji, mamy do dyspozycji polecenie create database, które to tworzy pustą bazę danych:

create database nowa_pusta;

gdy chcemy zacząć korzystać z nowo utworzonej bazy należy się na nią przełączyć ( wybrać ją).
W przypadku MySQL oraz SQLServer wykorzystujemy do tego celu polecenie use.
use nowa_pusta -- by korzystac z bazy utworzonej

Oczywiście mamy możliwość dodawania wielu dodatkowych parametrów wpływających na ustawienia bazy.
W przypadku SQL Server, za pomocą języka T-SQL możemy takie parametry odpowiednio modyfikować:


create database nowa_pusta on
( NAME = N'nowa_pusta', FILENAME = N'C:\temp\nowa_pusta.mdf' , SIZE = 167872KB , MAXSIZE = UNLIMITED, FILEGROWTH = 16384KB )

W powyższym przykładzie zostanie utworzona baza danych z plikiem o rozmiarze 168MB, W ten sposób system alokuje odpowiednią ilość miejsca na dysku. Wspomaga to wydajność zapisu danych. Pozostawienie MAXSIZE z wartością unlimited może skutkować wyczerpaniem miejsca na dysku.

create database nowa_pusta on
( NAME = N'nowa_pusta', FILENAME = N'C:\temp\nowa_pusta.mdf' , SIZE = 167872KB , MAXSIZE = UNLIMITED, FILEGROWTH = 16384KB )
LOG ON
( NAME = N'nowa_pusta_log', FILENAME = N'C:\temp\nowa_pusta.ldf' , SIZE = 2048KB , MAXSIZE = 2048GB , FILEGROWTH = 16384KB )

powyższy przykład utworzy bazę danych ze ściśle zdefiniowanymi plikami danych oraz logów. w Pliku LDF przechowywane są informację odnośnie wykonywanych(i wykonanych) transakcji. W przypadku problemów przy zatwierdzaniu transakcji RDBMS (SQL Server) wspomaga się tym plikiem by wykonać rollback oraz przywrócić bazę do stanu spójności.

Można utworzyć bazę z wieloma plikami przechowującymi dane.

create database nowa_pusta on primary
( NAME = N'nowa_pusta', FILENAME = N'C:\temp\nowa_pusta.mdf' , SIZE = 167872KB , MAXSIZE = UNLIMITED, FILEGROWTH = 16384KB )
,FILEGROUP [nowa_pusta2] ( NAME = N'nowa_pusta2', FILENAME = N'C:\temp\nowa_pusta2.mdf' , SIZE = 50MB, MAXSIZE = UNLIMITED, FILEGROWTH = 5MB )
LOG ON
( NAME = N'nowa_pusta_log', FILENAME = N'C:\temp\nowa_pusta.ldf' , SIZE = 2048KB , MAXSIZE = 2048GB , FILEGROWTH = 16384KB )

teraz tworząc tabele możemy je utworzyć w 2 różnych plikach, możemy w ten sposób np rozmieszczać mniej istotne dane na wolniejszych napędach.

create table nazwa (id int,nazwa varchar(255)) on primary

wykonanie tego polecenia będzie skutkować utworzeniem tabeli w pliku który został podany jako pierwszy podczas tworzenia bazy danych. W przypadku pominięcia informacji gdzie ma być utworzona tabela, będzie to równoznaczne z utworzeniem jej w pierwszym pliku. Warto dodać iż standardowo jest tworzony jeden plik bazy danych.


create table nazwa (id int,nazwa varchar(255)) on nowa_pusta2

skutkuje utworzeniem tabeli w pliku należącym do grup nowa_pusta2. Mimo iż każda grupa(filegroup) może zawierać wiele plików, w poleceniach SQL, można przypisywać tylko do poziomu grupy plików(przynajmniej z tego co mi wiadomo).

Wiersz, krotka, rekord w bazach danych

Wiersz/Krotka/Rekord w bazie danych jest konstrukcją analogiczną do rekordu w języku programowania. Posiada strukturę wewnętrzną tj. podział na pola o określonym typie. Rekordem może być wiersz pliku tekstowego, a pola mogą być określone poprzez pozycję w wierszu lub oddzielane separatorami. W relacyjnych bazach danych rekord to jeden wiersz w tabeli, czyli jedna krotka w relacji. Podczas przetwarzania wyników zapytań do bazy danych, które mogą zawierać połączone dane z kilku tabel, pojedynczy wiersz również jest nazywany rekordem. W bazach analitycznych w przypadku kostek OLAP można spotkać się określeniem krotki jako komórki zwracanej w wyniku zapytania (normalnie krotka odpowiada wierszowi/rekordowi).

widok – perspektywa

Widok (perspektywa) to logiczny byt (obiekt), osadzony na serwerze baz danych. Umożliwia dostęp do podzbioru kolumn i wierszy tabel lub tabeli na podstawie zapytania w języku SQL, które stanowi część definicji tego obiektu. Przy korzystaniu z widoku jako źródła danych należy odwoływać się identycznie jak do tabeli. Operacje wstawiania, modyfikowania oraz usuwania rekordów nie zawsze są możliwe ( np. w sytuacji gdy widok udostępnia część kolumn dwóch tabel tb_A oraz tb_B bez kolumny z kluczem głównym tabeli tb_B ). W niektórych SZBD widok służy tylko i wyłącznie do pobierania wyników i ograniczania dostępu do danych.

Encja

Encja (ang. entity) w bazach danych to reprezentacja obiektu (grupy obiektów) Formalnie jest to pojęcie niedefiniowalne, a podstawową cechą encji jest to, że jest rozróżnialna od innych encji (założeniem modelu relacyjnego jest unikalność encji).
Przykłady encji (i atrybuty w encji):
• Osoba (imię, nazwisko, PESEL)
• Pojazd (wysokość, szerokość, długość, sposób poruszania się)
Charakterystyczną cechą encji jest to, że włącza ona do swojego obszaru znaczeniowego obok obiektów fizycznych również obiekty niematerialne. Encja może stanowić pojęcie, fakt, wydarzenie (np. konto bankowe, którego atrybuty to np. numer, posiadacz, dopuszczalny debet itp.; konferencja, której atrybuty to np. temat, data, organizator itp.; wypożyczenie książki, z atrybutami np. imię i nazwisko wypożyczającego, numer karty bibliotecznej, data wypożyczenia itp.).
W pewnych kontekstach encja ma znaczenie bliższe tabeli przechowującej dane, a czasem jest utożsamiana z wystąpieniem danego obiektu w tabeli(instancja obiektu).

Na podstawie polskiej wiki, z moimi drobnymi zmianami.

Instancja bazy danych

Instancja bazy danych to byt nadrzędny utożsamiony z oprogramowaniem zarządzającym przetwarzanymi danymi. W przypadku Oracle instancja utożsamiana jest z jedną logiczną bazą danych. Instancja bazy danych Oracle składa się ze struktur pamięciowych, do których dostęp mają wszystkie procesy ją obsługujące oraz ze struktur prywatnych, dostępnych tylko dla procesów, które te struktury za-alokowały.
Można powiedzieć że Instancja Oracle składa się z struktur pamięciowych i procesów systemu operacyjnego obsługujących bazę danych. W przypadku SQL Server Instancja może obsługiwać wiele logicznych baz danych. Podobnie jak w SQL Server również MySQL pozwala na przechowywanie wielu logicznych baz danych. Konsekwencją takiego podejścia wspomnianych RDBMS jest możliwość zarządzania uprawnieniami i polityką kopii bezpieczeństwa na nieco innym poziomie (logiki danych) niż w przypadku Oracle.
W kontekście zbiorów danych instancją obiektu możemy nazwać jego wystąpienie. W przypadku baz danych dość częstym przypadkiem(niekoniecznie prawidłowym) jest fakt zapisu tego samego obiektu w różnych stanach(instancjach) mimo iż poprawność logiczna danych jest zachowana na poziomie jednostkowym może ona później zaburzać procesy analityczne.

SSMS – Denali CTP3 – sql Server 2011

Niestety Intelisense (podpowiedzi składni) nie działają zbyt stabilnie, widać że jest to element który uległ zmianie w stosunku do CTP1.

zrobiłem krótki screencast:
tutaj

Jak widać praca staje się praktycznie nie możliwa. Szkoda w CTP1 to działało…

Po wyłączeniu Intelisense SSMS zaczyna pracować stabilnie, zgłosiłem to jako BUG to MS.
Po odinstalowaniu CTP1 i CTP3, reinstalacji samego CTP3, problem pozostaje.

SQL Server 2011 aka Denali CTP3 (vs CTP1)

w lipcu firma z Redmont wypuściła kolejna beta wersję SQL Server o kodowej nazwie Denali. Pobrałem ją w nadziei iż irytującę błędy i niedoróbki w SSMS zostały poprawione. Mimo iż CTP3 zostało wydane ok 30 dni temu, już podczas instalacji trzeba pobrać ok 500MB poprawek. Niestety o ile CTP1 działało dość sprawnie i wszystko odbyło się bez problemów o tyle CTP3 mnie zawiodło. Instalacja nowej wersji podmieniła wersje SSMS (zamiast doinstalować kolejną – oczywiście mogłem dać ciała w Setupie). Dodatkowo podmieniły się jakieś biblioteki. Podłączając się z SSMS od CTP3 do instancji CTP1 już na starcie mamy problemy z kompatybilnością. Nie działa chociażby listowanie tabel, oraz wiele operacji (np. Backup). Na szczęście da się wykonać operację attach(załączenia) bazy z CTP1 na instancję CTP3.
W moim przypadku na środowisku windows 2008R2, SSMS z CTP3 zdążyło się już nieoczekiwanie zamknąć, oraz dość często rzuca komunikatem o błędach (Exceptions). W mojej ocenie jest mniej stabilne niż CTP1.
Teraz trochę o plusach, rzuca się w oczy ulepszone podpowiadanie składni. Oczywiście dodane wiele opcji i wykonano wiele modyfikacji w samym SQLServer (czego jeszcze nie miałem czasu testować).