среда, 12 января 2011 г. - www.msmirnov.ru

Стандарты и правила оформления кода T-SQL

Последнее время меня несколько раз спрашивали относительно стандартов и правил оформления кода на T-SQL , поэтому я решил выложить их в открытый доступ.

Для загрузки Стандарты и правила доступны по следующий ссылке на моем сайте: http://www.msmirnov.ru/public/TSQL_Coding_Standards.doc

Либо с ними можно ознакомиться прямо здесь.




Стандарт оформления кода T-SQL




Цель документа


Документ является соглашением по оформлению и написанию кода на языке T-SQL. В документе приведены основные правила оформления кода и приемы, используемые при написании программ.

Цели документа:

  • предоставить общие правила, позволяющие сохранить единый стиль написания кода, облегчив тем самым его понимание всеми участниками команды;

  • ввести базовые правила написания программ, что позволит повысить предсказуемость выполнения программ, а также избежать ошибок при написании программ новыми участниками команды, не знакомыми с внутренними стандартами разработки.


Стили именования

  • Pascal case первая буква каждого слова в имени идентификатора начинается с верхнего регистра.


Пример: TheCategory;


  • Camel case первая буква первого слова в идентификаторе в нижнем регистре, все первые буквы последующих слов в верхнем.


Пример: theCategory;


  • UpperCase стиль используется только для сокращений, все буквы в имени идентификатора в верхнем регистре.

Пример: ID;



  • Hungarian notation перед именем идентификатора пишется его тип в сокращенной форме.

Пример: strFirstName, iCurrentYear.




Правила именования объектов базы данных

  • Имена баз данных должны быть оформлены в стиле Pascal, нести осмысленное и логическое назначение и отвечать основному назначению баз данных.


  • Имена таблиц должны быть оформлено в стиле Pascal, нести осмысленное и логическое назначение и отвечать основному назначению таблиц. Не допускается использование пробелов или специальных символов в имени таблиц баз данных, а также символов национального алфавита.


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

В сложных случаях, когда есть базовая таблица и несколько зависимых, имя зависимой таблицы формируется из имени базовой таблицы и суффикса в конце имени,  для примера таблицы Campaign и CampaignGGL, CampaignMSN  и т.д.

Если таблица используется для хранения объектов определенных C#-классов, то ее имя должно совпадать с именем класса во множественном числе.


  • Имена индексов должны быть оформлены в стиле Camel. Не допускается использование пробелов или специальных символов в имени индекса таблицы, а также символов национального алфавита.

    В имени индекса должны быть перечислены имена всех полей, входящих в  выражение индекса, через символ «_».Если индекс включает в себя более пяти полей, достаточно в имени индекса перечислить первые 4-5 полей, и закончить строкой «_other».



  • Имена кластерных индексов должны начинаться с префикса «idx_».
    Имены некластерных индексов должны начинаться с «ndx_».
    Имена primary key должны начинаться с префикса «pk_».

Пример:

CREATE  CLUSTERED  INDEX [idx_TrackDate] ON [dbo].[EventTracking]([TrackDate])

CREATE
  INDEX [ndx_HitGUID]        ON [dbo].[EventTracking]([HitGUID]) ON [SECONDARY]

CREATE
  INDEX [ndx_SessionID_EventTypeID_TrackDate_VisitorID_Others] ON [dbo].[EventTracking]([SessionID] [EventTypeID], [TrackDate], [VisitorID], [Cost], [Profit])

CREATE
  INDEX [ndx_TrackDate_SessionID] ON [dbo].[EventTracking]([TrackDate], [SessionID])

  • Ограничения (constraints) должны быть поименованы следующим образом:

Foreign key constraints: «Fk» + +

Defaults: «Df» +

Checks: «Ck» +
<table name(Pascal case)> + <column name (Pascal case)>



  • Имена представлений должны быть оформлены в стиле Pascal, нести осмысленное и логическое назначение и отвечать основному его назначению, или же должно содержать в своем имени имя таблицы или имена таблиц, из которых происходит выборка данных.
    Не допускается использование пробелов или специальных символов в имени представления, а также символов национального алфавита. Обязательно наличие префикса «v» в имени представления.

  • Триггеры должны быть проименованы согласно следующим правилам:
    Префикс «Tr» + <TableName> + <TriggerType>.

    Если на одной таблице существует более одного триггера определенного типа, то в конце имени триггера добавляется порядковый номер такого триггера.
Для следующих типов триггеров определены следующие сокращения:

Insert I
Update
U
Delete
D


Например, триггеры на добавление, удаление и обновление для таблицы SomeTable  должны быть поименованы таким образом:

TrSomeTableI,
TrSomeTableD,
TrSomeTableD2,
TrSomeTableU

  • Полное имя объекта с использованием linked_server (четырехкомпонентные имена) формируется следующим образом:
    LINKED_SERVER_NAME.Catalog.schema.ObjectName


