All posts by Piotr Zieliński

Testy UI: Wzorzec PageObject

O testach UI, szczególnie w SpecFlow pisałem już kilka razy. Zawsze korzystałem z wzorca PageObject, chociaż nie wiedziałem, że ma on swoją nazwę. Czasami mam wrażenie, że na proste rzeczy wymyśla się “wzorce”. Muszę przyznać jednak, że  uproszcza to często komunikację między programistami. Wzorce w końcu stanowią pewnego rodzaju słownictwo dla programistów. Zamiast opisywać coś w kilku zdaniach, można powiedzieć po prostu nazwę wzorca.

PageObject pattern polega na tym, że pisząc testy jednostkowe, opakowujemy stronę w obiekty C#.  Początkujący programiści często piszą testy  w stylu:

        [Test]
        public void Query_ShouldBe_Persisted_When_SearchButtonIsClicked()
        {
            IWebDriver driver = new ChromeDriver();
            driver.Navigate().GoToUrl("http://www.bing.com");

            var textBox = driver.FindElement(By.Id("sb_form_q"));
            textBox.SendKeys("piotr zielinski c#");

            var searchButton = driver.FindElement(By.Id("sb_form_go"));
            searchButton.Click();

            string query = driver.FindElement(By.Id("sb_form_q")).GetAttribute("value");
            Assert.That(query,Is.EqualTo("piotr zielinski c#"));
        }

Test jest trudny w zrozumieniu oraz czasochłonny w utrzymaniu. Idealny test powinien być czytany jak skrypt opisujący kolejne akcje. W PageObject opakowujemy strony w obiekty c#. W naszym przypadku stworzymy dwa obiekty, SearchForm oraz SearchResults:

   public class SearchForm
    {
        private readonly IWebDriver _webDriver;

        public SearchForm(IWebDriver webDriver)
        {
            _webDriver = webDriver;
        }

        public SearchResults Search(string query)
        {
            var textBox = _webDriver.FindElement(By.Id("sb_form_q"));
            textBox.SendKeys(query);

            var searchButton = _webDriver.FindElement(By.Id("sb_form_go"));
            searchButton.Click();

            return new SearchResults(_webDriver);
        }        
    }
    public class SearchResults
    {
        private readonly IWebDriver _webDriver;

        public SearchResults(IWebDriver webDriver)
        {
            this._webDriver = webDriver;
        }

        public string GetQuery()
        {
            return _webDriver.FindElement(By.Id("sb_form_q")).GetAttribute("value");
        }
    }

Test zatem wygląda teraz następująco:

            IWebDriver driver = new ChromeDriver();
            driver.Navigate().GoToUrl("http://www.bing.com");
            const string query = "Piotr zielinski c#";
          
            var searchForm=new SearchForm(driver);
            var searchResults = searchForm.Search(query);

            Assert.That(searchResults.GetQuery(), Is.EqualTo(query));

Zwykle opakowuję również sterownik czyli interfejs IWebDriver. Można stworzyć obiekt o nazwie np. BingWebsite który będzie stanowił bramkę do wszelkich testów. Celem jest, aby jakikolwiek kod Selenium nie znajdował się bezpośrednio w teście. Dzięki temu, ten sam kod może być wykorzystywany w różnych testach. Ponadto, przy skomplikowanych testach, kod jest dużo prostszy w rozumieniu ponieważ stanowi skrypt bez magicznych zmiennych.

tSQLt – testowanie wyzwalaczy

O tSQLt pisałem już wiele razy. Dzisiaj chciałbym pokazać jak testować wyzwalacze (triggers). Załóżmy, że mamy następującą tabelę:


CREATE TABLE [dbo].[Articles](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Price] [int] NULL,
 CONSTRAINT [PK_Articles] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Jest to prosta tabela z kluczem głównym oraz jedną kolumną (cena). Napiszmy wyzwalacz, w którym za każdym razem, gdy dodajemy nowy wiersz, cena jest podwajana i kolejny wiersz jest dodawany:

CREATE TRIGGER [dbo].[OnArticleInserted]  
ON [dbo].[Articles]  
AFTER INSERT
AS    
  INSERT INTO Articles (Price) (SELECT Price*2 from inserted)

Nasz test z kolei będzie wyglądać następująco:

