Страницы

четверг, 9 октября 2014 г.

Автоматизация резервного копирования и восстановления многомерных баз данных.

Когда мы хотим создать на SQL Server-е автоматическое задание, которое бы периодически запускало хранимую процедуру, то используется SQL Server агент. Что делать, если аналогичную процедуру, например, резервное копирование, требуется выполнить для многомерной базы данных на OLAP-сервере? Для решения этой задачи также можно воспользоваться SQL Agent-ом. Только сначала на SQL Server-е необходимо создать связанный сервер к источнику данных:

use master
exec master.dbo.sp_addlinkedserver @server = N'SSAS', @srvproduct = N'', @provider = N'MSOLAP', @datasrc = N'EDYNAK\SHADOW'
exec master.dbo.sp_addlinkedsrvlogin @rmtsrvname = N'SSAS', @useself = N'False', @locallogin = null, @rmtuser = null, @rmtpassword = null
exec master.dbo.sp_serveroption @server = N'SSAS', @optname = N'data access', @optvalue=N'true'
exec master.dbo.sp_serveroption @server = N'SSAS', @optname = N'rpc', @optvalue=N'true'
exec master.dbo.sp_serveroption @server = N'SSAS', @optname = N'rpc out', @optvalue = N'true'

В качестве провайдера используется MOLAP, поскольку для задач администрирования в Analysis Services используется не MDX, а XML/A. После создания связанного сервера можно написать хранимую процедуру, которая будет выполнять резервное копирование многомерных баз данных:
if sessionproperty ( N'quoted_identifier' ) = 0
       set quoted_identifier on
go

if sessionproperty ( N'ansi_nulls' ) = 0
       set ansi_nulls on
go

if object_id ( N'dbo.CheckSrvAndDb', N'FN' ) is null
       exec ( N'create function dbo.CheckSrvAndDb ( @i int ) returns int as begin return 0 end' )
go

alter function dbo.CheckSrvAndDb
(
    @SrvName     nvarchar ( 200 ),
    @DbName      nvarchar ( 200 )
)
    returns nvarchar ( max )
as
begin
    declare @DataSource nvarchar ( 200 )
    if not exists
    (
        select 1
        from sys.servers
        where name = @SrvName
    )
    begin
        return N'Не обнаружен связанный сервер с именем "' + @SrvName + '".'
    end

    select @DataSource = data_source
    from sys.servers
    where name = @SrvName

    if not exists
    (
        select *
        from openquery
        (
            SSAS,
            '
                select *
                from $system.dbschema_catalogs
            '
        )
        where cast ( catalog_name as nvarchar ( max ) ) = @DbName
    )
    begin
        return N'База данных с именем "' + @DbName + '" не найдена на OLAP-сервере "' + @DataSource + '".'
    end
    return N''
end
go

if objecT_id ( N'dbo.BackUpMdDatabase', N'P' ) is null
       exec ( N'create proc dbo.BackUpMdDatabase as return' )
go