При этом имя linked_server-а указывается в upper-case, каталог(база данных) и имя объекта (таблица, хранимая процедура) указывается в стиле Pascal, имя схемы (владелец объекта) указывается в стиле lower-case.


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


В общем случае имя хранимой процедуры, функции должно состоять из:
<[project name, task name, service name] prefix (camel case)> + «_» + <object name (Pascal case)> + <operation name (Pascal case)>;

  • <[project name, task name, service name] prefix> - наименование проекта (применительно к c# solution) или процесса обработки данных, задачи, сервиса, в работе которой используется данная хранимая процедура или функция. Если это наименование подвергается сокращению, то даже сокращенное наименование должно быть понятным всем разработчикам, а не только разработчику данной процедуры, функции.

    В качестве примера, если это вспомогательная процедура для поддержки логики работы всех остальных, то используется префикс [support], если используется в работе приложения MyApplication то [myApplication] и т.д.

  • <operation name> - название операции, которая применяется к  <object name>. Если под операцией понимается какое-либо изменение конкретной таблицы, тогда допустимы следующие имена <operation name>.
    • Get возврат только одной строки;
    • Select возврат 1 и более строк или 1 и более наборов данных;
    • Insert Вставка в одну или более таблиц. Недопустимо возвращать из такой процедуры набор данных. Return code должен содержать признак 0 или 1 как признак удачного или неудачного выполнения;
    • Update Обновление данных в одной или более таблиц. Недопустимо возвращать из такой процедуры набор данных. Return code должен содержать признак 0 или 1 как признак удачного или неудачного выполнения;
    • Delete Удаление данных из одной или более таблиц. Недопустимо возвращать из такой процедуры набор данных. Return code должен содержать признак 0 или 1 как признак удачного или неудачного выполнения;
    • <другой> - если операция не попала ни в одну из перечисленных;
      Если под операцией понимается название какого-либо подпроцесса, логической операции, относящейся к группе таблиц в этом случае необходимо использовать логическое наименование этого подпроцесса, операции здесь допустимо как существительное (имя операции, например, backup), так и повелительное наклонение глагола (load, upload, remove и пр.).



  • <object name> - это название объекта или группы объектов, над которым будет выполнена <operation name> -  описанная выше операция. Это может быть как имя конкретной таблицы, так и логическое наименование любой сущности, которая имеет свое отражение в базе данных.

Пример:


- если в системе есть процесс
MyProcess, которая делает backup каких-либо данных, то возможно такое наименование хранимой процедуры:
[dbo].[ myProcess_MyDataBackup];


- если тот же самый процесс имеет процедуры для добавления новой записи в справочник
Dictionary, то возможно следующее наименование:
[dbo].[ myProcess_DictionaryInsert];


- если в пределах упомянутого процесса
MyProcess мы хотим логически выделить другой подпроцесс, скажем, работу с какими-либо настройками, то возможно следующее наименование процедуры для операции, например, вставки данных в таблицу SomeTable:

[dbo].[ myProcessSettings_SomeTableInsert];

  • В тех случаях, когда возникает необходимость разнести код и логику хранимой процедуры (а эта необходимость возникает, как правило, в случаях разрастания кода), следует разбить исходную хранимую процедуру на несколько, придерживаясь следующего правила именования вновь создаваемых процедур:

    Имя вновь созданной хранимой процедуры формируется как
<имя вызывающей хранимой процедуры> + «_» + <object name (Pascal case)> <operation name (Pascal case)>;



Оформление t-sql кода при написании user defined functions, stored procedures, triggers и прочих вспомогательных скриптов.



  • Любой код в теле ветвлений, циклов, блоков кода должен быть выделен отступом размером один tab. Размер Tab Size принимается равным 4 пробела. Условие выражений if, while отделяем от данных инструкций символом tab таким образом, чтобы сам t-sql код внутри блоков был выравнен по началу условия ветвления, цикла,



Пример:

begin

      
if        <condition>
      
begin
       
       <        condition body

       
               ...................
       
       >

       
       while        <while_condition>
       
       begin

       
               <        while loop body

       
                       ........................

       
               >
       
       end        

      
end       
end

В любых операциях операнды должны быть отделены от операторов одним пробелом.
На примере операций присваивания, сравнения:


declare        @variable        int
set
        @variable = 1

if
        @variable <> 1
begin

...

end


В выражениях, в функциях, в операциях вызова хранимых процедур, функций, в любых сложных выражениях, все аргументы должны быть отделены от предыдущего значения запятой + <один пробел>, например:


select
       substring(convert(nvarchar(10), getdate(), 21), 1, 4)

  • В запросах и подзапросах алиасы таблиц формируются путем сокращения имен участвующих в запросе таблиц, например, Users -> U и т.д.

  • Все комментарии в коде необходимо оформлять, используя «--» + 1 tab symbol перед началом текста самого комментария. Если комментарий идет перед началом блока, сам текст комментария должен начинаться с той же позиции, что и t-sql код блока т.е. символ начала комментарии «--» должен отставать от позиции начала блока на один tab size, для примера:

--        начало комментария
--
       ...........................
      
if        <condition>

      
begin
       
       <condition body>
      
end

Для более удобного восприятия кода, содержащего большое количество ветвлений, а также блоков кода (begin/end), размер текста которого превышает размеры страницы, рекомендуется оформлять end-statement  комментарием, содержащим или условие if-statement, или условие цикла (while), для примера:

--        начало комментария

--
       ...........................
      
if        <condition>
      
begin
       
       <        condition body
       
               ...................
       
       >
       
       while        <while_condition>
       
       begin

       
               <        while loop body

       
                       ........................

       
               >
       
       end        --        while        <while_condition>
       
      
      
end        --        if        <condition>



  • Ко всем встроенным инструкциям t-sql языка программирования в коде хранимых процедур, триггеров, пользовательских функций должен применяться стиль оформления lower-case. Это относится как к select, insert, update, delete, create и прочим инструкциям, так и к выховам extended stored procedures и встроенным функциям (substring(), getdate() и пр.).



К пользовательским объектам базы данных должен применяться стиль оформления Pascal, т.е. к пользовательским функциям, хранимым процедурам, именам таблиц и пр.

Стиль оформления Pascal должен применяться также и при работе с локальными переменными.


Стиль upper-case должен применяться только в alter-скриптах и только применительно к инструкциям create, select, update, delete, insert.



  • При  создании хранимой процедуры после ее имени с новой строки после объявления ее параметров должна быть указана в комментариях следующая информация:
    - дата создания,
    - кем была создана процедура
    - ее назначение.
    После каждого какого-либо существенного изменения эти изменения должны быть также зафиксированы в истории работы над кодом хранимой процедуры.
    Комментарий о произведенных изменениях оформляется аналогично оформлению комментария о назначении процедуры (Все комментарии оформляются согласно стилю оформления комментариев) .


Пример:
ALTER        PROCEDURE        [dbo].[support_LostDBRecordsProcessor]
      
@Dir                        nvarchar(100)        =        'c:\logs\LostDBRecords',

      
@FileExtension        nvarchar(100)        =        '*.txt',

      
@DateFormat                nvarchar(10)        =        'dmy'

--
       2010.02.07 created by smirnov a
--
       lost db records processor implementation

--
       This procedure must scan ..\Logs\LostDBRecords directory and execute
--
       the bulk insert operation for current
--
       database and after that create .cab archives or create a .log file for a

--
       corresponding  file in case of an error
--
       2010.04.06 modified by smirnov a

--
       sql server version analisys was added. If @version variable equals 2005, we must
--
       remove the "nooutput" parameter from xp_cmdShell
AS
begin
       

Те же самые правила относятся к оформлению user defined functions с той лишь разницей, что комментарии о создании, внесении изменений вносятся после объявления параметров.

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


Пример:
ALTER PROCEDURE         [dbo].[support_SearchDDLChanges]
      
@TableName        nvarchar(100)         = null

--
       2010.01.01        created by <author>
--
       Данная процедура предназначена для выполнения операции сравнения

--
       DDL описаний таблиц из указанной базы данных по сравнению с эталонной

--
       2010.03.01        modified by <changes_author>

--
       bugfix 0001 - скрипт неправильно работал

--
       2010.02.01        modified by <changes_author>

--
       Добавлена функциональность по автоматическому обновлению структуры
--
       указанной базы данных
--
       ....

AS

Если имеет смысл указать в самом t-sql коде, в каком именно месте были произведены изменения, перед новым или измененным выражением стоит поставить комментарий в таком формате, как и в истории изменений, если не требуется - без описания произведенных изменений.


--
       <date> modified by <author>

--
       bugfix <bug number> or additional description

      
insert        into        ....
      


  • Параметры хранимой процедуры, функции должны быть именованы в стиле Pascal и нести осмысленный логический смысл. Объявление  параметра должно начинаться с новой строки с отступом 1 tab size. Если смысл параметра непонятен, после объявления ее типа, значения по умолчанию (если есть) должен быть комментарий о назначении данного параметра. Если комментарий не убирается в пределах окна редактирования по ширине, его необходимо сделать многострочным, применяя следующий стиль оформления:

   @FullDatabasePathLogs        nvarchar(100) = 'dbo',        

       
               --        Полный путь к базе данных (linked_server.catalog.owner),

       
               --        откуда будем брать нормализованные логи для обработки

  
@FullDatabasePathReports        nvarchar(100) = 'MyServer.MyDB.dbo'
       
               --        Полный путь к базе данных (linked_server.catalog.owner),                                --        на котором будут храниться отчеты - MyServer.MyDB.dbo


  • К выражениям select необходимо применять следующий стиль оформления: имя каждого выражения (операции над column), поля должно быть на отдельной строке; каждое следующее выражение, поле должно начинаться с начальной позиции предыдущего; первое выражение, поле должно быть отделено от select statement отступом 1 tab size, как в примере ниже:


select
       column_01,
       
       expression(column_02, operand_01, operand_02, ..),
       
       column_03

Выражения from, join, where, order, group, having должны  быть на одном уровне с выражением select. К перечисленным выражениям инструкции  select  применяем такие же правила, т.е. остальную часть подвыражений отделяем отступом в 1 tab size.

Если условие в where clause, join clause состоит из нескольких логических выражений, то каждое такое логическое выражение должно начинаться с новой строки, причем сам логический оператор (Boolean expressions and, or, not), связывающий эти выражения в условии, должен быть вначале перенесенного выражения.


Условие связывания таблиц в join-ах (on-expression) должно записываться следующим образом: сначала указывается имя поля из той таблицы (или алиаса) , по которой  описывается join filter (собственно, указывается в join on <table_name>), затем  значение самого фильтра.

Все вышеперечисленное можно показать на примере:


select        count(*),
       
       table_01.column_01,

       
       expression(table_01.column_02, operand_01, operand_02, ..),

       
       table_02.column_03
from
       table_01

join
       table_01 on        table_01.column_01 = table_02.column_01        
       
                       and                <boolean expression>       
join
       table_nn on table_nn.column_03 = table_02.column_03

where
       table_01.column_03 not in (.....        )                
       
       and        <where boolean expression 01>   
       
       or        <where boolean expression 02>       
group
       by table_01.column_01,
               expression(table_01.column_02, operand_01, operand_02, ..),
       
       table_02.column_03
having
       count(*) > 10
order
       by table_01.column_01
select
       count(*),
       
       table_01.column_01,
       
       expression(table_01.column_02, operand_01, operand_02, ..),
       
       table_02.column_03
from
       table_01,
       
       table_02,
       
       table_nn ............

  • В выражении insert into список полей в выражении insert указывается одной строкой, если список не убирается в одну строку, переносится так, чтобы список был виден в пределах ширины страницы.

    Into statement должен начинаться с той же позиции, что и список полей из select_list из выражения select.


Select statement оформляется согласно описанным выше правилам.

Пример:
insert        into        <destination_table>        (field_01, field_02, field_03, field_04,...., field_nn)
select
       count(*),
       
       table_01.column_01,
       
       expression(table_01.column_02, operand_01, operand_02, ..),
       
       table_02.column_03,
       
       .....

....................................

select
       count(*),
       
       table_01.column_01,
       
       expression(table_01.column_02, operand_01, operand_02, ..),
       
       table_02.column_03,
       
       .....
into
       <destination_table>      
....................................

  • Для операций update, delete применяются те же правила, что и при оформлении select, insert выражений, т.е. операции присваивания над полями таблицы должны быть отделены от set на 1 tab size, каждая операция set в update-выражении должна начинаться с новой строки и начинаться с той же позиции, что и предыдущая. Операнды в операции присваивания должны быть отделены одним пробелом или tab от символа «=» (смотри рекомендации по отступам).

Сразу же за ключевым словом update или delete должен идти один tab, список обновляемых полей в выражении update должен быть на одном уровне с именем таблицы, так же как и само условие в выражении where. При описании where-условия придерживаемся тех же правил, что и при написании select statement.



Пример:

update
       <SourceTable>
set
               <Column_01> = <value01>,
       
       <Column_02> = <value02>,
       
       <Column_nn> = <valueNN>
where
       <condition_01>
       
       and <condition_02>



delete
       from <SourceTable>
where
       <condition_01>
               and <condition_02>


  • Выражение create table оформляется таким образом, чтобы описание каждого поля начиналось с новой строки. Желательно форматировать список колонок таким образом, чтобы типы данных находились один над другим, т.е. применяем колоночный стиль оформления.

    Пример:
create        table <table_name>        (        Column_01        int        identity (1, 1) primary key,
       
                                       Column_02        nvarchar(100) not null,
       
                                       Column_03        bit null,
       
                                       CreateDate        datetime default (getdate())        )



  • При объявлении курсора select statement должно начинаться с новой строки с той позиции, с которой начинается описание типа переменной курсора. К оформлению select statement применяются все вышеописанные правила.


declare
       columns_list        cursor        local forward_only for
       
                       select        <column_01>,
       
                               <column_02>,
       
                               <column_nn>
       
                       from        <source_table>
       
                       where        <where_condition>

declare
       @Columns_list                cursor       
set
               @Columns_list =        cursor        local forward_only for
       
                               select        <column_01>,
       
                                       <column_02>,
       
                                       <column_nn>
       
                               from        <source_table>
       
                               where        <where_condition>



  • При объявлении в t-sql коде переменных желательно придерживаться табличного стиля оформления, т.е. типы данных переменных должны начинаться с одной определенной позиции, имена переменных аналогично, приблизительно как показано ниже:


declare
       @Variable_01        int,
       
               @Variable_02        nvarchar(100),
       
               @Variable_04        bit
--
       или
      
declare        @Variable_01        int
      
declare        @Variable_02        nvarchar(100)
      
declare        @Variable_04        bit


Общие рекомендации.

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


Временные таблицы малого объема сохранять в памяти (вместо # - @), кроме тех случаев, когда это накладывает неприемлемые ограничения.
Т.е. вместо create table #temp (ID int)
использовать declare @temp table (ID int)


Требования, обязательные к применению.


  • Необходимо создавать связи для всех таблиц в базе данных.

  • Все таблицы должны быть отражены на соответствующих диаграммах.

  • Поля с допустимым значением NULL допускается использовать только в тех случаях, когда значение NULL несет логическое значение и не может быть заменено другим.

  • При добавлении новых полей допустимость значения NULL определяется в соответствии с предыдущим правилом.

  • Необходимо использовать типы минимально необходимого размера.

  • Для операции select необходимо указывать только те поля, которые требуются в данном запросе.
Пример:
select        ID, Keyword

from        KeywordParams (nolock)

  • Именование таблиц статусов: Имя статуса(с большой буквы)+Statuses.
Пример:
                        UserStatuses


  • Идентификаторы записей во всех таблицах должны называться ID.
Пример:
               в таблице Users поле идентификатора должно называться ID, а не UserID.


  • Идентификаторы в статических таблицах (справочниках) не должны быть помечены как identity.

  • Ссылки на идентификаторы других таблиц именуются: Имя таблицы в единственном числе+ID.

Пример:
                     Если таблица Events ссылается на UserStatuses,
                     то поле внешнего ключа должно называться UserStatusID, а не StatusID.

  • Для всех запросов, где участвуют таблицы, которые не подвергаются изменению, в запросах select необходимо использовать (nolock) для каждой такой таблицы.

Пример:

select
       Keyword, KP.ID
from
       Keywords (nolock),
       
       KeywordParams KP (nolock)
where
       Keywords.ID = KP.KeywordID


  • Для update запросов, которые обрабатывают одну запись необходимо использовать (rowlock, updlock) для каждой таблицы, в которой происходит update.
Пример
update        Keywords with (rowlock, updlock)
set
               Keyword = 'text'
from
       Keywords
where
       Keywords.ID = 123



Мой сайт - www.msmirnov.ru

понедельник, 27 декабря 2010 г. - www.msmirnov.ru

Dependency Injection и IoC-контейнеры на примере MEF и NInject - Доклад Андрея Маркеева

Несколько дней назад Андрей Маркеев прочитал нам свой замечательный доклад "Dependency Injection и IoC-контейнеры на примере MEF и NInject".

Доклад, на мой взгляд, получился очень интересным и содержательным.

Видео доклада доступно здесь:


или по ссылке: http://rutube.ru/tracks/3912859.html?v=e82a93d8e3d62fc22a4209c6f253ff4d
Мой сайт - www.msmirnov.ru

вторник, 21 декабря 2010 г. - www.msmirnov.ru

Влияют ли cross database joins на производительность запросов в SQL Server?

На днях мне пришлось столкнуться с вопросом – как влияют cross database joins на производительность запросов в SQL Server, при условии что базы находятся в пределах одного сервера?

Поискав в Google я не нашел однозначного ответа на этот вопрос – кто-то писал что разницы нет никакой, кто-то наоборот отмечал двукратное замедление запросов.

Поскольку для меня это важно, я провел несколько тестов на выборку и вставку, в которых объединялись две таблицы – одна размеров в 8 миллионов строк и другая в 30 тысяч строк.
Эти таблицы поочередно располагались в одной и в разных базах.

Никакой разницы в значениях logical reads, physical reads, duration не было.

Поэтому мой ответ – Нет, cross database joins не влияют на производительность запросов в SQL Server.

Мой сайт - www.msmirnov.ru

понедельник, 20 декабря 2010 г. - www.msmirnov.ru

Partitioned views на практике. Горизонтальное секционирование (партицирование) через partitioned views в SQL Server. Опыт собственной разработки.

Введение.

Сам по себе прием partitioned views, о котором пойдет речь в данной статье, известен уже около 10-ти лет, но здесь мне бы хотелось поделиться именно практическим опытом его реализации в современной обстановке.

Кратко напомню суть приема partitioned views.

Partitioned views используются для организации горизонтального секционирования больших таблиц баз данных с целью ускорения доступа и повышения производительности запросов.

Приведу пример.

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

ReportTable_History будет хранить данные до начала 2011 года,
ReportTable_2011_01 за январь 2011 и
ReportTable_2011_02 за февраль 2011.

Для этого выполним следующий SQL-скрипт:

-- Первая таблица

CREATE TABLE ReportTable_History(ReportDate datetime NOT NULL, SalesCount int NOT NULL, SalesAmount money NOT NULL)
GO

CREATE CLUSTERED INDEX CL_INDEX ON ReportTable_History (ReportDate)


ALTER TABLE ReportTable_History WITH CHECK ADD CONSTRAINT CK_ReportTable_History CHECK ((ReportDate<'2011-01-01'))
GO
 
-- Вторая таблица
CREATE TABLE ReportTable_2011_01(ReportDate datetime NOT NULL, SalesCount int NOT NULL, SalesAmount money NOT NULL)
GO

CREATE CLUSTERED INDEX CL_INDEX ON ReportTable_2011_01 (ReportDate)

ALTER TABLE ReportTable_2011_01 WITH CHECK ADD CONSTRAINT CK_ReportTable_2011_01 CHECK ((ReportDate>='2011-01-01' and ReportDate < '2011-02-01'))
GO


-- Третья таблица
CREATE TABLE ReportTable_2011_02( ReportDate datetime NOT NULL, SalesCount int NOT NULL, SalesAmount money NOT NULL)
GO


CREATE CLUSTERED INDEX CL_INDEX ON ReportTable_2011_02 (ReportDate)

ALTER TABLE ReportTable_2011_02 WITH CHECK ADD CONSTRAINT CK_ReportTable_2011_02 CHECK ((ReportDate>='2011-02-01' and ReportDate < '2011-03-01'))
GO


Как видим, все таблицы имеют одинаковую структуру и имеют также ограничения по полю ReportDate, которые не позволяют нам вставлять в эти таблицы данные за “неправильные” даты.


Создадим теперь представление следующего вида, которое будет объединять в себе все три таблицы:

CREATE VIEW ReportView AS
select ReportDate, SalesCount, SalesAmount
from ReportTable_History
union all
select ReportDate, SalesCount, SalesAmount
from ReportTable_2011_01
union all
select ReportDate, SalesCount, SalesAmount
from ReportTable_2011_02


Теперь попробуем выполнить вставку данных за Январь 2011 в ReportTable_2011_01:

insert into ReportTable_2011_01 (ReportDate, SalesCount, SalesAmount)
values ('2011-01-01', 1, 100)

 
 
Проверим содержимое таблицы за Январь 2011 (ReportTable_2011_01):

image

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


Теперь самое интересное – попробуем теперь выбрать январские данные из представления ReportView и посмотрим план запроса:

image

Из плана видим, что сервер производит обращение только к таблице ReportTable_2011_01, в которой содержатся данные за Январь 2011. Другие две таблицы при выполнении запроса не задействованы. Это происходит благодаря нашим CHECK CONSTRAINTS, которые помогают оптимизатору правильно определить месторасположение данных.

Для выбирающего скрипта эта работа не видна, т.е. он не знает про внутреннюю структуру view и про распределение данных по таблицам, вместо этого он просто обращается к view, как к обычной таблице.

Таким образом происходит распределение данных между таблицами, что уменьшает каждую из них и ускоряет работу с таблицами.

Данный прием также работает в случае если таблицы доступны через linked servers, только в таком случае происходит распределение данных не только между локальными таблицами, но и между серверами.

В SQL Server, начиная с версии 2005, существует встроенная возможность разбивать большие таблицы на партиции, но данная функция присутствует только в редакциях Developer и Enterprise.

Первый не подходит для больших баз данных, а второй стоит очень дорого.

Надеюсь в будущих версиях Microsoft это исправит, но сейчас мы имеем то, что имеем – для использования партиций надо покупать редакцию Enterprise.

Таким образом, встает вопрос собственной реализации механизма partitioned views.



Описание механизма.

Рассмотренный здесь механизм был использован для организации доступа к большим таблицам отчетных данных с секционированием по отчетным периодам (собственно говоря, именно такой пример я и привел во Введении). В описании механизма я буду рассматривать секционирование по месяцам.

На вскидку можно было бы предположить, что мы можем организовать partitioned view, которое будет объединять в себе неограниченное число таблиц, созданное для большого числа месяцев. Например, можно было бы объединить в нем 120 таблиц для отчетных данных за 10 лет.

К сожалению, это не так.

Длина запроса view ограничена 8000 символов, поэтому мы не можем создать partitioned view, достаточно длинное для объединения любого количества таблиц.

Как вариант решения проблемы, можно было бы организовать секционное хранение данных только за последние 12 месяцев, а все более старые данные хранить в одной отдельной общей таблице (как показано во введении).

На первый взгляд это логично, так как данные за последние 12 месяцев являются наиболее часто востребованными, поэтому именно для них является наиболее актуальной задача быстрого доступа. Конечно, 12 месяцев – это частый случай, количество может и больше, лишь бы хватило длины view.

Таким образом, наше partitioned view могло бы содержать одну большую таблицу, которая хранит данные старше 12-ти месяцев и 12 малых таблиц для 12-ти последних месяцев каждая.

На следующем рисунке показано, что partitioned view объединяет 12 таблиц по месяцам и одну большую таблицу.

image

Соответственно, перед началом каждого месяца мы должны были бы переместить 12-й месяц в большую таблицу и добавить в представление новую 1-ю таблицу для нового месяца.

image

Для этого мы должны были бы выполнить следующие действия:
1. Исправить CHECK CONSTRAINTS на большой таблице так, чтобы разрешенная в ней дата увеличилась на 1 месяц (т.е. чтобы таблица опять включала бы в себя все данные старше 12-ти месяцев, с учетом перехода на новый месяц).
2. Перенести данные из таблицы, которая содержала данные 12-ти месячной давности в большую таблицу.
3. Удалить пустую таблицу, в которой содержались данные 12-ти месячной давности.
4. Создать новую таблицу для нового месяца с новым CHECK CONSTRAINTS.
5. Изменить скрипт partitioned view, добавив в него новую таблицу и удалив старую.

Этот механизм будет работать, но он имеет ряд существенных недостатков:
1. Его довольно сложно поддерживать, так как структуры месячных таблиц и большой таблицы разные и мы не можем сформировать универсальный механизм генерации таблиц за любой период.
2. Проблема быстрого доступа к “старым” данным не решена – они все равно располагаются в большой таблице.
3. Необходимо выполнять действия по перемещение данных между таблицами, которые могут быть довольно затратными.

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

В качестве решения всех этих проблем можно использовать двухуровневую организацию таблиц и представлений:
1. Организовать хранение данных только в месячных таблицах: ReportTable_2011_01, ReportTable_2011_02 и т.д.
2. Создать набор представлений по годам, объединяющих по 12 месячных таблиц за каждый год.
3. Создать одно общее представление второго уровня, объединяющее представления по годам.
На схеме это можно показать следующим образом:

image

При это при обращении за получением данных к ReportView оптимизатор по прежнему будет выполнять обращение только к той таблице, которая содержит требуемые данные. Т.е. оптимизатор корректно обрабатывает наличие двух-уровневых partitioned views.
Длины скриптов таких представлений должно хватить на несколько десятков лет, что будет вполне достаточно для большинства прикладных систем.


Реализация механизма.

В процессе работы с таким представлением нам необходимо выполнять следующие операции:
1. Производить работу с данными в таблицах – вставлять, обновлять и удалять записи.
2. Создавать новые таблицы и изменять скрипты представлений, добавляя в них эти новые таблицы.

Сначала рассмотрим работу с данными.

Как я уже отметил выше, непосредственная работа с данными через partitioned view, при отсутствии на таблицах первичного ключа, невозможна.
Поэтому при выполнении запросов нам необходимо явно указывать, с какой таблицей мы ведем работу в данный момент.

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

Этот процесс лучше автоматизировать – т.е. дать нам такой способ выполнения запросов, который позволит нам выполнять запросы не задумываясь о том, в какой конкретно таблице физически расположены наши данные.

Для этого прежде всего изменим структуру таблиц и представлений.

Новые скрипты таблиц будут такими:
-- Таблица на Январь 2011

CREATE TABLE ReportTable_2011_01(ReportDate datetime NOT NULL, SalesCount int NOT NULL, SalesAmount money NOT NULL, [Version] tinyint)
GO

CREATE CLUSTERED INDEX CL_INDEX ON ReportTable_2011_01 (ReportDate)

ALTER TABLE ReportTable_2011_01 WITH CHECK ADD CONSTRAINT CK_ReportTable_2011_01 CHECK ((ReportDate>='2011-01-01' and ReportDate < '2011-02-01'))
GO

-- Таблица на Февраль 2011
CREATE TABLE ReportTable_2011_02( ReportDate datetime NOT NULL, SalesCount int NOT NULL, SalesAmount money NOT NULL, [Version] tinyint) GO

CREATE CLUSTERED INDEX CL_INDEX ON ReportTable_2011_02 (ReportDate) ALTER
TABLE ReportTable_2011_02 WITH CHECK ADD CONSTRAINT CK_ReportTable_2011_02 CHECK ((ReportDate>='2011-02-01' and ReportDate < '2011-03-01'))

GO

Теперь создадим наше представление первого уровня:


CREATE VIEW ReportView_2011
AS
select ReportDate, SalesCount, SalesAmount
from ReportTable_2011_01
union all
select ReportDate, SalesCount, SalesAmount
from ReportTable_2011_02

Создадим теперь преставление второго уровня. В нашем примере оно включает в себя всего лишь один год.

CREATE VIEW ReportView
AS
select ReportDate, SalesCount, SalesAmount
from ReportView_2011


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


Для этого создадим универсальную процедуру, которая будет принимать на входе SQL-запрос и выполнять его на к таблице или таблицам, в которых физически располагаются необходимые данные.

Рассмотрим простой вариант такой процедуры:
CREATE PROCEDURE ExecPartScript
   
@script nvarchar(4000),
    @TableName nvarchar(200),
   
@MinDate datetime AS
begin 

    declare @sql_script nvarchar(4000
    declare @PartTableName varchar(200
    while (@MinDate < getdate()) 
    begin 
        exec GetTableNameAtDate @CheckDate = @MinDate, @TableName = @TableName, @PartTableName = @PartTableName output 
        set @sql_script = replace(@script, @TableName, @PartTableName
        execute dbo.sp_executesql @sql_script 
        set @MinDate = cast((convert(varchar (08), dateadd(month, 1, @MinDate), 21) + '01') as datetime)     end
 end

Данная процедура принимает на вход запрос, который ей необходимо выполнить на наборе таблиц (@script), общая часть имен таблиц для выполнения запроса (@TableName – в нашем примере это ‘ReportTable’)  и минимальная дата секционирования, с которой необходимо выполнить запрос (@MinDate).

Параметр @MinDate, вообще говоря, можно было бы и не использовать, но его применение позволяет сократить время и нагрузку от выполнения процедуры.

Как видим, процедура выполняет цикл по месяцам, которые являются основной для разбивки таблиц.

Для каждого месяца она запускает процедуру GetTableNameAtDate, которая возвращает имя конкретной таблицы для выполнения запроса (т.е. например ‘ReportTable_2011_02’ вместо ‘ReportTable’). Затем в цикле происходит подмена имени таблицы на новое имя и затем происходит выполнение запроса.

Процедура GetTableNameAtDate довольно проста:

CREATE PROCEDURE GetTableNameAtDate
 @CheckDate datetime,
 @TableName nvarchar(200),
 @PartTableName nvarchar(200) output
AS
BEGIN 

    declare @part_name nvarchar(4
    set @part_name = month(@CheckDate
    if (len(@part_name) = 1) set @part_name = '0' + @part_name 
    set @part_name = '_' + cast (year(@CheckDate) as varchar(4)) + '_'  + @part_name
     set @PartTableName = @TableName + @part_name
    if not exists ( select * from dbo.sysobjects where id = object_id(@PartTableName) and OBJECTPROPERTY(id, N'IsUserTable') = 1)

       exec CreateTable @TableName = @TableName, @part_name = @part_name, @CheckDate = @CheckDate
END

Данная процедура подставляет дату к имени таблицы и возвращает новое имя.

Попутно она вызывает процедуру CreateTable, которая создает новую таблицу в базе, если ее еще нет.

CREATE PROCEDURE [dbo].[CreateTable]
 @TableName nvarchar(200),
 @part_name nvarchar(4),
 @CheckDate datetime AS
BEGIN 

    declare @script nvarchar(4000
    exec ScriptTable @TableName, @script output 
    set @script = replace(@script, @TableName, @TableName + @part_name
    declare @pos int 
    set @pos =
    set @pos = charindex('constraint', @script
    while (@pos > 0
        begin 
            set @pos = charindex(']', @script, @pos
            if (@pos = 0
                break
            set @script = stuff(@script, @pos, 0, @part_name
            set @pos = charindex('constraint', @script, @pos + 2
        end 
    declare @date_column varchar (100
    set @date_column = 'ReportDate' 
    declare @startdate varchar (15), @enddate varchar(15
    set @startdate = convert(varchar (05), @CheckDate, 21) + '01-01' 
    set @enddate = convert(varchar (05), dateadd(year, 1, @CheckDate), 21) + '01-01' 
    set @script = @script + 'ALTER TABLE [dbo].[' + @TableName + @part_name + '] WITH CHECK ADD CONSTRAINT [CK_ReportTable' + @TableName + @part_name + '] CHECK (([' + @date_column + ']>=''' + @startdate + ''' and [' + @date_column + ']<''' + @enddate + ''')) ' 
    execute dbo.sp_executesql @script
 END
Данная процедура вызывает процедуру скриптования таблицы ScriptTable, переименовывает в ней ограничения, добавляет ограничение по дате и запускает скрипт генерации таблицы.

Процедура ScriptTable возвращает скрипт таблицы и чтобы не перегружать данную статью информацией, я рассмотрю ее в отдельном посте.
Итак, на выходе мы получаем процедуру ExecPartScript, которую мы можем использовать для выполнения запросов к секционированным таблицам, которая автоматически перенаправляет запросы на различные физические таблицы, а также организует создания новые секционированных таблиц по мере необходимости.
Мой сайт - www.msmirnov.ru