poniedziałek, 29 sierpnia 2011

jqPlot - wykresy w ASP MVC

Chcąc wyświetlić na naszej stronie wykres wartości, możemy skorzystać z Silverlighta lub javascriptu. Możliwści silvrlighta podczas tworzenia wykresów prezentowałem w jednym z wcześniejszych postów. Teraz pokażę w jaki sposób można za pomocą JSONa i biblioteki jQPlot wyświetlić wykres dla pewnych wartości (np. pobranych z bazy danych).
Ze strony http://www.jqplot.com/ pobieramy bibliotekę jQPlot.
Po pobraniu dodajemy pliki pluginu do naszego projektu:


Następnie dodajemy odnośniki do bibliotek jQuery oraz jQPlot:


Code:
<script src="../../Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
    <script src="../../Scripts/jquery.jqplot.min.js" type="text/javascript"></script>
    <link href="../../Scripts/jquery.jqplot.css" rel="stylesheet" type="text/css" />

W naszym widoku umieścimy kod znajdujący się na stronie autora dodatku:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
 
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>
 
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <div id="chartdiv" style="height:400px;width:300px; "></div>
    <script type="text/javascript">
        $.jqplot('chartdiv', [[[1, 2], [3, 5.12], [5, 13.1], [7, 33.6], [9, 85.9], [11, 219.9]]]);
    </script>
</asp:Content>

Po uruchomieniu aplikacji otrzymamy następujący widok:



Oczywiście sztywne umieszczenie wartości w kodzie nie przyda się nam zbytnio. Potrzebna jest nam metoda, która pozwoli zwrócić dane do widoku:


Code:
public ActionResult GetChartData()
        {
            JsonResult jsonResult = new JsonResult();
            int[] data = { 1, 5, 6, 8, 3, 4 };
            jsonResult.Data = data;
            jsonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

            return jsonResult;
        }

W widoku umieszczamy kod jQuery odpowiedzialny za odebranie danych z kontrolera:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <div id="chartdiv" style="height:400px;width:300px; "></div>
    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("/Home/GetChartData", null, function (data) {
                $.jqplot('chartdiv', [data]);
            });
        });
    </script>
</asp:Content>

Po uruchomieniu aplikacji zobaczymy następujący wykres:


Oczywiście jqPlot oferuje dużo więcej możliwości, m.in.:
- różne typy wykresów
- osie z dowolnym formatowaniem
- automatyczne wyliczanie linii trendu
- podświetlanie punktów i wyświetlanie wartości w danym punkcie
- drag and drop punktów
To tylko kilka z wielu możliwości tego komponentu.
Zachęcam do używania i eksperymentowania.

JSON obsługa w ASP MVC 2

Z pewnością wielu z Was spotkało się z formatem JSON - JavaScript Object Notation. Format ten przekazywany jest w postaci tekstowej. Ponieważ dane przekazywane są jako zwykły tekst, mniej zajmują a co za tym idzie przesyłanie jest szybsze niż w przypadku standardowego XMLa. Obecnie wykorzystywany jest w aplikacjach silnie korzystających z AJAXu.
ASP MVC 2 pozwala w łatwy sposób na przesyłanie danych w omawianym formacie. Wystarczy tylko zwrócić dane w metodzie kontrolera jako typ JsonResult.

Zobaczmy na bardzo prosty przykład:
W kontrolerze umieścimy metodę zwracającą tablicę liczb od 1 do 20, a następnie stworzymy z niej listę w naszym widoku:


Code:
public ActionResult GetJSONData()
        {
            int[] data = Enumerable.Range(1, 20).ToArray();
            JsonResult jSonResult = new JsonResult();
            jSonResult.Data = data;
            jSonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

            return jSonResult;
        }

Widok Home:


Code:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%: ViewData["Message"] %></h2>
    <br />

    <div id="JsonDiv">
        <ul>
            
        </ul>
    </div>

    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("/Home/GetJSONData", null, function (data) {
                $.each(data, function (index, value) {
                    var newValue = "<li>" + value + "</li>";
                    $("#JsonDiv ul").append(newValue);
                });
            });
        });
    </script>
</asp:Content>