ALTER PROCEDURE [sampleTests].[testTrigger]
AS
BEGIN
  --Assemble
  exec tSQLt.FakeTable 'Articles'
  exec tSQLt.ApplyTrigger 'Articles', 'OnArticleInserted'

  create table ExpectedValues
  (
     Price integer
  )

  INSERT INTO ExpectedValues (Price) Values (6),(12)
  
  --Act
  INSERT INTO Articles (Price) Values (6)
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'Articles'
  
END

Zawsze chcemy odizolowywać tabele. Z tego względu używamy FakeTable na “Articles”. W naszym przypadku, wyzwalacz operuje tylko na tej tabeli więc to wystarczy. W praktyce izolujemy wszystkie tabele z danego wyzwalacza.
Następnie wywołujemy ApplyTrigger. Wiemy, że FakeTable powoduje stworzenie mock’a, zatem wszystkie wyzwalacze domyślnie będą odizolowane. Wywołanie ApplyTrigger umożliwia selektywną aktywację wyzwalaczy.

Dalsza część kodu już wygląda analogicznie do innych testów. Wykonujemy INSERT INTO na Article, co spowoduje uruchomienie naszego wyzwalacza. Na końcu sprawdzamy, czy nowy wiersz faktycznie został dodany.

Unity 3d – wprowadzenie do skryptów

Dzisiaj napiszemy nasz pierwszy skrypt w Unity 3d.

Najpierw dodajmy jakiś element na scene, który potem będziemy mogli obracać za pomocą skryptu. W tym celu z menu głównego wybieramy  GameObject->3d Object-> Cube:

1

Następnie w celu dodania skryptu klikamy na “Assets” w panelu “Project” i wybieramy  Create->C# Script:

2

Zanim przejdziemy do edycji skryptu, warto upewnić się, że Visual Studio jest skonfigurowany jako narzędzie do pisania kodu. W tym celu wchodzimy do Edit->Preferences. External Script Editor powinien być ustawiony na VS:

3

Teraz możemy dwa razy kliknąć na skrypcie i Unity 3d odpali instancję Visual Studio w raz z szablonem nowo wygenerowanego skryptu:

public class SpinCube : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

Nas interesuje wyłącznie metoda Update, która jest odpalana w każdej ramce renderowania. To właśnie tam będą znajdować się wszelkie transformacje. Jeśli chcemy obracać obiektem wtedy należy skorzystać z pola “transform”:

public class SpinCube : MonoBehaviour
{
	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update ()
	{
             transform.Rotate(0, 1, 0);	
	}
}

Właśnie napisaliśmy nasz pierwszy skrypt. Musimy jednak go doczepić do jakiegoś obiektu. W naszym przypadku będzie to dodany wcześniej sześcian. Wystarczy na niego kliknąć i po prostu przeciągnąć skrypt:

4

Od tego momentu klasa SpinCube będzie wykonana w każdej ramce renderowania. Oczywiście ten sam skrypt możemy doczepiać do różnych obiektów. Po naciśnięci przycisku “Play”, sześcian zacznie obracać się.

Zwykle skrypty przyjmują jakieś parametry wejściowe. W naszym przypadku może to być prędkość obracania. Z punktu widzenia C#, jest to zwykłe pole publiczne:

public class SpinCube : MonoBehaviour
{
        public int Speed;

	void Start () {
	
	}
	
	// Update is called once per frame
	void Update ()
	{
        transform.Rotate(0, Speed, 0);	
	}
}

Po ponownym kliknięciu obiektu, zobaczymy, że “Speed” jest teraz konfigurowalny:

5

Unity 3d – wprowadzenie

Dzisiaj zaczynam nowy cykl wpisów. Pamiętam jak po zaprzestaniu wspierania XNA szukałem nowego framework’a do tworzenia wizualizacji czy gier.

Kilka lat temu może nie było to jeszcze tak oczywiste, ale dzisiaj Unity3d jest pewnego typu standardem dla prostych gier. Unity3d to nie framework, ale cały silnik 2d\3d wraz z zestawem narzędzi. Istnieje kilka typów licencji, ale będę zajmował się wyłącznie unity personal, którego można ściągnąć z stąd.

Jedną z ważniejszych cech Unity to wieloplatformowość. Ten sam kod będzie działał zarówno na Windows, Linux, przeglądarce, iOS jak i na wielu innych platformach.

