in

dotNet Umbria

Il primo User Group in Umbria sul mondo .Net

Sql Server Tips & Best Practices

February 2008 - Posts

  • SQL Server Tip: Spostare le tabelle da un filegroup ad un'altro

    La creazione della struttura del DataBase con SqlServer 2005 ci permette di creare più di un filegroup, ed è possibile distribuire gli oggetti sui filegroups in modo ottenere alcuni vantaggi di performance ed alcuni vantaggi nella tecniche di Disaster Recovery. Tuttavia, la maggior parte delle volte i database non sono progettati su più filegroups, ma tutti gli oggetti vengono posizionati di default sul filegroup primario. L'Ide non ci permette di modificare la posizione di oggetti gia creati, questo vale anche per le tabelle gia popolate. Come fare allora per spostare le tabelle da un filegroup ad un'altro filegroup ?

    La risposta non è così immediata, dobbiamo infatti agire sull'indice clustered della tabella per "spostare" la tabella stessa. L'indice clustered infatti istruisce SQL Server sul modo in cui devono essere memorizzate le righe all'interno del DataBase, tutte le righe cioè vengono memorizzate nella posizione e con l'ordinamento indicato dall'indice clustered. Questo tipo di indice è quindi aderente alla struttura della tabella stessa, e per sua natura e funzione, ne può esistere solo uno per ogni tabella.

    Immaginiamo di creare una tabella su un DB composto da più filegroups:

    [code language="SQL"]

    CREATE DATABASE TestDB
    GO

    ALTER DATABASE [TestDB] ADD FILEGROUP [SECONDARY]
    GO

    ALTER DATABASE [TestDB] ADD FILE ( NAME = N'TestDB2', FILENAME = N'c:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\TestDB2.ndf' , SIZE = 3072KB , FILEGROWTH = 1024KB ) TO FILEGROUP [SECONDARY]

    [/code]

    Creiamo una nuova tabella nel filegroup primario.

    [code language="SQL"]

    CREATE TABLE dbo.FGTest1
        (
        Id uniqueidentifier NOT NULL,
        Descrizione varchar(50) NULL
        )  ON [PRIMARY]
    GO


    ALTER TABLE dbo.Table_1 ADD CONSTRAINT
        PK_FGTest1 PRIMARY KEY CLUSTERED
        (
        Id
        )  ON [PRIMARY]

     

    [/code]

     

    La tabella viene creata nel file group primario così come la sua primary key, che la maggior parte delle volte corrisponde all'indice clustered.
    Se volessimo spostare la tabella da un FileGroup ad un'altro filegroup a questo punto sarebbe sufficiente spostare l'indice clustered, cancellandolo e ricreandolo.
    Questa operazione sposta fisicamente le righe di una tabella da un filegroup da un'altro filegroup e potrebbe richiedere molto tempo in tabelle con grosse quantità di dati.

     

    [code language="SQL"]

    ALTER TABLE dbo.FGTest1
        DROP  PK_FGTest1

    GO

    ALTER TABLE dbo.FGTest1 ADD CONSTRAINT
        PK_FGTest1 PRIMARY KEY CLUSTERED
        (
        Id
        )  ON [SECONDARY]

     

    [/code]

    Dobbiamo fare attenzione ad una particolarità. Tutta la struttura delle foreign key si basa sulla presenza degli indici, quindi non possiamo droppare l'indice corrispondente alla primary key senza compromettere il controllo dell'integrità referenziale all'interno del DataBase. La soluzione a questo piccolo dettaglio è non droppare l'indice corrispondente alla primary key, ma trasformarlo in un indice non clustered e creare un secondo indice clustered identico alla primarykey.
    Per eseguire l'operazione di trasformazione da clustered a non clustered possiamo utilizzare l'IDE che ci genera tutti gli script per mantenere intatta la sturttura del DB.
    In seguito aggiungeremo il nuovo indice clustered

    [code language="SQL"]

     

    CREATE CLUSTERED INDEX IX_FGTest_Clust ON dbo.FGTest1
    (
        Id
    )  ON [SECONDARY]

     

    [/code]

  • SQL Server 2005 - Esplodere le gerarchie con WITH

    Fino alla versione 2000 per esplodere una gerarchia padre-figlio erano necessarie tabelle di appoggio, viste e stored procedures costruite ad-hoc.

    Con SqlServer 2005 è stata introdotta una nuova istruzione che ci permette di raggiungere lo scopo in una singola query.

    Vediamo in dettaglio la problematica ed il funzionamento della parola chiave WITH.

    Supponiamo di avere una tabella che contiene una struttura Padre-Figlio e un po' di dati.

    [code language="SQL"]

    CREATE DATABASE TestDB
    GO

    USE TestDB
    GO

    CREATE TABLE [dbo].[Tree]
    (
        [Id] [int] NOT NULL,
        [Padre] [int] NULL,
        [Descrizione] [varchar](100) NULL,
    CONSTRAINT [PK_Tree] PRIMARY KEY CLUSTERED
    ([Id] ASC)
    )
    GO

    INSERT INTO Tree(Id, Padre, Descrizione) Values (1,Null,'Padre')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (2,1,'Figlio 1')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (3,1,'Figlio 2')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (4,1,'Figlio 3')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (5,2,'Nipote 1')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (6,2,'Nipote 2')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (7,3,'Nipote 3')
    INSERT INTO Tree(Id, Padre, Descrizione) Values (8,7,'Pronipote 1')
    GO


    [/code]

     

    Quello che vogliamo ottenere è l'elenco dei nodi dell'albero con l'indicazione del livello del nodo e della struttura padre-figlio di tutta la catena in un'unica query.
    Questi sono i dati che abbiamo inserito:

    Id          Padre       Descrizione
    ----------- ----------- ---------------------
    1           NULL        Padre
    2           1              Figlio 1
    3           1              Figlio 2
    4           1              Figlio 3
    5           2              Nipote 1
    6           2              Nipote 2
    7           3              Nipote 3
    8           7              Pronipote 1

    Per prima cosa dobbiamo individuare il nodo radice.

    [code language="SQL"]

    SELECT Id,Padre,Descrizione
    FROM Tree
    WHERE Padre is Null

    [/code]

    Utilizziamo With per iniziare a dichiarare la vista che ci permetterà di esplodere la gerarchia. Dobbiamo indicare nella dichiarazione della vista con With l'elenco delle colonne che utilizzeremo come risultato.

    [code language="SQL"]

     

    WITH TreeExploded(Id,Padre,Descrizione,Struttura,Livello) AS
    (
        SELECT Id,Padre,CAST(Descrizione as varchar(50)), CAST('' as varchar(20)), 0
        FROM Tree
        WHERE Padre is Null
    )
    SELECT * FROM TreeExploded

     

    [/code]

    Abbiamo dichiarato 2 colonne che attualmente non abbiamo, ovvero "Struttura" e "Livello". Queste colonne verranno calcolate nella vista. Con WITH con dichiaro la vista che poi posso riutilizzare nella select successiva.
    Nella query all'interno della vista abbiamo inoltre fatto un CAST di una stringa vuota a varchar(100), questo è necessario per indicare a WITH con quale tipo di dato avrà a che fare con le successive righe.

    Fino a qui non abbiamo visto nulla di strano, se non il modo con cui si può dichiarare una vista subito prima di utilizzarla. La caratteristica di WITH però è la possibilità di utilizzare i dati estratti dalla vista, come dati di partenza per la vista stessa. Ovvero possiamo referenziare all'interno della vista, la vista stessa in una query in UNION ALL. Vediamo come fare:

    [code language="SQL"]

    WITH TreeExploded(Id,Padre,Descrizione,Struttura,Livello) AS
    (
        SELECT        Id,
                    Padre,
                    CAST(Descrizione as varchar(50)),
                    CAST('' as varchar(20)), 0
        FROM Tree
        WHERE Padre is Null

        UNION ALL

        SELECT        Tree.Id,
                    Tree.Padre,
                    CAST(Tree.Descrizione as varchar(50)),
                    CAST('' as varchar(20)),
                    Livello + 1
        FROM Tree
        JOIN TreeExploded ON
            Tree.Padre = TreeExploded.Id
    )
    SELECT * FROM TreeExploded

    [/code]

    Nella seconda query all'interno della WITH abbiamo messo in JOIN la vista stessa. In questo modo possiamo utilizzare i dati estratti dalla prima query nella UNION per estrarre ulteriori dati nella query successiva senza fare ricorso a tabelle temporanee.

    Arricchiamo il tutto con la definizione della struttura e con un po' di ordinamenti.

    [code language="SQL"]

    WITH TreeExploded(Id,Padre,Descrizione,Struttura,Livello) AS
    (
        SELECT        Id,
                    Padre,
                    CAST(Descrizione as varchar(50)),
                    CAST(Id as varchar(20)), 0
        FROM Tree
        WHERE Padre is Null

        UNION ALL

        SELECT        Tree.Id,
                    Tree.Padre,
                    CAST(REPLICATE ( ' ' , Livello )  + Tree.Descrizione as varchar(50)),
                    CAST(Struttura + '.' + CAST(Tree.Id as varchar(10)) as varchar(20)),
                    Livello + 1
        FROM Tree
        JOIN TreeExploded ON
            Tree.Padre = TreeExploded.Id
    )
    SELECT Id,Padre,Struttura,Descrizione,Livello
    FROM TreeExploded
    ORDER By Struttura

    [/code]

    Ho introdotto il REPLICATE per indentare anche le descrizioni in modo da rendere più chiaro il risultato:

    Id          Padre       Struttura            Descrizione                                        Livello
    ----------- ----------- -------------------- -------------------------------------------------- -----------
    1           NULL        1                    Padre                                              0
    2           1              1.2                  Figlio 1                                           1
    5           2              1.2.5                 Nipote 1                                       2
    6           2              1.2.6                 Nipote 2                                       2
    3           1              1.3                  Figlio 2                                           1
    7           3              1.3.7                 Nipote 3                                       2
    8           7              1.3.7.8                Pronipote 1                                3
    4           1              1.4                  Figlio 3                                           1


    Buon lavoro.

  • Best Practice - Evitare le query nidificate nella SELECT

    Evitare dove possibile di inserire query nidificate all'interno della clausola SELECT della Query.

    Le query nidificate, comportano un lavoro ulteriore all'ottimizzatore della query che cercherà di effettuare meno operazioni di lettura possibile per risolvere la query.
    Es:

    [code language="SQL"]

    -- Query non ottimizzata

    SELECT    Orders.OrderID,
                     Orders.OrderDate,
            (SELECT Count(1)
                FROM [Order Details]
                WHERE [Order Details].OrderID  = Orders.OrderId)
        FROM Orders

    -- Query ottimizzata

    SELECT    Orders.OrderID,
                     Orders.OrderDate,
                     Conta
        FROM Orders
        JOIN (SELECT OrderID, Count(1) as conta FROM [Order Details] GROUP BY OrderID) as T1 ON
            Orders.OrderID = T1.OrderID



    [/code]

    La prima query viene risolta dall'ottimizzatore delle query in modo molto simile alla seconda query, tuttavia l'ottimizzatore delle query deve eseguire una operazione in più per capire il modo in cui la query deve essere eseguita, e per limitare al massimo le operazioni di IO. Dove possibile quindi è opportuno spostare la query nidificata nella clausola JOIN ed eseguire le operazioni di raggruppamento in modo esplicito.

  • Best Practice - Creazione degli indici

    Per migliorare le prestazioni dei nostri DataBase è necessario valutare in modo attento la creazione degli indici all'interno delle nostre tabelle.

    1. Creare gli indici per i campi che sono frequentemente utilizzati all'interno delle nostre query in operazioni di WHERE e in operazioni di JOIN.
      Evitiamo in questo modo che l'ottimizzatore delle query di SQL Server decida di fare dei Table Scan per effettuare operazioni di ricerca e per mettere in JOIN le tabelle.

    2. Creare indici composti per colonne frequentemente utilizzate

    3. Separare su dischi diversi tabelle che partecipano alle join utilizzando differenti filegroups.
      Separando su più filegroups e su dischi differenti le tabelle più corpose, miglioriamo le performance generali poichè aumentiamo le operazioni di I/O che possono essere eseguite in parallelo.

    4. Non esagerare nella creazione degli indici
      Gli indici aiutano in modo sensibile le performance ma occupano spazio all'interno dei DataBase, e richiedono un tempo di manutenzione durante le operazioni di INSERT/UPDATE/DELETE.
      Creare indici in modo superfluo significa penalizzare le operazioni di modifica dei dati.

    5. Separare gli indici non-clustered dai dati utilizzando differenti filegroups su differenti dischi
      Anche in questo caso è possibile migliorare le performance, aumentando il parallelismo, dividendo le operazioni di "lettura" dei dati dalle operazioni di "ricerca" dei dati.
      La separazione degli indici in filegroups differenti ci permette anche di avere differenti strategie di Backup, gli indici infatti possono essere ricostruiti con DBCC. E' quindi possibile fare il backup dei dati in modo separato dagli indici.
      Ed eseguire le operazioni di ripristino in modo differito.

    6. Limitare il fattore di riempimento per tabelle modificate di frequente
      Il fattore di riempimento di un indice, indica a SQL Server quanto deve essere completo l'indice per effettuare la ricerca. L'indice infatti non viene utilizzato per "trovare" in modo esatto il dato, ma viene utilizzato per arrivare il più vicino possibile al dato stesso. Avere un fattore di riempimento alto produce un'alta precisione della ricerca con l'indice, ma comporta un grande lavoro di modifica dell'indice durante le operazioni CUD (Create, Update, Delete). Il valore di default del fattore di riempimento in genere è 70%. Può essere alzato nel caso di tabelle che vengono movimentate raramente.

    7. Creare SEMPRE un indice Clustered
      Un indice di tipo clustered, è un indice che indica esattamente l'ordine con il quale devono essere inseriti i dati all'interno della tabella. Può essere creato un solo indice clustered per ogni tabella che di solito coincide con la PrimaryKey.
      L'indice clustered per sua natura è sempre posizionato in corrispondenza dei dati.
  • Tip - Trigger su logon degli utenti

    Con il ServicePack 2 di SqlServer è stato introdotto l'evento Logon su cui è possibile agganciare un Trigger. In questo modo possiamo tracciare o controllare in modo più granulare il modo in cui gli utenti si collegano al nostro DataBase.
    Possiamo ad esempio evitare che due utenti si logghino contemporaneamente con le stesse credenziali, oppure possiamo limitare l'accesso ad alcuni utenti in certi orari della giornata o durante alcuni tipi di operazioni.

    Per creare un trigger di questo tipo dobbiamo utilizzare la seguente sintassi

    [code language="SQL"]

     

    CREATE TRIGGER tr_userlogon ON ALL SERVER FOR LOGON
    AS

    BEGIN

    -- mycode here.

    END

    [/code]

     

    Utilizzando il comando EVENTDATA() posso ricavare una struttura XML che mi informa sui dati legati all'evento.
    Inoltre utilizzando il comando ROLLBACK posso annullare la transazione corrente e quindi in questo tipo di trigger posso "rifiutare" il login dell'utente.

  • Performance Tuning - Utilizzare le PCC (Persistent Calculated Columns) per ottimizzare le performance delle nostre query.

    In SqlServer è possibile creare delle colonne calcolate in modo da dedurre alcuni valori da altri campi all'interno della stessa tabella.

    Molto spesso durante le operazioni di SELECT è necessario eseguire alcune operazioni di calcolo per ottenere dati complessi, calcolati sulla base di alcune colonne della tabella.
    Utilizzando le PCC è possibile spostare il carico di lavoro che SQL eseguirebbe durante le operazioni di SELECT per eseguire i calcoli, alla fase di inserimento e modifica dei dati.
    In pratica durante le operazioni di INSERT e UPDATE le PCC vengono ricalcolate memorizzate all'interno della tabella, alleggerendo il lavoro di calcolo che si avrebbe durante la SELECT.
    Il tutto è perfettamente trasparente alla operazione di insert/update che si limiterà ad inserire i dati.

dotNet Umbria 2007-2008
Powered by Community Server (Commercial Edition), by Telligent Systems