alter proc dbo.BackUpMdDatabase
(
    @DbName      nvarchar ( 200 ),
    @SrvName     nvarchar ( 200 ),
    @Password    nvarchar ( 200 ),
    @FilePath    nvarchar ( 300 ),
    @Compr       bit = 1,
    @Overwrite   bit = 0
)
with encryption
as
begin
    set nocount, xact_abort on

    declare @ErrMsg nvarchar ( max ), @cmd nvarchar ( max ), @sql nvarchar ( max )

    begin try
        set @ErrMsg = dbo.CheckSrvAndDb ( @SrvName, @DbName )
        if isnull ( @ErrMsg, N'' ) <> N''
        begin
            raiserror ( @ErrMsg, 16, 1 )
        end
            
        set @cmd = N'
            <Backup xmlns="http://schemas.microsoft.com/analysisservices/2003/engine">
              <Object>
                <DatabaseID>' + @DbName + '</DatabaseID>
              </Object>
              <File>' + @FilePath + '</File>
              <ApplyCompression>' + case when @Compr = 1 then N'true' else N'false' end + '</ApplyCompression>' + nchar ( 13 ) +
              case when @Password is null then N'' else '<Password>' + @Password + '</Password>' end + nchar ( 13 ) +
              N'<AllowOverwrite>' + case when @Overwrite = 0 then N'false' else N'true' end + N'</AllowOverwrite>' + nchar ( 13 ) +
            '</Backup>
        '
            
        set @sql = N'exec ( ''' + @cmd + N''' ) at ' quotename @SrvName )
        exec sp_executesql @sql
    end try
    begin catch
        set @ErrMsg = error_message ()
    end catch

    if isnull ( @ErrMsg, N'' ) <> N''
    begin
        raiserror ( @ErrMsg, 16, 1 )
    end
end
go

Функция CheckSrvAndDb выполняет простую обработку ошибок. По каталогу sys.servers проверяется наличие связанного сервера. Также проверяется есть ли на сервере база с заданным именем. Для этого используется системный каталок $system.dbschema_catalogs. Запрос к этому каталогу можно запускать и в редакторе MDX-запросов.

В хранимой процедуре мы строим динамическое выражение на языке XML/A. Этот код может выполнять резервное копирования баз данных в Analyis Services. Его нельзя запускать через функцию openquery, так как он не возвращает табличных данных. Вместо этого требуется применить конструкцию exec ( ... ) at SSAS, указав после at имя связанного сервера. Для бэкапа обязательно требуется указать пароль и расположение. Есть параметры со значениями по умолчанию для задания опций сжатия и перезаписи. Вcе настройки отражаются в выражении XML/A.

Подобным образом можно написать несложную хранимую процедуру для восстановления многомерной базы данных из бэкапа:

if object_id ( N'dbo.RestoreMdDatabase', N'FN' ) is null
       exec ( N'create proc dbo.RestoreMdDatabase as return' )
go

alter proc dbo.RestoreMdDatabase
(
    @DbName      nvarchar ( 200 ),
    @SrvName     nvarchar ( 200 ),
    @Password    nvarchar ( 200 ),
    @FilePath    nvarchar ( 300 ),
    @Overwrite   bit = 0
)
with encryption
as
begin
    set nocount, xact_abort on

    declare @ErrMsg nvarchar ( max ), @cmd nvarchar ( max ), @sql nvarchar ( max )

    begin try
        set @ErrMsg = dbo.CheckSrvAndDb ( @SrvName, @DbName )
        if isnull ( @ErrMsg, N'' ) <> N'' and not ( isnull ( @ErrMsg, N'' ) like N'База данных%' )
        begin
            raiserror ( @ErrMsg, 16, 1 )
        end
        set @ErrMsg = N''
            
        set @cmd = N'
            <Restore xmlns="http://schemas.microsoft.com/analysisservices/2003/engine">
              <File>' + @FilePath + '</File>
              <DatabaseName>' + @DbName + '</DatabaseName>
              <Password>' + @Password + '</Password>
              <AllowOverwrite>' + case when @Overwrite = 0 then N'false' else N'true' end + '</AllowOverwrite>
            </Restore>  
        '
            
        set @sql = N'exec ( ''' + @cmd + N''' ) at ' + quotename @SrvName )
        exec sp_executesql @sql
    end try
    begin catch
        set @ErrMsg = error_message ()
    end catch

    if isnull ( @ErrMsg, N'' ) <> N''
    begin
        raiserror ( @ErrMsg, 16, 1 )
    end
end
go

В этой процедуре задаются похожие опции, что и при бэкапировании. Обе процедуры созданы с опцией шифрования, чтобы никто не смог запустить профайлер и посмотреть с каким паролем вызываются процедуры. Теперь вызовы подобных процедур можно прописать как шаги заданий SQL агента, и периодически выполнять задачи резервного копирования или восстановления многомерных баз данных.

среда, 8 октября 2014 г.

Автоматическое скриптование объектов. Объектная модель SMO.

Рассмотрим задачу автоматизированного получения скрипта на создание объекта базы данных. Если это программируемый объект, например, процедура, функция, представление или что-то подобное, то можно воспользоваться хранимой процедурой sp_helptext. Или же можно сделать запрос к каталогу sys.sql_modules. А что делать, если требуется скрипт на создание объекта, код которого не хранится sys.sql_modules? Например, надо автоматически заскриптовать таблицу, индекс или объект Service Broker-а. Или же полнотекстовый каталог, базу данных или еще что-нибудь подобное.

Конечно, для каждого из этих объектов есть свой каталог с метаданными. Например, для таблицы это sys.tables, sys.columns, sys.types, для индекса - sys.indexes, sys.index_columns, sys.columns. Для очередей Service Broker-а есть каталог sys.service_queues и так далее.

Однако при таком подходе потребуется долгий и мучительный анализ различных столбцов этих каталогов для учета всех настроек. Причем содержимое этих каталогов может меняться от версии к версии, и за этим надо также следить. Например, при создании кластерного или некластерного индекса требуется отследить ключевые столбцы, порядок их сортировки, включенные столбцы, файловую группу, опции индекса. Также надо учесть возможность пробельных символов в именах.

Хочу предложить технически более простой способ скриптования объектов. Это можно делать с помощью объектной модели SMO. Эта модель позволяет не только скриптовать объекты, но и осуществлять полноценное администрирование сервера: создавать и редактировать объекты, делать запросы и пр.

Конечно, в рамках этой модели для каждого типа объектов надо писать отдельную ветку кода для вызова метода объекта соответствующего класса (для таблиц Tables, для баз данных Databases и так далее). Но зато не нужно анализировать свойства объектов. Нужно лишь создать экземпляр нужного класса и вызвать метод Script. Рассмотрим работу этого метода на примере скриптования таблиц. Правильнее всего было бы сделать clr-сборку и вызывать ее на сервере. Однако Microsoft запрещает обращаться к SMO в clr-сборках, поэтому для начала напишем консольное приложение, которое будет получать через командную строку информацию о таблице и выводить скрипт создания таблицы.

Приступим к кодингу. Создадим проект консольного приложения. Необходимо добавить ссылки на библиотеки из файлов Microsoft.SqlServer.Smo.dll, Microsoft.SqlServer.Management.Sdk.Sfc.dll, Microsoft.SqlServer.ConnectionInfo.dll. На моем компьютере они находятся в каталоге C:\Program Files (x86)\Microsoft SQL Server\120\SDK\Assemblies. Ниже представлен полный исходный код приложения:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

using System.Collections;
using System.Collections.Specialized;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;

namespace SmoTasks
{
    class TablesWrk
    {
        static void Main(string[] args)
        {
            string output, SrvName = args[0], DbName = args[1], SchName = args[2], ObjName = args[3];

            try
            {
                ServerConnection srvConn = new ServerConnection();
                srvConn.ServerInstance = SrvName;
                srvConn.LoginSecure = true;
                srvConn.Connect();

                Server srv = new Server(srvConn);
                Database db = srv.Databases[DbName];

                Table tab = db.Tables[ObjName, SchName];

                ScriptingOptions opt = new ScriptingOptions();
                opt.DriAllConstraints = true;
                opt.DriAll = true;
                opt.Indexes = true;
                opt.NoCollation = false;
                opt.XmlIndexes = true;
                opt.Statistics = true;
                opt.IncludeIfNotExists = true;
                StringCollection tabScr = tab.Script(opt);

                output = "";
                foreach (string str in tabScr)
                {
                    output = output + "\n" + str;
                }
                Console.WriteLine(output);
            }
            catch(Exception ex)
            {
                Console.WriteLine("error" + "\n" + ex.ToString());
            }
        }
    }
}

В этом коде мы создаем объект Table и используем его метод Script. Точно также можно поступать, если требуется создать любой другой объект. Перед вызовом метода Script задаются опции скриптования. Опция DriAll означает, что в определение таблицы включаются все ограничения целостности данных, Indexes, XmlIndexes включают определения всех обычных и xml-индексов. Установив NoCollation в false, мы получим вместе с определениями тестовых столбцов имена параметров их сортировки. Опция Statistics, как следует из ее названия, предоставит все статистики таблицы. Очень интересна опция IncludeIfNotExists. Благодаря ей, в скрипте перед кодом создания объекта в блоке if будет проверяться существует ли он.

Теперь можно написать хранимую процедуру, которая будет вызывать данное приложение через xp_cmdshell. Результаты, которые выводятся через Console.WriteLine, можно поймать с помощью конструкции insert into ... exec xp_cmdshell ...
Ниже полный код процедуры:

if sessionproperty ( N'quoted_identifier' ) = 0
       set quoted_identifier on
go

if sessionproperty ( N'ansi_nulls' ) = 0
       set ansi_nulls on
go

if object_id ( N'dbo.TabScriptGet', N'P' ) is null
       exec ( N'create proc dbo.TabScriptGet as return' )
go

alter proc dbo.TabScriptGet
(
       @TabName     nvarchar ( 200 ),
       @Script             nvarchar ( max ) out
)
as
begin
       set nocount, xact_abort on

       declare @SchName nvarchar ( 200 ) = isnull ( parsename ( @TabName, 2 ), N'dbo' ), @SrvName nvarchar ( 200 ) = @@servername, @DbName nvarchar ( 200 ) = db_name (), @cmd varchar ( 8000 ),
             @file nvarchar ( 200 ) = 'C:\V\Временные\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe', @log nvarchar ( max )
      
       if object_id ( @TabName, N'U' ) is null
       begin
             raiserror ( N'Таблица с именем "%s" не найдена.', 16, 1, @TabName )
             return
       end

       set @TabName = parsename ( @TabName, 1 )

       set @cmd = N'chcp 866 & ' + @file + N' ' + @SrvName + N' ' + @DbName + N' ' + @SchName + N' ' + @TabName

       if object_id ( N'tempdb..#res', N'U' ) is not null
             drop table #res
       create table #res
       (
             iRowId int identity ( 1, 1 )      not null,
             val          nvarchar ( max )           null
       ) on [PRIMARY]

       insert into #res ( val )
             exec master.dbo.xp_cmdshell @cmd
       delete #res
             where val is null
       delete #res
             where val like 'Active code page%'

       set @Script = replace ( cast
             (
                    (
                           select val + nchar ( 13 ) as [text()]
                           from #res
                           for xml path ( N'' )
                    )
                    as nvarchar ( max )
             ), N'&#x0D;', nchar ( 13 ) )
      
       if exists
       (
             select 1
             from #res
             where val = N'error'
       )
       begin
             set @log = @Script

             set @log = replace ( cast
             (
                    (
                           select val + nchar ( 13 ) as [text()]
                           from #res
                           where val <> N'error'
                           for xml path ( N'' )
                    )
                    as nvarchar ( max )
             ), N'&#x0D;', nchar ( 13 ) )

             set @Script = null
             raiserror ( @log, 16, 1 )
             return
       end
end
go

В этом коде при формировании команды мы предварительно меняем кодовую страницу, чтобы получить русские буквы в читабельном виде. Если скрипт состоит из нескольких строк, то все они через инструкцию insert into ... exec ... попадут в таблицу #res в виде отдельных строк. Чтобы получить их конкатенацию используется запрос в #res с опцией for xml path. Это пожалуй самый быстрый способ конкатенации строк в sql. На случай, если в приложении произошло исключение, делается проверка на наличие строки error в #res. В этом случае вычисляется полный текст сообщения об ошибке и он поднимается наверх.
Испытаем процедуру в действии. Создадим такую таблицу:

create table test.Accounts
(
       i      sql_variant,
       h      datetime,
       q      varchar ( 200 )
)
create nonclustered index IX on test.Accounts ( i )
alter table table test.Accounts add constraint DF default ( '2000-01-01' ) for h

В код на создание таблицы я намеренно не включил многие опции, которые используются по умолчанию, чтобы дополнительно удостовериться, что метод Script их всех учтет:

declare @TabName nvarchar ( 200 ), @Script nvarchar ( max )
exec dbo.TabScriptGet 'test.Accounts', @Script out
select @Script