Na blogu będę oczywiście korzystać z C#, ale Unity wspiera jeszcze skrypty JavaScript. To jest blog programistyczny, dlatego większość postów będzie tego właśnie dotyczyła. Z tego względu, dużo uwagi poświęcę również testom jednostkowym. Przeglądając wiele tutorial’ow, zauważyłem, że ludzie często pomijają testy jak i separacje logiki od warstwy prezentacji. Myślę, że przez to, że wiele rzeczy można zrobić z edytora to bardzo łatwo popełnij błędy związane z zasadami S.O.L.I.D.

Jak wspomniałem, Unity 3d to nie pojedyncza biblioteka a zestaw narzędzi. Głównym elementem jest zatem edytor, od którego wszystko zaczyna się. Po instalacji unity z powyższego linka zobaczymy następujący edytor:
1

Na ekranie widzimy wiele paneli. Najprostszym sposobem jest po prostu spędzenie trochę i czasu  przetestowanie wszystkich menu. Najważniejsze elementy to “Scene”, “Game” oraz “Inspector”.

W pierwszym oknie, jak nie trudno domyślić się, mamy naszą “scenę” czyli świat, który budujemy. Na początku poruszanie się po scenie będzie kłopotliwe, a w raz z czasem, można się przyzwyczaić. W lewym górnym oknie do dyspozycji mamy następujący zestaw przycisków:
2

Bardzo często się z nich korzysta i dlatego warto zapamiętać skróty klawiszowe Q, W, E, R, T.

Pierwszy przycisk (Q), służy do nawigacji. Naciskając lewy przycisk na scenie będziemy mogli poruszać się po niej. Z kolei prawy przycisk myszy albo klawisz ‘ALT’ służy do obracania się. Tutaj warto również zwrócić uwagę na osie w prawym górnym rogu:
3

Pokazują one pozycje i orientację kamery na scenie.

Pozostałe przyciski (W, E, R, T) służą do zmiany pozycji, rotacji oraz skalowania zaznaczonego obiektu:
4

Kolejny ważny panel to “Game”. W tym panelu, nasza gra będzie renderowana. Gdy chcemy ją przetestować, wystarczy nacisnąć przycisk “Play” znajdujący się na górze:

5

Warto wspomnieć, że w dowolnym momencie możemy zatrzymać grę i zmienić parametry takie jak np. rotacja elementu. Za pomocą tego, w łatwy sposób na bieżącą możemy dostosowywać różne parametry do naszych potrzeb.

Wiem, że dzisiaj nie było programowania, ale w następnym wpisie zajmiemy się już Unity Scripting w C#.

Typ Geography w Dapper

W poprzednim wpisie zajęliśmy się typem Geography, który jest przydatny na operacjach związanych z współrzędnymi geograficznymi. Jak wiemy, zapytania SQL wyglądają dość skomplikowanie, tzn.:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('POLYGON((-122.358 47.653 , -122.348 47.649, -122.348 47.658, -122.358 47.658, -122.358 47.653))', 4326)); 

Na szczęście Dapper ma wsparcie dla Geography i nie trzeba samemu tworzyć powyższych zapytań. Przede wszystkim należy skorzystać z klasy SqlGeography, która znajduje się w Microsoft.SqlServer.Types.dll. Jeśli ta biblioteka nie jest dołączona do projektu, należy dodać odpowiednią referencję:
1

Następnie, dodanie nowej wartości wygląda następująco:

            using (var connection = new SqlConnection(connectionString))
            {
                var geography = SqlGeography.STLineFromText(
                    new SqlChars(new SqlString("LINESTRING(-122.360 47.656, -122.343 47.656 )")), 4326);

                var pars = new { geo = geography };

                connection.Execute("INSERT INTO [Locations] (Location) values (@geo)", pars);
            }

Jak widać, Dapper rozpozna typ SqlGeography i wygeneruje odpowiedni kod SQL. Nie musimy ręcznie wstrzykiwać żadnego zapytania i korzystamy z parametrów tak jak to ma miejsce z innymi typami.

SQL Server – typ geography

W ostatnim w poście pokazałem jak odczytać współrzędne geograficzne. Kolejne możliwe pytanie, to jak je przechowywać w bazie danych. Najprostsze podejście to stworzenie dwóch kolumn typu float, dla szerokości i długości geograficznej.

W SQL Server istnieje jednak lepsze rozwiązanie, a mianowicie typ “geography”. Służy on do przechowywania lokalizacji i nie koniecznie  wyłącznie pojedynczego punktu. Możliwe jest zapisywanie całych poligonów. Utworzenie tabeli z taką kolumną, nie różni się niczym od pozostałych typów:

CREATE TABLE Locations   
    ( Id int IDENTITY (1,1),  
    Location geography );  
GO  

Następnie dodanie lokalizacji, opisującej poligon wygląda następująco:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('POLYGON((-122.358 47.653 , -122.348 47.649, -122.348 47.658, -122.358 47.658, -122.358 47.653))', 4326));  

W celu dodania lokalizacji w formie pojedynczego punktu, należy stworzyć obiekt POINT zamiast powyższego POLYGON:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('geography::Point(47.65100, -122.34900, 4326))', 4326));

Zaletą korzystania z geography jest, że wszelkie operacje takie jak znajdowanie odległości między dwoma punktami, są już zaimplementowane. Jeśli chcemy znaleźć części wspólne dwóch lokalizacji wtedy:

DECLARE @g geometry;  
DECLARE @h geometry;  
SET @g = geometry::STGeomFromText('POLYGON((0 0, 0 2, 2 2, 2 0, 0 0))', 0);  
SET @h = geometry::STGeomFromText('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))', 0);  
SELECT @g.STIntersection(@h).ToString();  

Zwracanie odległości między punktami wygląda następująco:

DECLARE @g geography;  
DECLARE @h geography;  
SET @g = geography::STGeomFromText('LINESTRING(-122.360 47.656, -122.343 47.656)', 4326);  
SET @h = geography::STGeomFromText('POINT(-122.34900 47.65100)', 4326);  
SELECT @g.STDistance(@h);  

Ponadto, SQL Server management studio, ma pewne wsparcie dla typu geography. Wykonując prosty SELECT zobaczymy wyłącznie binarne wartości:
1

Przechodząc jednak do zakładki “Spatial Results”, zobaczymy całą mapkę:
2

W przyszłym wpisie, pokażę jak korzystać z tego typu w C# (a konkretnie w Dapper).

Odczytywanie współrzędnych geograficznych

Ostatnio musiałem odczytać współrzędne geograficzne na podstawie nazwy lokalizacji.

Pierwszą opcją, którą sprawdziłem był pakiet “GoogleMaps.LocationServices”. Po instalacji NuGet, odczytanie współrzędnych było bardzo proste:

            var gls = new GoogleLocationService();
            var location = gls.GetLatLongFromAddress("Warsaw, Poland");

            Console.WriteLine("{0}, {1}", location.Latitude,location.Longitude);

Wynik:
1

Rozwiązanie bardzo proste. Biblioteka korzysta z Google Maps, ale niestety ma limity na liczbę zapytań w ciągu dnia. Co gorsza, nie istnieje możliwość zwiększenia tego limitu.

Z tego względu zdecydowałem się na bezpośrednie połączenie z Google API, a konkretnie z Geocoding API. Co prawda, rozwiązanie też ma limity dzienne, ale po darmowej rejestracji uzyskamy limit 100k dziennie, co w moim w wypadku było wystarczające. API jest w formie REST:
“https://maps.googleapis.com/maps/api/geocode/json?address=Warsaw,Poland&key=YOUR_API_KEY”

Oczywiście należy najpierw uzyskać klucz, rejestrując konto na Console Google. Przykładowa odpowiedź przychodzi w następującym formacie:

{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "Warsaw",
               "short_name" : "Warsaw",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "Warszawa",
               "short_name" : "Warszawa",
               "types" : [ "administrative_area_level_3", "political" ]
            },
            {
               "long_name" : "Warszawa",
               "short_name" : "Warszawa",
               "types" : [ "administrative_area_level_2", "political" ]
            },
            {
               "long_name" : "Masovian Voivodeship",
               "short_name" : "Masovian Voivodeship",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "Poland",
               "short_name" : "PL",
               "types" : [ "country", "political" ]
            }
         ],
         "formatted_address" : "Warsaw, Poland",
         "geometry" : {
            "bounds" : {
               "northeast" : {
                  "lat" : 52.3679992,
                  "lng" : 21.2710983
               },
               "southwest" : {
                  "lat" : 52.0978767,
                  "lng" : 20.8512898
               }
            },
            "location" : {
               "lat" : 52.2296756,
               "lng" : 21.0122287
            },
            "location_type" : "APPROXIMATE",
            "viewport" : {
               "northeast" : {
                  "lat" : 52.3679992,
                  "lng" : 21.2710983
               },
               "southwest" : {
                  "lat" : 52.0978767,
                  "lng" : 20.8512898
               }
            }
         },
         "place_id" : "ChIJAZ-GmmbMHkcR_NPqiCq-8HI",
         "types" : [ "locality", "political" ]
      }
   ],
   "status" : "OK"
}