Parsowanie JSONa przy użyciu jQuery jest niezwykle proste. W tym przypadku:
  1. Czekamy aż dokument całkowicie się załaduje w celu dodania do diva listy naszych liczb
  2. Pobieramy dane przekazane przez metodę za pomocą metody getJSON - przekazując jako argument nazwę kontrolera i metody która zwraca dane
  3. Za pomocą funkcji each iterujemy po kolekcji zawierającej nasze dane i dodajemy je do listy
Rezultat:





Jak widać proste i bardzo łatwe, podobnie ma się rzecz do obiektów o bardziej skomplikowanej strukturze:

W modelu tworzymy definicję klasy Person:


Code:
public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public List<Car> Cars { get; set; }

        public Person(int id, string firstName, string lastName, DateTime birthDate)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            BirthDate = birthDate;
        }
    }

Następnie klasę Car:


Code:
public class Car
    {
        public string Make { get; set; }
        public int Year { get; set; }
        public string Model { get; set; }
    }

Ostatnia klasa będzie klasą generującą testowe dane:


Code:
public class FakeData
    {
        public List<Person> GetFakeData(int fakeEntitiesCount)
        {
            List<Person> lPerson = new List<Person>();
            Random r = new Random();
            string[] firstNames = { "Jacek", "Maciek", "Paweł", "Sylwek" };
            string[] lastNames = { "Kowalski", "Markowski", "Piotrkowski" };
            string[] carMakers = { "VW", "Ford", "Fiat" };
            string[] carModels = { "Jetta", "Bora", "Focus", "Panda" };

            for (int i = 0; i < fakeEntitiesCount; i++)
            {
                Person p = new Person(i, firstNames[r.Next(0, firstNames.Length)], lastNames[r.Next(0, lastNames.Length)],
                    DateTime.Now.AddYears(r.Next(19, 50)));
                p.Cars = new List<Car>();
                int carCount = r.Next(0, 4);
                for (int j = 0; j < carCount; j++)
                {
			        p.Cars.Add(new Car { Make = carMakers[r.Next(0, carMakers.Length)], 
                        Model = carModels[r.Next(0, carModels.Length)],
                        Year = DateTime.Now.AddYears(r.Next(0,8)).Year});
                }
                lPerson.Add(p);
            }

            return lPerson;
        }
    }

Oczywiście testowe dane - to tylko testowe dany, raczej każdy wie że Fiat nie produkuje Bory :).

W metodzie Index naszego kontrolera w zmiennej sesji przechowamy dane odnośnie osób i ich samochodów:


Code:
public ActionResult Index()
        {
            Session["Data"] = new FakeData().GetFakeData(20);
            ViewData["Message"] = "Przekazywanie danych w formacie JSON";

            return View();
        }

Metoda CarEvidence zwróci nam listę osób:


Code:
public ActionResult CarEvidence()
        {
            List<Person> lPerson = new List<Person>();
            if (Session["Data"] != null)
            {
                lPerson = (List<Person>)Session["Data"];
                List<SelectListItem> lPersonList = new List<SelectListItem>();
                foreach (var person in lPerson)
                {
                    lPersonList.Add(new SelectListItem { Text = person.FirstName + " " + person.LastName, Value = person.Id.ToString() });
                }
                ViewData["PersonList"] = lPersonList;
            }

            return View(lPerson);
        }

Łatwo zauważyć, że jeżeli ktoś od razu wejdzie na stronę CarEvidence nie otrzyma żadnych danych - jest to jednak tylko przykład w realnej aplikacji z pewnością źródłem danych będzie baza danych bądź plik XML.

Za pomocą metody GetPersonCars wydobędziemy informacje o samochodach danego kierowcy:


Code:
[HttpGet]
        public ActionResult GetPersonCars(int idPerson)
        {
            JsonResult jSonData = new JsonResult();
            jSonData.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
            if (Session["Data"] != null)
            {
                List<Car> lCars = ((List<Person>)Session["Data"]).Where(x => x.Id == idPerson).SingleOrDefault().Cars;
                jSonData.Data = lCars;
            }

            return jSonData;
        }

Definicja widoku CarEvidence:


Code:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MvcApplication2.Models.Person>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	CarEvidence
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>CarEvidence</h2>
    <div>
        <%: Html.Label("Wybierz osobę: ")%>
        <br />
        <%: Html.DropDownList("ddlPersonList", (List<SelectListItem>)ViewData["PersonList"]) %>
    </div>

    <div id="PersonCars">
        <ul>
        </ul>
    </div>

    
    <script type="text/javascript">
        $("#ddlPersonList").change(function () {
            var id = $("#ddlPersonList").val();
            $.getJSON("/Home/GetPersonCars/", { idPerson: $(this).val() }, function (data) {
                $("#PersonCars ul li").remove();
                $.each(data, function (index, value) {
                    var car = "<li>" + value.Year + " " + value.Make + " " + value.Model + "</li>";
                    $("#PersonCars ul").append(car);
                });
            });
        });    
    </script>

</asp:Content>

Po uruchomieniu aplikacji otrzymamy następujący widok:





Zmieniając wybór na liście, możemy oglądać dane o samochodach danego użytkownika.
Kilka uwag co do kodu jQuery tutaj użytego:
- Metoda change przechwytuje zdarzenie zmiany elementu na liście
- funkcja val() pobiera wartość - id osoby danego elementu listy
- metoda getJSON pobiera JSON przesyłając do metody kontrolera informację o wybranej osobie: {idPerson: $(this).val()}
- za pomocą metody each()  - wyświetlamy informacje o samochodach wybranego kierowcy

Jak widać korzystanie z JSONa nie jest trudne. Można za pomocą niego uzyskać bardzo ciekawe efekty bez potrzeby przeładowywania całej strony. System serializacji wbudowany w ASP MVC z łatwością deserializuje i serializuje dane przekazywane w tym formacie - co czyni jego użycie jeszcze prostszym. jQuery dopełnia zestaw narzędzi pozwalając w prosty sposób wyświetlić użytkownikowi otrzymane dane czy też przekazać je do kontrolera w celu otrzymania żądanych dnaych.

Miłego eksperymentowania.

sobota, 13 sierpnia 2011

Indeksy w SQL Server

Czym jest indeks w bazie danych wie z pewnością każdy deweloper. Jednak już o samych strategiach i nowych ich możliwościach rzadko się piszę. W tym artykule opiszę rodzaje indeksów w bazie MS SQL Server, strategiach ich nakładania, a także przedstawię przykłady uzyskanych wyników przy zastosowaniu różnych indeksów i konsekwencjach ich braków.

Indeks to obok normalizacji jeden z najważniejszych tematów w bazach danych. Indeks to nic innego jak struktura danych ułatwiająca wyszukiwanie danych. Wyobraźmy sobie sytuacje w której mamy plik z zapisanymi 3000 wierszy danych. Aby znaleźć interesujące nas informacje średnio będziemy potrzebować przejrzeć połowę zawartości tego pliku - czyli dokonamy około 1500 odczytów. Dzięki zastosowaniu indeksu jesteśmy w stanie zmniejszyć ten wskaźnik do 4 odczytów. Zysku takiego dokonuje się poprzez zastosowanie drzewiastej struktury danych B-drzewa oraz B+-drzewa (w dziale literatura na końcu tego artykułu znajdują się linki opisujące dokładnie te struktury danych).
Przyjrzyjmy się różnym typom indeksów obecnych w MS SQL Server.

Nonclustered:
Indeks ten, można sobie wyobrazić jako skorowidz w książce. Jeżeli weźmiemy do ręki katalog narzędzi w którym mamy 600 stron, a chcemy znaleźć konkretne narzędzie np. młotek, to szkoda w takim wypadku przeglądać kartka po kartce w poszukiwaniu informacji. Łatwiej zaglądnąć do skorowidza i tam w szybki sposób zlokalizować stronę na której znajduje się opis szukanego przez nas narzędzia.
Tak więc:
- dane fizycznie nie są ułożone zgodnie z logiczną kolejnością danych reprezentowaną przez ten indeks
- stosowany jest w klauzulach typu: JOIN, WHERE, ORDER BY
- dobre dla tabel często modyfikowanych
- dla SQL Servera 2005 możemy utworzyć 249 takich indeksów w tabeli, dla 2008 - 999
- wszędzie tam, gdzie mamy niewielką selekcję danych z tabeli 0 - 5% wszystkich wierszy z docelowej tabeli

Clustered
W przeciwieństwie do poprzednio omawianego indektu, ten indeks definiuje sposób w jaki dane są ułożone na dysku.
Podsumowując:
- tylko jeden na tabelę
- determinuje kolejność składowania danych w pliku
- jeżeli nie nałożymy go jawnie, jego rolę będzie pełnił PRIMARY KEY
- szczególnie widać jego zalety podczas pobierania przedziałów danych

