В SQL Server 2012 появилась долгожданная возможность генерировать числа из последовательности - объект SEQUENCE.

Хорошо, что SEQUENCE-ом можно пользоваться, как функцией, а не только, как DEFAULT значением на поле таблицы и не увязывать момент получения значения с моментом физического добавления записи.
Но в реальной жизни довольно популярное требование - динамическое формирование имени последовательности (или любой другой способ реализации "хочу нумерацию с начала года/месяца"). Также часто хотят иметь хоть какой-то механизм "повторного" получения "пропущенных значений".

Обычно предлагаемые в решения по генерации "бизнес-номеров", написанные на TSQL и базирующиеся на таблицах с текущими значениями счетчиков, имеют один недостаток - блокировки. Действительно, если мы "генерируем" новый номер, то на время генерации мы должны заблокировать счетчик, чтобы в другом соединении не было получено такое же значение. При этом зачастую номер нам нужно получать в рамках уже открытой транзакции, что чревато тем, что два изначально независимых бизнес-процесса будут в лучшем случае долго блокировать один другого, а в худшем - окажутся не настолько независимыми, чтобы избежать взаимоблокировки (deadlock).

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

insert into MyTable(DocNum, DocDate, Comment)
select 
    Generator.NextValue('SequenceFor_DocNum'), IncomeDate, Comment
from #SomeBuffer

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

Но не совсем любые!
В функциях есть возможность вызывать расширенные хранимые процедуры и CLR процедуры и функции.

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

Причем сборку мы будем создавать из самого же SQL Server - не нужен даже VisualStudio, но об этом позже.

(Все запросы выполняем в той базе, где нам нужны счетчики)

Настраиваем сервер - включаем CLR:

if exists (select * from sys.configurations where name='clr enabled' and value_in_use=0)
begin
    exec('sp_configure ''show advanced options'', 1')
    exec('reconfigure')
    exec('sp_configure ''clr enabled'', 1')
    exec('reconfigure')
end
go

Настраиваем базу - позволяем в ней работать unsafe сборкам:

declare @sql nvarchar(max)
set @sql = N'alter database '+DB_NAME()+N' set trustworthy on'
exec(@sql)

Создаем схему _Generator, в которой будут находиться основные объекты, необходимые для манипуляции со счетчиками:
if SCHEMA_ID('_Generator') is null exec ('create schema _Generator')

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

create table _Generator.List
(
     ID uniqueidentifier not null default newid() ,   
     Name sysname not null  
     StartValue int not null constraint DF__Generator_List_StartValue default 0,   
     IsWorkWithHoles tinyint,   
     constraint PK_List_ID primary key clustered(ID),   
     constraint AK_List_Name unique (Name)
)
go

Теперь создадим триггер, который для каждого внесенного в _Generator.List счетчика будет создавать в схеме с именем G$имя_счетчика, функции NextValue и CurrentValue. Причем в зависимости от параметра IsWorkWithHoles, реализации функции NextValue несколько различаются.

create trigger [_Generator].[TR_List_UpdateGenerator] on [_Generator].[List]
with execute as owner   
after insert, update, delete
as  
begin
    set nocount on 
    set ansi_nulls on
    declare   
        @Name nvarchar(128),  
        @IsWorkWithHoles tinyint,   
        @ID uniqueidentifier,  
        @FunctionName nvarchar(128),   
        @sql nvarchar(maX  
    declare cursd cursor local static forward_only for  
        select N'G$'+i.Name   
        from deleted i   
    open cursd   
    while 1=1   
    begin  
        fetch next from cursd into @Name  
        if @@FETCH_STATUS <> 0 break     
        if object_id(@Name+N'.CurrentValue', N'FN') is not null  
        exec(N'drop function '+@Name+N'.CurrentValue'  
        if object_id(@Name+N'.NextValue', N'FN') is not null 
        exec(N'drop function '+@Name+N'.NextValue'  
        if object_id(@Name+N'.NextValueHole', N'FN') is not null  
        exec(N'drop function '+@Name+N'.NextValueHole'  
        if object_id(@Name+N'.RegisterHole', N'P') is not null  
        exec(N'drop procedure '+@Name+N'.RegisterHole' 
    -- If generator's schema is "empty" (i.e. it was used only by generator)   
        if not exists(select * from sys.all_objects where schema_id=schema_id(@Name))  
        exec(N'drop schema '+@Name)   
    end
    
    declare cursi cursor local static forward_only for   
        select i.Name, i.ID, i.IsWorkWithHoles   
        from inserted i   
    open cursi   
    while 1=1   
    begin   
        fetch next from cursi into @Name, @ID, @IsWorkWithHoles   
        if @@FETCH_STATUS <> 0 break        
  
        set @FunctionName = 'NextValue' + case when @IsWorkWithHoles=1 then 'Hole' else '' end   
      
        if SCHEMA_ID(@Name) is null   
            exec(N'create schema G$'+@Name)
  
        if object_id(@Name+N'.CurrentValue') is null   
        begin
              set @sql = N'create function G$'+@Name+N'.CurrentValue(@Sequence nvarchar(256))
returns int as 
  begin   
    declare @CV int
 
    select @CV=CurrentValue   
    from _Generator.Sequence   
    where Name='''+@Name+N'.''+isnull(''.''+@Sequence, ''''
  
    if @@rowcount = 0    
        select  @CV = StartValue  
        from _Generator.List    
        where ID = ''' +convert(varchar(40), @ID)+N'''
  
        return @CV
end
         exec(@sql)   
        end     
        if object_id(@Name+N'.NextValue') is null   
        begin   
            set @sql = N'create function G$'+@Name+N'.NextValue(@Sequence nvarchar(256)) returns int as   
    begin   
     return (_Generator.'+@FunctionName+N'('''+@Name+'.''+isnull(''.''+@Sequence, ''''), @@SPID, @@SERVERNAME, DB_NAME()))
  
end'
              exec(@sql)   
        end  
    end 
end

Обратите внимание - триггер создан с опцией with execute as owner, что позволяет фактически превратить операции по вставке данных в таблицу в DDL операции, расширяющие синтаксис в нашей базе. При этом создающий генератор пользователь вовсе не должен иметь какие-либо права на модификацию структуры БД.
Кроме того, кому-то будет удобнее так отлаживать сам механизм.

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

create table _Generator.Hole  
  
     Sequence nvarchar(256) not null  
     HoleValue int not null  
     constraint PK__Generator_Hole primary key(Sequence, HoleValue)   
  
go

Процедура добавления генератора в таблицу _Generator.List.
Она в общем-то скорее нужна, чтобы продемонстрировать, что достаточно дать права например на запуск одной процедуры для создания новых генераторов.

create procedure _Generator.New   
    @Name sysname, -- Имя генератора   
    @StartValue int, -- Начальное значение   
    @IsWorkWithHoles tinyint -- Работа с пропущенными значениями   
as 
begin
    set nocount on
    if not exists(
        select
        from _Generator.List 
        where Name = @Name
        )
  
    insert into _Generator.List(Name, StartValue, IsWorkWithHoles)
    values(@Name, @StartValue, @IsWorkWithHoles)
end  
go 

Теперь наконец-то создадим таблицу с текущими значениями счетчиков и процедуру, генерирующую значения.

Процедур будет 2 - одна для генераторов, для которых укist(Name, StartValue, IsWorkWithHoles)