Generując automatycznie klasy C# w Visual Studio na podstawie JSON uzyskamy:

  public class Rootobject
    {
        public Result[] results { get; set; }
        public string status { get; set; }
    }

    public class Result
    {
        public Address_Components[] address_components { get; set; }
        public string formatted_address { get; set; }
        public Geometry geometry { get; set; }
        public string place_id { get; set; }
        public string[] types { get; set; }
    }

    public class Geometry
    {
        public Bounds bounds { get; set; }
        public Location location { get; set; }
        public string location_type { get; set; }
        public Viewport viewport { get; set; }
    }

    public class Bounds
    {
        public Northeast northeast { get; set; }
        public Southwest southwest { get; set; }
    }

    public class Northeast
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Southwest
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Location
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Viewport
    {
        public Northeast1 northeast { get; set; }
        public Southwest1 southwest { get; set; }
    }

    public class Northeast1
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Southwest1
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Address_Components
    {
        public string long_name { get; set; }
        public string short_name { get; set; }
        public string[] types { get; set; }
    }

W przypadku usług REST, standardowo używam RestSharp+JSON.NET:

           var query = "https://maps.googleapis.com/maps/api/geocode/json?address=Warsaw,Poland&key=API_KEY";

            var client = new RestClient(query);
            var response = client.Get<dynamic>(new RestRequest());

            Rootobject data = JsonConvert.DeserializeObject<Rootobject>(response.Content);
            
            Console.WriteLine("{0}, {1}", data.results[0].geometry.location.lat, data.results[0].geometry.location.lng);

W praktyce, automatycznie wygenerowane klasy zostałyby zastąpione czytelniejszą strukturą. Tyle jednak wystarczy na szybkie sprawdzenie możliwości API.

Aktualziacja statystyk w SQL Server

O Sql Statistics pisałem już tutaj.
Wiemy, że dzięki takim statystykom dobierane są indeksy oraz plany wykonania. Na przykład jeśli zagęszczenie danych jest bardzo niskie, wtedy nawet warto zrezygnować z indeksów.

Utrzymanie statystyk nie jest łatwym zadaniem. Generalnie SQL Server jest odpowiedzialny za ich przeliczanie. Oczywiście przeliczanie od nowa statystyk za każdym razem, gdy dane są zmieniane, byłoby zbyt czasochłonne. Z tego wynika fakt, że istnieje ryzyko, że statystyki nie odzwierciedlają dokładnie danych.  SQL Server aktualizuje je w następujący sposób:

  1. Gdy nie ma żadnych danych, wtedy przeliczane są one w momencie dodania pierwszego wiersza.
  2.  Gdy jest mniej niż 500 wierszy, wtedy przeliczane są one w momencie osiągnięcia progu 500.
  3.  Gdy jest więcej niż 500 wierszy, każda zmiana 20% powoduje ponowne przeliczenie.

Jeśli mamy kilka milionów danych, przy odrobinie pecha, może zdarzyć się, że mniej niż 20% danych zostanie zmodyfikowanych, ale w taki sposób, że zaburzają one aktualnie wyliczoną dystrybucję danych.

W dowolnym momencie możemy sprawdzić, kiedy ostatnio statystyki były przeliczanie. Wystarczy użyć następującej komendy:

DBCC SHOW_STATISTICS('dbo.Articles',PRICE_INDEX)

W wyniku dostaniemy m.in. kolumnę Updated:

Można również ręcznie zaktualizować statystyki za pomocą Update Statistics:

UPDATE STATISTICS Sales.SalesOrderDetail

Możliwe jest również przeliczenie wszystkich statystyk w bazie:

EXEC sp_updatestats

Ostatnio pisałem o fragmentacji indeksów oraz potrzebie ich przebudowania. Dobrą wiadomością jest fakt, że gdy przebudowujemy indeks, również statystyki są przeliczanie. Nie ma zatem potrzeby wykonywania tych dwóch operacji jednocześnie.

Niestety nie ma łatwego sposobu na sprawdzenie, czy należy przebudować statystyki. Zwykle nie ma takiej potrzeby, ale czasami dane są modyfikowane w tabelach w taki sposób, że potrzebna jest ingerencja. Objawia się to wtedy źle dobranymi planami wykonania.