Nonclustered indeks wraz z dodatkowymi kolumnami
Aby rozwinąć możliwości indeksów nonclustered, można zawrzeć w ich definicji. Dzięki temu można "pokryć" większą ilość zapytań co zmniejszy ilość odczytów z tabeli (ponieważ szukane informacje będą już zawarte w indeksie). Dodatkowo indeks ten może zawierać kolumny które nie są w normalnych okolicznościach dostępne dla indeksów typu nonclustered (np. varchar(max)).
Podsumowując:
- indeks nonclustered wraz z dodatkowymi kolumnami pozwala na dołączenie dodatkowych kolumn
- indeks taki może dodatkowo zawierać dane które w normalny sposób nie mogłyby zostać zaindeksowane
- zmniejsza ilość odczytów i dostępów do tabeli
- maksymalna ilość kolumn dołączony to 1023

Filtered indeks
Pozwala indeksować dane na podstawie wyspecyfikowanego warunku. Dzięki temu pokrywa tylko część danych.
Najważniejsze fakty dotyczące tego indeksu:
- typ nonclustered wraz z klauzulą WHERE
- mniejsza ilość zajmowanego miejsca
- zmniejsza narzut podczas tworzenia statystyk
- zmniejsza narzut podczas dodawania nowych danych, ponieważ indeks tworzony jest tylko dla danych wyspecyfiowanych w klauzuli WHERE
- zmniejsza czas podczas odbudowy tabeli.
Prosty przykład zastosowania. Mamy tabelę użytkowników. Jeżeli zastosujemy filtered indeks na kolumnie aktywność z warunkiem aktywny (WHERE aktywny='A') wtedy dane dodawane z aktywnością 'N' (nieaktywny) są nieindeksowane co zmniejsza wartość wielkości pliku indeksu jak i przyspiesza dodawanie takich rekordów.

Indeksowane widoki
Z pewnością, czasem rzadziej czasem częściej przychodzi nam stosować widoki. Widoki ułatwiają przede wszystkim korzystanie z wielokrotnie wykorzystywanych zapytań, posiadających skomplikowaną logikę. Na takim widoku możemy stworzyć indeks. Co dodatkowo może on objąć kolumny z różnych widoków.
Można by tu znaleźć podobieństwo do Filtered index, jednak różnic jest więcej niż podobieństw. Bardzo dobre porównanie przedstawił w swoim artykule Kanasz Robert. Przedstawię w skrócie najważniejsze punkty:


Fill factor
Fill factor to modyfikator pozwalający na określenie czy SQL Server ma pozostawiać wolne miejsce w ramkach. Po co takie wolne miejsce pozostawiać albo nie pozostawiać? Otóż w przypadku kiedy do tabeli wstawiane są nowe dane silnik bazy danych musi wygospodarować miejsce aby je ulokować w odpowiedniej kolejności. W najgorszym przypadku musi przenieść znaczną część danych na następne rami co odbija się na wydajności. Dobierając odpowiednio Fill factor, możemy w zależności od sytuacji określić w jaki sposób ma być zagospodarowane miejsce w ramce. Fill factor przyjmuje wartości od 0 - 100%. 100% w tym przypadku oznacza, że nie pozostawiamy żadnego dodatkowego miejsca. Ustawienie takie jest dobre dla tabel, które będą tylko odczytywane - czyli tabele tylko do odczytu. W przypadku mniejszej wartości zostanie zostawione więcej miejsca, co ograniczy potrzebę przenoszenia informacji między ramkami.
Strategie nadawania:
- tabela w których jest mało operacji zmian danych - parametr na 90 - 100
- dane będą często zmieniane - 60 - 80

