'Id' в формате: YYYYNNNNNN с повторным запуском части NNNNNN каждый год

11

У меня есть бизнес-требование, чтобы каждая запись в таблице счетов имела идентификатор, который выглядит как ГГГГНННННН.

Часть NNNNNN должна перезапускаться в начале каждого года. Итак, первая строка, введенная в 2016 году, будет выглядеть как 2016000001, а вторая как 2016000002 и т. Д. Допустим, последняя запись за 2016 год была 2016123456, Следующая строка (из 2017) должна выглядеть как 2017000001

Мне не нужен этот идентификатор, чтобы быть первичным ключом, и я также храню дату создания. Идея состоит в том, что этот «идентификатор отображения» является уникальным (поэтому я могу сделать запрос по нему) и способен работать с группой людей по годам.

Вряд ли какие-либо записи будут удалены; однако я был бы склонен защищаться от чего-то подобного.

Есть ли способ, которым я мог бы создать этот идентификатор без необходимости запрашивать максимальный идентификатор в этом году каждый раз, когда вставлять новую строку?

Идеи:

  • А CreateNewInvoiceSP, который получает MAXзначение за этот год (хрен)
  • Какая-то волшебная встроенная функция для именно этого (я могу мечтать правильно)
  • Возможность указать какой-либо UDF или что-то в объявлении IDENTITYили DEFAULT(??)
  • Представление, которое использует PARTITION OVER + ROW()(удалено будет проблематично)
  • Триггер включен INSERT(все равно нужно выполнить какой-то MAXзапрос :()
  • Ежегодная справочная работа, обновляемая таблица с MAX для каждого вставленного года, который я тогда ... Что-то ?!

Все это немного не идеально. Любые идеи или варианты приветствуются!

DarcyThomas
источник
У вас есть хорошие ответы, но если у вас есть год, идентифицируйте как PK, тогда выберите max довольно быстро.
Папараццо
использование запроса select max id является обычной практикой. используйте это.
Угур Гюмюшан,

Ответы:

17

На вашем поле есть 2 элемента

  • Год
  • Автоматическое увеличение числа

Их не нужно хранить как одно поле

Пример:

  • Столбец года, который имеет значение по умолчанию YEAR(GETDATE())
  • Числовой столбец на основе последовательности.

Затем создайте вычисляемый столбец, объединяя их (с соответствующим форматированием). Последовательность может быть сброшена при смене года.

Пример кода в SQLfiddle : * (SQLfiddle не всегда работает)

-- Create a sequence
CREATE SEQUENCE CountBy1
    START WITH 1
    INCREMENT BY 1 ;

-- Create a table
CREATE TABLE Orders
    (Yearly int NOT NULL DEFAULT (YEAR(GETDATE())),
    OrderID int NOT NULL DEFAULT (NEXT VALUE FOR CountBy1),
    Name varchar(20) NOT NULL,
    Qty int NOT NULL,
    -- computed column
    BusinessOrderID AS RIGHT('000' + CAST(Yearly AS VARCHAR(4)), 4)
                     + RIGHT('00000' + CAST(OrderID AS VARCHAR(6)), 6),
    PRIMARY KEY (Yearly, OrderID)
    ) ;


-- Insert two records for 2015
INSERT INTO Orders (Yearly, Name, Qty)
    VALUES
     (2015, 'Tire', 7),
     (2015, 'Seat', 8) ;


-- Restart the sequence (Add this also to an annual recurring 'Server Agent' Job)
ALTER SEQUENCE CountBy1
    RESTART WITH 1 ;

-- Insert three records, this year.
INSERT INTO Orders (Name, Qty)
    VALUES
     ('Tire', 2),
     ('Seat', 1),
     ('Brake', 1) ;
ГБН
источник
1
Может быть, чище иметь одну последовательность в год. Таким образом, нет необходимости выполнять DDL как часть обычных операций.
USR
@gbn Итак, мне нужна фоновая работа для перезапуска SEQUENCE в начале каждого года?
DarcyThomas
@usr К сожалению, вы не можете использовать NEXT VALUE FORв CASEзаявлении (я пытался)
DarcyThomas
8

Рассматривали ли вы создать поле идентификации с seed = 2016000000?

 create table Table1 (
   id bigint identity(2016000000,1),
   field1 varchar(20)...
)

Это семя должно автоматически автоинкрементироваться каждый год, например, ночью 2017/1/1 вы должны запланировать

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Но я уже вижу проблемы с дизайном, например: что если у вас есть миллион записей?

Лия Танская
источник
2
Другая проблема, если записи не появляются в хронологическом порядке. Идентичность, вероятно, не тот путь, если это так.
Даниэль Хутмахер
@LiyaTansky В моем случае мне сказали, что должно быть только 50 тысяч записей в год. Но я понимаю, что ты имеешь в виду, что он хрупок с 1кк строк
DarcyThomas
1

В этом сценарии я умножил год на 10 ^ 6 и добавил к нему значение последовательности. Это имеет то преимущество, что не требует вычисляемого поля с его (небольшими) текущими издержками, и поле можно использовать как PRIMARY KEY.

Есть две возможные ошибки:

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

  • вам не гарантируется последовательность без пробелов из-за кэширования последовательности.

Я не эксперт по SQL Server, но вы, вероятно, можете установить событие для запуска в 201x 00:00:00, чтобы сбросить вашу последовательность в ноль. Это также то, что я сделал на Firebird (или это был Interbase?).

Verace
источник
1

Изменить: это решение не работает под нагрузкой

Я не фанат триггеров, но это, кажется, лучшее, что я мог бы решить.

Плюсы:

  • Нет фоновых заданий
  • Может делать быстрые запросы на DisplayId
  • Триггер не должен сканировать предыдущую часть NNNNNN
  • Будет перезапускать часть NNNNN каждый год
  • Будет работать, если есть более 100000 строк в год
  • Не требует обновлений схемы (например, сброса последовательности), чтобы продолжать работать в будущем

Изменить: Минусы:

  • Сбой под нагрузкой (обратно на чертежную доску)

(Благодарю @gbn, поскольку я черпал вдохновение из их ответа) (Любая обратная связь и указание на очевидные ошибки приветствуются :)

Добавить несколько новых COLUMNиINDEX

ALTER TABLE dbo.Invoices
ADD     [NNNNNNId]      INT  NULL 

ALTER TABLE dbo.Invoices
ADD [Year]              int NOT NULL DEFAULT (YEAR(GETDATE()))

ALTER TABLE dbo.Invoices
ADD [DisplayId]     AS  'INV' +
                        CAST([Year] AS VARCHAR(4))+
                        RIGHT('00000' + CAST([NNNNNNId] AS VARCHAR(4)),  IIF (5  >= LEN([NNNNNNId]), 5, LEN([NNNNNNId])) )                  

EXEC('CREATE NONCLUSTERED INDEX IX_Invoices_DisplayId
ON dbo.Invoices (DisplayId)')

Добавить новый TRIGGER

CREATE TRIGGER Invoices_DisplayId
ON dbo.Invoices
  AFTER  INSERT
AS 
BEGIN

SET NOCOUNT ON;    

UPDATE dbo.Invoices
SET NNNNNNId = CalcDisplayId
FROM (SELECT I.ID, IIF (Previous.Year = I.Year , (ISNULL(Previous.NNNNNNId,0) + 1), 1) AS CalcDisplayId  FROM
        (SELECT 
            ID  
           ,NNNNNNId 
           ,[year]
        FROM  dbo.Invoices
        ) AS Previous
    JOIN inserted AS I 
    ON Previous.Id = (I.Id -1) 
    ) X
WHERE 
   X.Id = dbo.Invoices.ID       
END
GO
DarcyThomas
источник
Я настоятельно рекомендую не делать этого. Это может привести к взаимоблокировке и возникновению сбоев вставки, когда вы окажетесь под небольшой нагрузкой. Вы положили копию в фиктивную базу данных и забили ее несколькими десятками потоков, одновременно выполняющих вставки (и, возможно, также выбирает / обновляет / удаляет), чтобы увидеть, что происходит?
Коди Кониор,
@CodyKonior - это в корне ошибочный или его можно воскресить с некоторой разумной блокировкой? Если нет, то как бы вы подошли к проблеме?
DarcyThomas
Хммм. Побежал с 10 темы. Не уверен, что это мертвые замки, но я получаю некоторые условия гонки. Где завершается один триггер, до того, как триггер предыдущих строк завершился. Это приводит к вводу множества NULLзначений. Вернуться к чертежной доске ...
DarcyThomas
Бедствие предотвращено :-) Мой секрет в том, что я узнал образец того, что сделал около пяти лет назад. Я только знаю, что то, как вы сканируете таблицу внутри триггера в поисках следующей последовательности, запутывает вещи под нагрузкой. Я не помню, как я решил это, но я могу проверить позже.
Коди Кониор,
@CodyKonior Я не думаю, что это делает сканирование ( ON Previous.Id = (I.Id -1) должен просто искать), но да, по-прежнему не работает. Если бы я мог заблокировать таблицу (?) Во время вставки и запуска, то я думаю, что это сработало бы. Но это звучит как запах кода.
DarcyThomas