Fragmentacja indeksów

To jest nie blog o SQL Server, ale myślę, że podstawowe informacje o bazach danych są przydatne dla każdego programisty.

Wszyscy używamy indeksów i wiemy, że dzielą się one m.in. na clustered index oraz non-clustered index. Odpowiednie stworzenie ich może okazać się dość trudnym zadaniem. Niestety nasza rola nie ogranicza się wyłącznie do ich utworzenia ponieważ wcześniej czy później będziemy mieli do czynienia z fragmentacją indeksów.

Co to jest fragmentacja? W informatyce jest to znane pojęcie i oznacza, że dane nie są rozmieszczone równomiernie w pamięci. Indeks jest reprezentowany jako drzewo. Jeśli jego węzły nie są rozmieszczone równomiernie (w spójnych blokach), wtedy odczytywanie danych jest trudniejsze ponieważ musimy skakać z jednego adresu na drugi.

Tak na prawdę w bazach danych musimy być świadomi dwóch typów fragmentacji. Pierwszy z nich to fragmentacja fizyczna dysku. Jeśli dane na dysku nie są spójne, to naturalnie może zdarzyć się, że plik bazy danych również zostanie podzielony na różne bloki w pamięci. Ten typ fragmentacji nie ma nic wspólnego z SQL Server, ale jeśli ma miejsce, spowoduje spowolnienie wszystkich zapytań. Wyobraźmy sobie, że plik bazy danych jest umieszczony na tym samym dysku, gdzie umieszczone są jakieś inne pliki. Jeśli tylko jest tam dużo operacji Input\Output, wtedy fragmentacja może mieć miejsce dosyć często. Rozwiązaniem jest zwykła defragmentacja dysku wykonywana przez Windows. Dodatkowo warto umieszczać plik bazodanowy na osobnym, niezależnym dysku.

Drugi typ fragmentacji, to fragmentacja indeksu i jest ściśle związana z silnikiem baz danych. Wchodząc szczegóły, wyróżnia się fragmentację wewnętrzną oraz zewnętrzna. Jeśli usuniemy dane ze środka tabeli, wtedy pojawią się puste fragmenty na stronach pamięci. Wykonując DELETE wielokrotnie, możemy doprowadzić zatem do fragmentacji wewnętrznej, czyli pustych fragmentów (“dziur”) na stronach pamięci.

Z kolei zewnętrzna fragmentacja ma miejsce, gdy logiczna kolejność stron nie odpowiada fizycznej kolejności. Przytrafia się to, gdy wiersze są dodawane do środkowych stron, w których nie ma już miejsca. Załóżmy, że mamy strony Page1, Page2, Page3, które odpowiadają fizycznej kolejności Page1->Page2->Page3. Załóżmy, że wykonujemy INSERT, który powinien dodać dane na końcu strony Page1. Jeśli nie będzie tam wystarczającao miejsca, wtedy zostanie dodana nowa strona (nazwijmy ją Page1A) i fizyczny porządek będzie wyglądał Page1->Page2->Page3->Page1A, a logiczny z kolei Page1->Page1a->Page2->Page3. Innymi słowy, zewnętrzna fragmentacja to “dziury” pomiędzy stronami, a wewnętrzna to “dziury” w obrębie pojedynczych stron.

Wniosek taki, że indeksy trzeba przebudowywać od czasu do czasu. W  SQL Management Studio można wybrać z menu kontekstowego opcję Rebuild:

1

Na ekranie pojawi się informacja jak bardzo dany indeks jest sfragmentowany:

2

Oczywiście w praktyce automatyzuje się takie zadania. Administratorzy baz danych zwykle definiują skrypty, które wykonają się, gdy poziom fragmentacji osiągnie dany próg.

Na przykład, podany skrypt wyświetli informacje o wszystkich indeksach wraz z ich fragmentacją (źródło):

SELECT dbschemas.[name] as 'Schema', 
dbtables.[name] as 'Table', 
dbindexes.[name] as 'Index',
indexstats.avg_fragmentation_in_percent,
indexstats.page_count
FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats
INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id]
INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id]
INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id]
AND indexstats.index_id = dbindexes.index_id
WHERE indexstats.database_id = DB_ID()
ORDER BY indexstats.avg_fragmentation_in_percent desc