HAVIT Knowledge Base

Vývoj webových aplikací, .NET, SQL, návrh
Welcome to HAVIT Knowledge Base Sign in | Join | Help
-
Home Články Forums Obrázky Soubory

SQL

Microsoft SQL Server, Transact-SQL, Business Intelligence, CLR, ...

  • Microsoft Fest 2009 - Optimalizace DB aplikací - prezentace a dema ke stažení

    Na konferenci Microsoft Fest 2009 jsem dnes prezentoval přednášku "Optimalizace databázových aplikací v praxi", jedná se o částečně obměněnou podobu session již prezentované na SQL DevDays 2009. Slides, demo-kód i ukázkové databáze (atributová databáze a schema-killers) jsou k dispozici ke stažení.
  • SQL DevCon 2009: Optimalizace SQL dotazů v praxi - prezentace a dema z přednášky ke stažení

    V sekci soubory najdete ke stažení materiály (prezentaci a dema) z mé přednášky "Optimalizace SQL dotazů v praxi" na SQL DevCon 2009 Praha.
  • Transaction log narůstá, backup nepomáhá, shrink neúčinný - aktualizováno pro SQL2008

    Na MSSQL serveru se občas stává, že backupování přestane truncatovat transaction log a ten narůstá a narůstá.

    Z nějakého důvodu je potřeba udělat sekvenci shrink-backup-shrink, aby bylo dosaženo požadovaného účinku:

    1) Run this code:

    DBCC SHRINKFILE(pubs_log, 2)

    2) Run this code if you want to truncate the transaction log and not keep a backup of the transaction log. Truncate_only invalidates your transaction log backup sequence. Take a full backup of your database after you perform backup log with truncate_only:

    BACKUP LOG pubs WITH TRUNCATE_ONLY

    -or-

    Run this code if you want to keep a backup of your transaction log and keep your transaction log backup sequence intact. See SQL Server Books Online topic "BACKUP" for more information:

    BACKUP LOG pubs TO pubslogbackup

    3) Run this code:

    DBCC SHRINKFILE(pubs_log, 2)

    Aktualizace pro SQL 2008

    V SQL Serveru 2008 již není TRUNCATE_ONLY přepínač použitelný, místo toho je potřeba přepnout DB do Simple recovery modelu a udělat shrink logu. Pak je možné přepnout zpět na Full.

  • Vytvoření loginu pro doménovou skupinu

    V management nástrojích pro SQL Server 2005 nelze založit login pro doménovou skupinu.
    Login se mi podařilo založit pomocí příkazu
    create login [HAVIT\Development] from windows
    kde v našem případě je Development doménovou skupinou.

    Další pozorování je, že login doménové skupiny není možné pomocí management studia zakázat:
    • při nastavení Login na Disabled je zobrazena chyba,
    • nastavení Permissions to connect to database engine na Deny je ignorováno.

  • Uložení schématu databáze do SQL skriptu, z GUI i příkazové řádky

    Občas se Vám může hodit možnost vyskriptovat kompletní databázové schéma do SQL skriptu. Spuštěním takového skriptu pak můžete úplné DB schema zpětně reakonstruovat. Pokud jste si (ne)oblíbili schopnosti databázové edice Visual Studia stejně jako já, pak Vás potěším, že to jde i mnohem jednodušeji pomocí tzv. SQL Publishing Wizzardu přímo ze sady standardních management  nástrojů SQL Serveru 2005:

    Vyskriptování schématu DB z GUI

    1. Spusťte si SQL Server 2005 Management Studio
    2. V Object Exploreru pravým tlačítkem na dotčenou databázi a zvolte Tasks ~ Generate Scripts...
    3. Dole zaškrtněte "Script all objects in the selected database" (v horní části okna máte předvolenu dotčenou DB), pokračujte Next,
    4. Options - Podle své potřeby můžete upravit nastavení generátoru a ovlivnit podobu výsledného skriptu, např. zvolit jinou cílovou verzi SQL Serveru, pokračujte Next,
    5. Script Mode - zvolte, kam chcete skript vygenerovat - obvykle do souboru, pokračujte Next,
    6. Review nastavení + Finish

    ...a je hotovo.

    Vyskriptování schématu DB z příkazové řádky

    ...není nic jednoduššího:

    "C:\Program Files\Microsoft SQL Server\90\Tools\Publishing\1.2\sqlpubwiz" script -d DbName -S ServerName -U UserName -P Password TargetScriptFile.sql -schemaonly -f

    U nás tímto způsobem průběžně ukládáme ke každé solution aktuální schéma DB, tak, aby bylo v Subversion (source-control) vždy uložena příslušná verze schématu DB, s kterou aplikace pracuje.

    Pro úplnost dodávám, že pro synchronizaci databázových schémat a generování rozdílových skriptů používám RedGate SQL Compare.

  • Přenastavení počítadla pro identity sloupec (Identity Seed, RESEED)

    Přenastavení počítadla pro identity sloupec na 1:

    DBCC CHECKIDENT (MyTable, RESEED, 1)Počítadlo vyresetuje na výchozí hodnotu i TRUNCATE TABLE, který slouží pro vymazání celé tabulky.

    Více o DBCC CHECKIDENT [MSDN].

  • Rozdíl mezi COALESCE() a ISNULL()

    1. COALESCE() je z ANSI/ISO standardu SQL32, kdežto ISNULL() je jen T-SQL rozšíření (Microsoft SQL Serveru).
    2. ISNULL() má vlastní interpretaci, kdežto COALESCE() je pouze zkrácený zápis pro CASE strukturu a i se tak provádí:
      CASE
      WHEN (expression1 IS NOT NULL) THEN expression1
      ...
      WHEN (expressionN IS NOT NULL) THEN expressionN
      ELSE NULL
      END
    3. Typ výsledku ISNULL() je vždy dle prvního parametru, typ výsledku COALESCE() odpovídá CASE struktuře, tj. pokouší se o maximální záběr ze všech parametrů (SQL Server Books Online: "Returns the highest precedence type from the set of types in result_expressions and the optional else_result_expression.").
      DECLARE @Test char(2)
      SET @Test = NULL
      SELECT ISNULL(@Test, 'abcde'), COALESCE(@Test, 'abcde')
      Např. výše uvedený test vrací 'ab', 'abcde'.
    4. Vzhledem k převodu COALESCE() na CASE oproti vlastnímu provádění je ISNULL() mnohdy rychlejší.
    5. ISNULL() pochopitelně bere pouze dva parametry, kdežto COALESCE() víc.


  • Pozor na transakce - rollback není vždy automatický!

    Nezkušeného vývojáře, ale jak jsem se bohužel na vlastní oči mnohdy přesvědčil - mnohdy i velmi zkušeného vývojáře, dokáže zdrcujícím způsobem překvapit výsledek následujícího jednoduchého příkladu:

    CREATE TABLE TransactionTestTable
    (
        TransactionTestID int IDENTITY PRIMARY KEY,
        TransactionText nvarchar(100)
    )
    GO

    CREATE PROCEDURE TransactionTest
    AS
        BEGIN TRANSACTION
            
            -- Korektní SQL statement uvnitř transakce
            INSERT INTO TransactionTestTable(TransactionText) VALUES('První insert')

            -- Simulace chyby uvnitř transakce (do identity-column nelze zapisovat)
            INSERT INTO TransactionTestTable(TransactionTestID, TransactionText) VALUES(1, 'Druhý insert')

            -- Korektní SQL statement uvnitř transakce, po chybě
            INSERT INTO TransactionTestTable(TransactionText) VALUES('Třetí insert')

        COMMIT TRANSACTION
    GO

    EXEC TransactionTest
    SELECT * FROM TransactionTestTable
    GO

    ...spuštěná transakce (zde pouze pro formu zabalená do procedury TransactionTest) selže - druhý INSERT zfailuje, protože nemůže zapisovat přímo do identity-column. To zde není podstatné, je to pouze simulace pro zfailování statementu uvnitř transakce.

    Velkým překvapením mnohých však je, že v tabulce TransactionTestTable budou po skončení transakce následující data:

    TransactionTestID TransactionText
    1 První insert
    2 Třetí insert

    Zfailováním druhého statementu nedojde k automatickému rollbacku celé transakce!
    Run-time chyby statementů rollbackují automaticky pouze statement, který chybu způsobil!

    Pokud chceme, aby run-time chyba automaticky zrollbackovala celou transakci, musíme nastavit SET XACT_ABORT ON:

    CREATE PROCEDURE TransactionTest
    AS
        SET XACT_ABORT ON
        BEGIN TRANSACTION
            
            -- Korektní SQL statement uvnitř transakce
            INSERT INTO TransactionTestTable(TransactionName) VALUES('První insert')

            -- Chybný SQL statement uvnitř transakce, do identity-column nelze zapisovat
            INSERT INTO TransactionTestTable(TransactionTestID, TransactionName) VALUES(1, 'Druhý insert')

            -- Korektní SQL statement uvnitř transakce, po chybě
            INSERT INTO TransactionTestTable(TransactionName) VALUES('Třetí insert')

        COMMIT TRANSACTION
    GO
    Jemněji než automatický rollback pomocí SET XACT_ABORT ON lze v praxi použít ošetření chyb pomocí TRY..CATCH:

    USE AdventureWorks;
    GO
    BEGIN TRANSACTION;

    BEGIN TRY
       -- Generate a constraint violation error.
       DELETE FROM Production.Product
          WHERE ProductID = 980;
    END TRY
    BEGIN CATCH
       SELECT 
          ERROR_NUMBER() AS ErrorNumber,
          ERROR_SEVERITY() AS ErrorSeverity,
          ERROR_STATE() as ErrorState,
          ERROR_PROCEDURE() as ErrorProcedure,
          ERROR_LINE() as ErrorLine,
          ERROR_MESSAGE() as ErrorMessage;

       IF @@TRANCOUNT > 0
          ROLLBACK TRANSACTION;
    END CATCH;

    IF @@TRANCOUNT > 0
       COMMIT TRANSACTION;
    GO
    ...případně "postaru" testovat @@ERROR <> 0 po každém statementu.

  • Service Broker: Notifikace .NET aplikace z SQL serveru (events, queries, messages)

    SQL Server 2005 přichází s novou funkčností tzv. Service Brokera - komplexní message-based komunikační platformy, s frontami a dalšími prvky potřebnými pro stavbu robustních SOA aplikací (Service Oriented Architecture).

    Podrobněji o Service Brokeru viz SQL Books Online, pro nás je pro tuto chvíli podstatné, že prostřednictvím Service Brokera lze implementovat rozumnou podobu notifikací z SQL serveru do .NET aplikací, kdy jsou .NET aplikace SQL serverem informovány o určitých událostech (např. různých CREATE, DROP, ALTER DDL příkazech), o změnách dat (tzv. query notifications) či jakýchkoliv jiných zprávách, které si sami iniciujeme (např. z trigerů, atp.). Jak dále uvidíme, dotnetovské SqlCacheDependency či SqlDependency nejsou nic jiného, než konkrétním využitím Service Brokera.

    Základní principy

    Podívejme se nejprve na základní principy práce Service Brokera:

    1. Komunikace prostřednictvím Service Brokera je založena na zprávách (MESSAGE). Každá jednotlivá zpráva představuje příslušnou událost, příkaz, notifikaci, prostě atomickou komunikační jednotku.
    2. Zprávy jsou uchovávány ve frontách (QUEUE). Fronta je určitým bufferem mezi odesílatelem a příjemcem zprávy.
    3. Formální požadavky, které musí zpráva splňovat (např. XML schema), určuje typ zprávy (MESSAGE TYPE).
    4. Zprávy se posílají v konverzaci (DIALOG CONVERSATION) mezi dvěma endpointy, které představují služby (SERVICE). Cesty Mezi různými instancemi SQL serveru lze zprávy předávat pomocí routů (ROUTE).
    5. Formální požadavky na konverzaci/dialog určuje CONTRACT, který mj. určuje typ zpráv vyměňovaných mezi službami a směr, kterým se posílají.
    6. Zpráva může mimo explicitní konverzace vzniknout např. i událostí (EVENT) na straně SQL Serveru (např. DDL události CREATE, ALTER, DROP, nebo trace události, jak je známe z Profileru), nebo prostřednictvím sledování aktualizací dat (Query Notification), kdy SQL Server sleduje výsledky určitého SQL dotazu a oznámí jejich změnu (klasicky využíváno pro expiraci datové cache ASP.NET).
    7. Podstatné je, že Service Broker sám neinicializuje komunikaci mimo SQL Server, do .NET aplikace, naopak .NET aplikace si musí zprávu sama vyzvednout z fronty (RECEIVE), resp. může využít konstrukce WAITFOR, čímž v případě prázdné fronty pasivně vyčkává, než se zpráva objeví.

    Výše uvedené není ani zdaleka vyčerpávajícím popisem fungování Service Brokeru, pro naše účely však postačuje jako úvod do problematiky.

    Pro dále popisovanou funkčnost je potřeba mít Service Brokera na databázi zapnutého:

    ALTER DATABASE AdventureWorks SET ENABLE_BROKER

    Query Notifications - SqlNotificationRequest, SqlDependency, SqlCacheDependency

    Nejběžnějším způsobem využití Service Brokera v .NET aplikaci jsou Query Notification Services - novinka SQL Serveru 2005 spočívají v možnosti vyžádat (subscribe) sledování výsledku určitého SQL dotazu (query). V případě změny pak dojde k vytvoření příslušné zprávy (message), jejímu uložení do fronty (queue), odkud si ji z naší .NET aplikace vyzvedneme a o aktualizaci na straně SQL Serveru se tak dozvíme (a může tak například dojít k vyřazení určitých dat z cache atp.).

    Základní principy Query Notifications
    1. Query Notifications nelze aktivovat z T-SQL, ani CLR kódu hostovaného v SQL Serveru. Query Notifications lze aktivovat pouze prostřednictvím klientské aplikace, v případě .NET Frameworku prostřednictvím třídy SqlNotificationRequest (vlastnost SqlCommand.Notification).
    2. SqlNotificationRequest je svázán s příkazem (SqlCommand), kterému je nastaven a teprve vykonáním tohoto příkazu se provede příslušná subscription QN na straně SQL serveru.
    3. SQL Server sleduje pouze první změnu výsledku dotazu, tím QN končí a případný další notification request je potřeba iniciovat novým vykonáním příkazu.
    4. V žádném případě se nejedná o komunikaci iniciovanou SQL Serverem, nýbrž si musíme z příslušné fronty sami zprávu o aktualizaci vyzvednout, resp. si obsah fronty průběžně hlídat.
    5. V případě SqlDependency a SqlCacheDependency za nás vyzvednutí zprávy z příslušné fronty Service Brokera zajišťuje samotný .NET Framework prostřednictvím opakovaného volání příkazu WAITFOR (RECEIVE ...) TIMEOUT v kontinuálně otevřené samostatné SqlConnection ze samostatného threadu naší aplikace. Přesvědčit se o tom můžete v SQL Profileru.
    Ilustrační příklad SqlDependency:

        class Program
        {
            static void Main(string[] args)
            {
                const string connString = "Server=localhost;Database=AdventureWorks;Integrated Security=true;";
                using (SqlConnection conn = new SqlConnection(connString))
                {
                    conn.Open();

                    SqlCommand cmd = new SqlCommand("SELECT Bonus FROM Sales.SalesPerson");
                    cmd.Connection = conn;

                    SqlDependency dependency = new SqlDependency(cmd);
                    dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);
                    SqlDependency.Start(connString);

                    cmd.ExecuteNonQuery();

                    Console.ReadLine();
                }

            }

            static void dependency_OnChange(object sender, SqlNotificationEventArgs e)
            {
                Console.WriteLine("Bonus changed...");
            }
        }

    SqlNotificationRequest

    Pokud nám nestačí vyšší programátoská abstrakce SqlDependency či SqlCacheDependency a potřebujeme řídit Query Notifications podrobněji, můžeme využít třídu SqlNotificationRequest, resp. instanční vlastnost SqlCommand.Notification, kam instanci třídy SqlNotificationRequest přiřadíme a zajistíme tak subscribování daného příkazu do Query Notifications Service. Na rozdíl od SqlDependency pak ale musíme sami zajistit vyzvedávání zpráv z příslušné fronty a jejich zpracování (viz níže).

    Požadavky na sledovaný SQL dotaz

    Dotaz, ke kterému chceme QueryNotification aktivovat, musí splňovat určité nemalé restrikce:

    • jména tabulek musí být uváděna včetně schématu, tedy Sales.SalesPerson, dbo.MojeTabulka, atp.
    • nelze použít SELECT *, vždy musíme udělat výčet sloupců
    • nelze použít agregační funkce
    • nelze používat subqueries, outer-joins, self-joins

    Podrobný výčet omezení viz např. http://msdn2.microsoft.com/en-US/library/ms181122.aspx.

    Event Notifications

    Dalším typem zpráv, které můžeme od SQL Serveru prostřednictvím Service Brokera odebírat, jsou tzv. Event Notifications. V zásadě se jedná o DDL a trace události na straně serveru, ať už na úrovni databáze nebo serveru jako celku. Např. CREATE_DATABASE, ALTER_PROCEDURE, DROP_ASSEMBLY, Audit_Login, SP_Recompile, atp. atp. Události jsou hiearchicky uspořádány a lze se přihlásit i k odběru celé skupiny najednou.

    Na rozdíl od Query Notifications, kdy se nám příslušné prvky Service Brokera vytvářely převážně automaticky, zde již musíme provést základní inicializaci ručně. Na rozdíl od Query Notifications se Event Notifications celé řídí prostřednictvím T-SQL příkazů. Jednoduchý příklad by mohl vypadat nějak takto:

    CREATE QUEUE MyNotifyQueue;

    CREATE SERVICE MyNotifyService
        ON QUEUE MyNotifyQueue
        (
        [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]
        )


    CREATE ROUTE MyNotifyRoute WITH SERVICE_NAME = 'MyNotifyService', ADDRESS = 'LOCAL';

    CREATE EVENT NOTIFICATION Notify_AlterProcedure
        ON DATABASE
        FOR ALTER_PROCEDURE
        TO SERVICE 'MyNotifyService', 'current database'
    MESSAGE TYPE pro události je již v systému pod názvem PostEventNotification připraven, vytvoříme tedy jen frontu (CREATE QUEUE), službu (CREATE SERVICE), routu (CREATE ROUTE) a pak už samotnou event-notifikace (CREATE EVENT NOTIFICATION). Tím je na straně SQL Serveru vše připraveno a nyní už můžeme jen vyzvedávat zprávy na straně .NET aplikace:

        class Program
        {
            static bool done = false;

            static void Main(string[] args)
            {
                Thread t = new Thread(ReceiveEvent);
                t.Start();
                
                while (!done)
                {
                    Thread.Sleep(1000);
                    Console.Write(".");
                }

                Console.ReadLine();
            }

            static void ReceiveEvent()
            {
                const string connString = "Server=localhost;Database=AdventureWorks;Integrated Security=true;";

                using (SqlConnection conn = new SqlConnection(connString))
                {
                    conn.Open();

                    SqlCommand cmd = new SqlCommand();
                    cmd.Connection = conn;
                    cmd.CommandText = "WAITFOR (RECEIVE TOP(1) * FROM MyNotifyQueue), TIMEOUT @Timeout";
                    cmd.Parameters.AddWithValue("@Timeout", 60000); // 60s

                    do
                    {
                        SqlDataReader reader = cmd.ExecuteReader();
                        if (reader.Read())
                        {
                            OnEvent();
                        }
                        done = true;
                    }
                    while (!done);
                }
            }

            static void OnEvent()
            {
                Console.WriteLine("Event received...");
            }
        }
    Obdobně jako interní implementace SqlDependency zde v samostatném threadu provádíme průběžný polling WAITFOR(RECEIVE ...) s příslušným timeoutem, zatímco v hlavním vlákně běží aplikace dál (obsluha UI, atp.). Receiving-vláknu bychom také měli nastavit vlastnost IsBackground na true, aby při skončení hlavního vlákna došlo k ukončení tohoto pollingu.

    Vlastní zprávy odesíláné z SQL Serveru (např. z triggerů)

    Dalším zdrojem zpráv předávaných prostřednictvím Service Brokera mohou být i zprávy, které sami vytvoříme, jejichž odeslání sami inicializujeme - např. z různých triggerů (DML i DDL), nebo uložených procedur.

    Pro tuto chvíli přesahuje podrobný popis zamýšlený rozsah tohoto článku, nicméně jak je už z výše uvedeného zřejmé, nejde opět o nic jiného než o inicializaci jednotlivých prvků Service Brokera (vše T-SQL) a dále posílání zpráv prostřednictvím T-SQL příkazů BEGIN DIALOG a SEND. Samotná .NET aplikace by pak byla stejný princip, jak u Event Notifikacions, jen formát zpráv se bude lišit podle toho, jak si ho zadefinujeme (zprávy mohou být i prázdné, nebo plain-text).

    Mnohé příklady odesílání zpráv přes Service Brokera viz SQL Books Online. Možná tento článek rozšířím někdy později.

  • Nelze použít Maintenance Plan Wizzard v SQL 2005 Management Studiu (jen na x64 verzi)

    Při pokusu použít Maintenance Plan Wizzard v SQL 2005 Management studiu jsem získal při pokusu o naplánování úlohy chybu, že to není možné. Po instalaci SP1 se situace změnila v tom duchu, že Maintenance Plan Wizzard nebylo možné otevřít vůbec - Studio si stěžovalo na blíže nespecifikovanou chybějící komponentu. Ve formuláři je uveden odkaz vedoucí na návod k instalaci (k ničemu), problém se vyskytuje pravděpodobně jen v x64 verzi.
    The action you attempted to perform on a remote instance of SQL
    Server has failed because the action requires a SQL Server component
    that is not installed on the remote computer. To proceed, install SQL
    Server 2005 Management Tools on the remote computer, and then try
    again.  For more information, see "How to: Install SQL Server 2005
    (Setup)" in SQL Server 2005 Books Online, or find the article on MSDN
    at http://go.microsoft.com/fwlink/?LinkID=57083 .
    (Microsoft.SqlServer.Management.MaintenancePlanWizard)
    
    For help, click: http://go.microsoft.com/fwlink/?LinkID=57083
    Řešení zdá se být jednoduché - stačí nainstalovat Integration services, které onu neznámou komponentu obsahují. SP2 by měl být upraven tak, aby komponenta byla součástí databázového jádra.

    Zdroj: Maintenance Plan problem
  • @@IDENTITY vs. SCOPE_IDENTITY()

    Velmi často se setkávám se zaměňováním "funkcí" @@IDENTITY a SCOPE_IDENTITY(). Obě dvě dvě představují hodnotu identity-sloupce posledního INSERTu, nicméně každý trochu jinak definovanou a v určitých případech se mohou jejich výsledky lišit.

    Definice

    @@IDENTITY představuje poslední vloženou identity hodnotu v kontextu celé session, veškeré její aktivity.

    SCOPE_IDENTITY() představuje poslední vloženou identity hodnotu v kontextu aktuálního scope, v nejužším slova smyslu.

    Příklad

    Mějme tabulku Table1 s identity-column a k ní insert trigger, který vkládá hodnotu do druhé tabulky Table2 opět s identity-column. A tady je rozdíl, pokud zavoláme dávku

    INSERT INTO Table1 DEFAULT VALUES

    SELECT @@IDENTITY -- vrátí identity hodnotu Table2 z triggeru
    SELECT SCOPE_IDENTITY() -- vrátí identity hodnotu Table1

    ...s použitím @@IDENTITY si můžeme pěkně naběhnout.

    IDENT_CURRENT('table')

    Funkce IDENT_CURRENT('tablename') vrací poslední hodnotu identity-column tabulky, nezávisle na session či scope.

  • SQL2005: Odmazávání starších záloh

    Na SQL 2005 Serveru mě velice překvapilo, že pomocí základního maintenance-planu již nelze nastavit automatické odmazávání starších záloh, starších .BAK souborů.

    Řešení je naštěstí snadné, i když ho bohužel pomocí wizzardu nedosáhneme. Stačí však ručně modifikovat (pravým tlačítkem - Modify, nebo jen double-click) wizzardem vygenerovaný maintenance-plan a přidat do něj novou úlohu - Maintenance Cleanup Task, v jehož vlastnostech pouze nastavíme v jaké složce máme zálohy, jestli se mají procházet i podsložky a jak staré zálohy se mají zlikvidovat.

    Přidání je opravdu snadné, je to snad na tři kliknutí (přetáhnout z toolboxu, nastavit vlastnosti a navázat do workflow protažením příslušné šipky). Na zvážení administrátorů nechávám, jakou závislost čištění udělat na zálohování (poklikáním na vazbu můžeme volit Success, Error, nebo jen Completion). Teoreticky tedy můžeme čištění podmínit úspěšným zálohováním, aby nám po čase nezmizely staré soubory a nové nevznikaly.

     

  • Propojení databázového uživatele na login (sp_change_users_login)

    Při přesunech databází mezi servery, obnovování ze záloh a podobných úkonech se nám může stát, že se ztratí propojení mezi databázovým uživatelem (User) a jeho loginem (SQL Server login). Pomocí běžných management-nástrojů pak nelze toto propojení obnovit.

    Propojení obnovíme pomocí stored procedury sp_change_users_login:

    USE mydb
     
    -- Auto_Fix, pokud mají user i login stejné jméno, pokud login není, bude vytvořen
    -- můžeme přidat i parametr @Password, který se použije, pokud bude login zakládán nově
    EXEC sp_change_users_login @Action='Auto_Fix', @UserNamePattern='user'
     
    -- Update_One použijeme, pokud se nám jména neshodují
    EXEC sp_change_users_login @Action='Update_One', @UserNamePattern='user', @LoginName='username'

    ...tuto metodu nelze použít pro Windows-loginy, pouze pro SQL Server loginy.

    Pro SQL2000 lze použít GUI utilitu Db Maint Sync SQL Logins.

  • Jsou dvě data (small)datetime ze stejného dne?

    Potíž je v tom, že typ (small)datetime obsahuje mimo údaje data i čas. Můžeme samozřejmě zařídit, aby naše záznamy měly tento čas 00:00, ale stejně na tento problém narazíme třeba při porovnávání s aktuálním datem u GETDATE(), které je včetně času.

    Můžeme tedy buď porovnávat po složkách:

    ...
    WHERE
       (YEAR(Datum) = YEAR(GETDATE()))
       AND (MONTH(Datum) = MONTH(GETDATE())
       AND (DAY(Datum) = DAY(GETDATE())

    Nebo použít přetypování na číslo, které udává vzdálenost od určitého nultého dne a to ve dnech:

    ...
    WHERE
       FLOOR(CONVERT(float, Datum)) = FLOOR(CONVERT(float, GETDATE())

    Na běžných objemech dat se mi nepodařilo na SQL2005 naměřit žádný rozdíl v rychlosti, na obrovských objemech by to stálo za vyzkoušení, teoreticky by mohlo lépe vycházet to druhé.

    Každopádně mi při běžném použití přijde přehlednější ta první varianta, ten druhý zápis by osobu neseznámenou mohl pěkně potrápit.

  • IntArray - pole hodnot typu int jako UDT + aggregate (CLR)

    Na SQL serveru do verze 2000 mě štvalo, že nemá žádnou rozumnou práci s poli. Jakmile tedy vyšel SQL2005 s možností použití .NET Frameworku (CLR), napsal jsem si hned vlastní typ IntArray. Nyní, po půl roce jeho používání, mohu říct, že se výborně osvědčil.

    Typ IntArray samozřejmě není primárně určen pro použití v tabulkách, kde by sloužil jako typ sloupce, tím bychom degradovali relační schéma. Co nám tedy takové pole prvků typu int usnadní?

    • můžeme snadno psát parametrické dotazy s podmínkou WHERE CiselnikID IN [1, 2, 3, ...], typické pro vyhledávání - máme-li například tabulku osob, číselník jejich pracovních pozic a chceme-li vypsat několik pozic najednou - například všechny účetní, personalisty a uklízečky k tomu,
    • můžeme snadno "jedním vrzem" (do jedné řádky) načítat objekty i s položkami - pokud používáme v Business Layer klasický přístup s ghost objekty inicializovanými svým ID a s lazyloadem dalších hodnot, pak můžeme krásně načítat nadřazený objekt i s jeho členskými kolekcemi. Například v objednavka.Load() načteme data objednávky a zároveň pole IDček všech řádek objednávky, kterým hned vytvoříme ghost objekty,
    • stejně jako načítat, můžeme snadno ukládat vybrané kolekce objektu, například můžeme snadno uložit jedním vrzem uživatele i se seznamem jeho rolí.

    SqlInt32Array

    První, co tedy potřebujeme, je samotný CLR user-defined type:

    using System;
    using System.Data;
    using Microsoft.SqlServer.Server;
    using System.Data.SqlTypes;
    using System.Text;
    using System.Collections;
    using System.Collections.Generic;
      
    namespace Havit.Data.SqlTypes
    {
     [Serializable]
     [SqlUserDefinedType(
      Format.UserDefined,
      Name = "IntArray",
      IsByteOrdered = true,
      MaxByteSize = 8000)]
     public class SqlInt32Array : INullable, IBinarySerialize
     {
      #region private value holder
      private List<SqlInt32> values = null;
      #endregion
     



      #region Constructors
      /// <summary>
      /// Vytvoří instanci s hodnotou NULL.
      /// </summary>
      public SqlInt32Array()
      {
       this.values = null;
      }
     

      /// <summary>
      /// Vytvoří instanci a naplní ji předanými hodnotami.
      /// </summary>
      /// <param name="values">hodnoty, které mají instance reprezentovat</param>
      public SqlInt32Array(int[] values)
      {
       if ((values == null) || (values.Length == 0))
       {
        this.values = null;
        return;
       }
       if (values.Length > 1999)
       {
        throw new ArgumentException(String.Format("Maximální velikost pole je 1999 hodnot, požadováno je však {0} hodnot.",
         values.Length));
       }

       this.values = new List<SqlInt32>();
       for (int i = 0; i < values.Length; i++)
       {
        this.values.Add(new SqlInt32(values[i]));
       }
      }
      #endregion
      

      #region Add
      /// <summary>
      /// Přidá prvek do pole.
      /// </summary>
      /// <param name="value"></param>
      public void Add(SqlInt32 value)
      {
       if (!value.IsNull && (value.Value == Int32.MinValue))
       {
        throw new ArgumentException("Prvek nesmí mít vyhrazenou hodnotu Int32.MinValue.");
       }

       if (this.values == null)
       {
        this.values = new List<SqlInt32>();
       }
       values.Add(value);
      }
      #endregion

      #region Count
      /// <summary>
      /// Počet prvků v seznamu.
      /// </summary>
      public int Count
      {
       get { return values.Count; }
      }
      #endregion
      

      #region Indexer
      /// <summary>
      /// Indexer pro přístup k prvkům podle jejich pořadí.
      /// </summary>
      /// <param name="index">index (pořadí) prvku</param>
      /// <returns>hodnota <see cref="SqlInt32"/></returns>
      public SqlInt32 this[int index]
      {
       get { return values[index]; }
      }
      #endregion
      

      #region Merge
      /// <summary>
      /// Spojí dvě pole v jedno.
      /// </summary>
      /// <param name="array">přidávané pole</param>
      public void Merge(SqlInt32Array array)
      {
       if (!array.IsNull)
       {
        for (int i = 0; i < array.values.Count; i++)
        {
         this.values.Add(array.values[i]);
        }
       }
      }
      #endregion
      

      #region Accessors
      /// <summary>
      /// Vrátí pole SqlInt32[] s hodnotami.
      /// </summary>
      /// <returns>Pole SqlInt32[] s hodnotami.</returns>
      public SqlInt32[] GetSqlInt32Array()
      {
       if (this.IsNull)
       {
          return null;
       }
       return (SqlInt32[])values.ToArray();
      }

      /// <summary>
      /// Vrací tabulku Int32 hodnot.
      /// Metoda určená pro mapování do T-SQL na table-valued function (TVF).
      /// </summary>
      /// <param name="values">Proměnná, která má být rozbalena do tabulky hodnot Int32.</param>
      /// <returns>tabulka Int32 hodnot (pomocí FillInt32Row)</returns>
      [SqlFunctionAttribute(
       Name= "IntArrayToTable",
       TableDefinition = "[Value] int",
       FillRowMethodName = "FillSqlInt32Row")]
      public static IEnumerable GetSqlInt32Values(SqlInt32Array values)
      {
       return values.GetSqlInt32Array();
      }

      /// <summary>
      /// Metoda zajišťující převod řádku v table-valued function (TVF).
      /// </summary>
      /// <param name="int32ArrayElement">vstupní hodnota řádku</param>
      /// <param name="value">výstupní hodnota řádku</param>
      public static void FillSqlInt32Row(object sqlInt32ArrayElement, out SqlInt32 value)
      {
       value = (SqlInt32)sqlInt32ArrayElement;
      }
      #endregion
      

      #region Parse
      /// <summary>
      /// Vytvoří z CSV textové reprezentace hodnotu pole.
      /// </summary>
      /// <param name="text">CSV text hodnot</param>
      /// <returns>pole s hodnotami dle CSV</returns>
      [SqlMethod(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
      public static SqlInt32Array Parse(SqlString text)
      {
       if (text.IsNull)
       {
        return Null;
       }
       string[] parts = text.Value.Split(',');
       int length = parts.Length;
       SqlInt32Array result = new SqlInt32Array();
       for (int i = 0; i < length; i++)
       {
        if (String.Compare(parts[i].Trim(), "NULL", true) == 0)
        {
         result.Add(SqlInt32.Null);
        }
        else
        {
         result.Add(new SqlInt32(Convert.ToInt32(parts[i])));
        }
       }
       return result;
      }
      #endregion
      

      #region ToString
      /// <summary>
      /// Převede hodnotu na CSV textovou reprezentaci string
      /// </summary>
      /// <returns>CSV seznam hodnot</returns>
      public override string ToString()
      {
       if (this.IsNull)
       {
        return null;
       }

       StringBuilder sb = new StringBuilder();
       for (int i = 0; i < this.values.Count; i++)
       {
        if (this.values[i].IsNull)
        {
         sb.Append("NULL");
        }
        else
        {
         sb.Append(this.values[i].Value);
        }
        if (i < this.values.Count - 1)
        {
         sb.Append(",");
        }
       }
       return sb.ToString();
      }
      #endregion
      

      #region Null (static)
      /// <summary>
      /// Hodnota NULL.
      /// </summary>
      public static SqlInt32Array Null
      {
       get
       {
        return new SqlInt32Array();
       }
      }
      #endregion
      

      #region INullable Members
      /// <summary>
      /// Indikuje, zda-li je hodnota NULL.
      /// </summary>
      public bool IsNull
      {
       get
       {
        return ((this.values == null) || (this.values.Count == 0));
       }
      }
      #endregion
      

      #region IBinarySerialize Members
      /// <summary>
      /// Načte hodnotu z binární reprezentace.
      /// </summary>
      /// <remarks>
      /// Binární serializace je takováto:
      /// byte 1-4 ~ Int32 Length (velikost pole, pokud je 0, pak je hodnota NULL)
      /// byte 5-(8000) ~ values (NULL hodnoty reprezentuje Int32.MinValue)
      /// </remarks>
      /// <param name="r"><see cref="System.IO.BinaryReader"/> s binární reprezentací hodnoty</param>
      public void Read(System.IO.BinaryReader r)
      {
       // byte 1 - počet hodnot
       Int32 length = r.ReadInt32();
       if (length == 0)
       {
        // NULL
        this.values = null;
       }
       else
       {
        // hodnoty
        this.values = new List<SqlInt32>();
        for (int i = 0; i < length; i++)
        {
         Int32 temp = r.ReadInt32();
         if (temp == Int32.MinValue)
         {
          this.values.Add(SqlInt32.Null);
         }
         else
         {
          this.values.Add(new SqlInt32(temp));
         }
        }
       }
      }
     

      /// <summary>
      /// Vytvoří binární reprezentaci hodnoty.
      /// </summary>
      /// <remarks>
      /// Binární serializace je takováto:
      /// byte 1-4 ~ Int32 Length (velikost pole, pokud je 0, pak je hodnota NULL)
      /// byte 5-(8000) ~ values (NULL hodnoty implementuje Int32.MinValue)
      /// </remarks>
      /// <param name="w"><see cref="System.IO.BinaryWriter"/> do kterého má být binární reprezentace zapsána</param>
      public void Write(System.IO.BinaryWriter w)
      {
       // byte 1 - počet hodnot
       if (this.IsNull)
       {
        w.Write(0);
       }
       else
       {
        w.Write(this.values.Count);
        // hodnoty
        for (int i = 0; i < this.values.Count; i++)
        {
         if (this.values[i].IsNull)
         {
          w.Write(Int32.MinValue);
         }
         else
         {
          w.Write(this.values[i].Value);
         }
        }
       }
      }
      #endregion
     }
    }

    Jak jste si jistě všimli, mimo typu IntArray zavádíme hned i UDF funkci IntArrayToTable, kterou bude v T-SQL pole převádět na tabulku.

    Náš UDT můžeme do SQL serveru zavést buď přímo z Visual Studia pomocí Deploy, pokud máme jako typ projektu SQL Server Project, nebo ručně pomocí T-SQL:

    CREATE ASSEMBLY [Havit.Data.SqlServer]
    FROM 'C:\Havit.Data.SqlServer.dll'
     
    CREATE TYPE [dbo].IntArray
    EXTERNAL NAME [Havit.Data.SqlServer].[Havit.Data.SqlTypes.SqlInt32Array]
     
    CREATE FUNCTION IntArrayToTable
    (
        @array dbo.IntArray
    )
    RETURNS TABLE
    (
         [Value] int
    )
    AS EXTERNAL NAME [Havit.Data.SqlServer].[Havit.Data.SqlTypes.SqlInt32Array].[GetInt32Values]

    Příklad vyhledávání:

    CREATE PROCEDURE Filter
    (
         @Vlastnosti dbo.IntArray = NULL
    )
    AS
        SELECT col FROM tab
            WHERE ((@Vlastnosti IS NULL) OR (VlastnostID IN (SELECT Value FROM dbo.IntArrayToTable(@Vlastnosti))))

    Příklad využití pole IDček pro ukládání:

    CREATE PROCEDURE dbo.Uzivatel_Update
    (
     @UzivatelID int,
     @Username varchar(30),
     @Password nvarchar(30),
     @DisplayAs nvarchar(50),
     @Email nvarchar(80),
     @Deleted bit = 0,
     @Role IntArray = NULL
    )
    AS
     SET NOCOUNT ON
       
     SET XACT_ABORT ON
     BEGIN TRANSACTION
       
      -- Update tabulky uživatelů
      UPDATE dbo.Uzivatel
       SET
        Username = @Username,
        Password = @Password,
        DisplayAs = @DisplayAs,
        Email = @Email,
        Deleted = @Deleted
       WHERE
        (UzivatelID = @UzivatelID)
          
      -- Update rolí
      DELETE FROM dbo.Uzivatel_Role
       WHERE
        (UzivatelID = @UzivatelID)
        
      IF (@Role IS NOT NULL)
       INSERT INTO dbo.Uzivatel_Role(UzivatelID, RoleID)
        SELECT @UzivatelID AS UzivatelID, Value AS RoleID FROM dbo.IntArrayToTable(@Role)
       
     COMMIT TRANSACTION
      
     RETURN

    Použití UDT z aplikace pak vypadá nějak takto:

       SqlParameter paramRole = new SqlParameter("@Role", SqlDbType.Udt);
       paramRole.UdtTypeName = "IntArray";
       paramRole.Value = new SqlInt32Array(this.Role.GetIDs());
       cmd.Parameters.Add(paramRole);

    SqlInt32ArrayAggregate

    Na to, abychom z tabulky se sloupcem int hodnot dostali pole, potřebujeme agregát. Tedy obdobu SUM, AVG, MAX, MIN, prostě něco, co udělá ze všech hodnot jednu.

    using System;
    using System.Data;
    using System.Data.SqlClient;
    using System.Data.SqlTypes;
    using Microsoft.SqlServer.Server;
    using System.IO;
     
    namespace Havit.Data.SqlTypes
    {
     /// <summary>
     /// Aggregate k UDT SqlInt32Array, který zajišťuje převod tabulky hodnot na pole.
     /// </summary>
     [Serializable]
     [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
      Format.UserDefined,
      IsInvariantToDuplicates = false, IsInvariantToNulls = false, IsInvariantToOrder = true, IsNullIfEmpty = true,
      MaxByteSize = 8000, Name = "IntArrayAggregate")]
     public class SqlInt32ArrayAggregate : IBinarySerialize
     {
      #region private value holder
      /// <summary>
      /// Uchovává mezivýsledek.
      /// </summary>
      private SqlInt32Array array;
      #endregion
     
      #region Init
      /// <summary>
      /// Inicializace agregátoru.
      /// </summary>
      public void Init()
      {
       array = new SqlInt32Array();
      }
      #endregion
     
      #region Accumulate
      /// <summary>
      /// Přidá další hodnotu do agregace.
      /// </summary>
      /// <param name="value">přidávaná hodnota</param>
      public void Accumulate(SqlInt32 value)
      {
       array.Add(value);
      }
      #endregion
     
      #region Merge
      /// <summary>
      /// Spojí dva agregáty v jeden
      /// </summary>
      /// <param name="group">druhá agregace</param>
      public void Merge(SqlInt32ArrayAggregate group)
      {
       group.array.Merge(group.array);
      }
      #endregion
     
      #region Terminate
      /// <summary>
      /// Vrátí výsledek agregace.
      /// </summary>
      public SqlInt32Array Terminate()
      {
       return this.array;
      }
      #endregion
     
      #region IBinarySerialize Members
      /// <summary>
      /// De-serializuje agregaci.
      /// </summary>
      /// <param name="r">BinaryReader</param>
      public void Read(BinaryReader r)
      {
       this.array = new SqlInt32Array();
       this.array.Read(r);
      }
     
      /// <summary>
      /// Serializuje agregaci.
      /// </summary>
      /// <param name="w">BinaryWriter</param>
      public void Write(BinaryWriter w)
      {
       this.array.Write(w);
      }
      #endregion
     }
    }

    A příklad použití takového agregátu:

    CREATE PROCEDURE dbo.Uzivatel_Load
    (
     @UzivatelID int
    )
    AS
     SET NOCOUNT ON
      
     SELECT
       Uzivatel.*,
       (SELECT dbo.IntArrayAggregate(RoleID) FROM dbo.Uzivatel_Role WHERE UzivatelID = @UzivatelID) AS Role
      FROM dbo.Uzivatel
      WHERE
       (UzivatelID = @UzivatelID)
      
     RETURN

    ... a jeho využití v aplikaci:

    SqlInt32Array roleArray = (SqlInt32Array)reader["Role"];

     ...určitě se toho dá mnohé vylepšovat, uvítám samozřejmě jakékoliv komentáře.

More Posts Next page »