Czas na podsumowanie i parę rad podczas tworzenia indeksów:
- podczas tworzenia indeksów, starajmy się je budować na jednej kolumnie lub tylu ile jest faktycznie potrzebne. Co to oznacza? Otóż im krótszy jest indeks tym szybciej silnik bazy danych sprawdzi go, a także ilość zajmowanego miejsca będzie najmniejsza.
- w każdej tabeli powinniśmy nakładać clustered index na kolumnie wykorzystywanej najczęściej w operacjach wyszukiwania
- nakładając clustered index starajmy się go nakładać na kolumnę z dużą selektywnością (mała ilość duplikatów kluczy)
- unikaj nakładania clustered index na kolumny, które często podlegają operacji uaktualniania. Oprócz samego uaktualnienia indeksu na kolumnie której jest nałożony, muszą zostać także uaktualnione wszystkie indeksy nonclustered odwołujące się do tego indeksu.
- unikajmy i usuwajmy zbędne czy też powtarzające się indeksy
- indeksy nonclustered powinniśmy (o ile to jest możliwe) tworzyć na osobnym dysku, zapewniając tym samym poprawienie wydajności
- tworząc złożony indeks (z kilku kolumn) najbardziej selektywną kolumnę wprowadzamy od lewej strony indeksu



Przejdźmy teraz do części praktycznej omawianego artykułu.
Dla przykładu stworzyłem nową bazę z tabelą Person o następującej strukturze:


Code:
CREATE TABLE [dbo].[Person]
(
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[FirstName] [varchar](50) NULL,
	[LastName] [varchar](50) NULL,
	[City] [varchar](50) NULL,
	[BirthDate] [datetime] NULL,
	[Activ] [char](1) NULL,
        CONSTRAINT [PK_Person] PRIMARY KEY
)

Do tabeli załadowałem 1 mln wierszy danych testowych:

Mając dane możemy rozpocząć nasze testy.
1. Wyszukiwanie i wyświetlenie wartości całego wiersza:




Jak widać dla zapytania Estimated Subree Cost jest równy 5,7. Jest to bardzo duży wynik i naszym zadaniem jest zmniejszenie go jak najbardziej się da. Na kolumnie firstName nie ma indeksu tak więc tabela musiała zostać przejrzana w całości.

2. Wyszukiwanie i wyświetlenie wartości całego wiersza - nałożony indeks nonclustered
Założymy na kolumnie firstName indeks typu nonclustered:

CREATE INDEX IX_Person_FistName
ON Person (FirstName);

Po tej operacji wykonamy ponownie nasze zapytanie:
Jak widać koszt spadł diametralnie.

3. Wyszukiwanie i wyświetlenie wartości kolumny na której jest nałożony nonclustered index
Jeżeli nasze zapytanie przyjmie postać:
SELECT FirstName
FROM Person
WHERE FirstName='Ślipienko'

wtedy koszt zapytania będzie wynosić:
4. Dołożenie do indeksu nonclustered dodatkowych kolumn
Aby uniknąć dodatkowego odwołania do tabeli, można do indeksu dołożyć dodatkowe kolumny:
CREATE INDEX IX_Person_FistName
ON Person (FirstName)
INCLUDE(LastName,City)

Dzięki dodaniu dodatkowych kolumn do indeksu, nie jest potrzebne odwołanie się do tabeli po dane.


5. Wykorzystanie Filtered index:
W tabeli mamy użytkowników aktywnych i nieaktywnych. Nasz klient w większości przypadków będzie chciał oglądać użytkowników aktywnych. Zobaczmy proste zapytanie wyświetlające użytkowników aktywnych:
SELECT FirstName,LastName,City
FROM Person
WHERE Activ='A'

Jak widać koszt zapytania jest bardzo duży. W tym przypadku, wiemy jakich danych oczekuje użytkownik. Skorzystamy więc z filtered index:
CREATE INDEX IX_Person_FistName
ON Person (FirstName)
INCLUDE (LastName,City)
WHERE Activ='A'

Po wykonaniu zapytania otrzymujemy wynik:
Wynik znacząco się poprawił.



Podsumowując otrzymane wyniki można stwierdzić, że stosując dobrze dobrane indeksy można uzyskać znaczące przyspieszenie wyszukiwania danych. W większości systemów na 10 wyszukań przypada 5 insertów. Tak więc koszt utrzymywania indeksów jest niewielki w porównaniu z przyspieszeniem wydobywania danych.



Literatura
http://en.wikipedia.org/wiki/B-tree
http://en.wikipedia.org/wiki/B%2B_tree
http://msdn.microsoft.com/en-us/library/dd171921%28v=sql.100%29.aspx
http://www.codeproject.com/KB/database/DB_Prerformance_Tuning_1.aspx
http://msdn.microsoft.com/en-us/library/aa933139%28v=sql.80%29.aspx