Изменение первичного ключа с IDENTITY на сохранение. Вычисляемый столбец с использованием COALESCE.

10

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

По сути, мы перешли от определения столбца;

PkId INT IDENTITY(1,1) PRIMARY KEY

к;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

Во всех случаях PkId также является ПЕРВИЧНЫМ КЛЮЧОМ, и во всех случаях, кроме одного, он КЛАСТЕР. Все таблицы имеют те же внешние ключи и индексы, что и раньше. По сути, новый формат позволяет PkId предоставляться отделенным приложением (как external_id), но также позволяет PkId быть значением столбца IDENTITY, что позволяет существующему коду, который опирается на столбец IDENTITY, используя SCOPE_IDENTITY и @@ IDENTITY. работать как раньше.

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

Учитывая, что новый столбец - PRIMARY KEY, тот же тип данных, что и раньше, и PERSISTED, я ожидал, что запросы и планы запросов будут вести себя так же, как и раньше. Должен ли PkId COMPUTED PERSISTED INT вести себя точно так же, как и явное определение INT с точки зрения того, как SQL Server будет генерировать план выполнения? Есть ли другие вероятные проблемы с этим подходом, которые вы можете увидеть?

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

Мистер лось
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт 9

Ответы:

4

ПЕРВЫЙ

Вы , вероятно , не нужны все три колонки: old_id, external_id, new_id. new_idКолонна, будучи IDENTITY, будет иметь новое значение , сгенерированное для каждой строки, даже при вставке в external_id. Но, между old_idи external_id, они в значительной степени взаимоисключающие: либо old_idзначение уже есть, либо этот столбец, в текущей концепции, будет просто NULLпри использовании external_idили new_id. Поскольку вы не будете добавлять новый «внешний» идентификатор в строку, которая уже существует (то есть, в которой есть old_idзначение), и не будет никаких новых значений old_id, то может быть один столбец, который используется для обеих целей.

Таким образом, избавьтесь от external_idстолбца и переименуйте, old_idчтобы быть что-то вроде old_or_external_idили как- то еще. Это не должно требовать каких-либо реальных изменений, но уменьшает некоторые осложнения. Самое большее, вам может понадобиться вызвать столбец external_id, даже если он содержит «старые» значения, если код приложения уже написан для вставки external_id.

Это уменьшает новую структуру, чтобы быть просто:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Теперь вы добавили только 8 байтов в строку вместо 12 байтов (при условии, что вы не используете SPARSEопцию или сжатие данных). И вам не нужно было менять код, T-SQL или код приложения.

ВТОРОЙ

Продолжая этот путь упрощения, давайте посмотрим на то, что мы оставили:

  • old_or_external_idСтолбец либо имеет значение уже, или будет дан новое значение из приложения, или оставить как NULL.
  • new_idВсегда будет иметь новое значение , сгенерированное, но это значение будет использовано только если old_or_external_idстолбец NULL.

Никогда не бывает времени, когда вам понадобятся значения как в, так old_or_external_idи в new_id. Да, будут времена, когда оба столбца имеют значения из-за new_idтого, что они являются IDENTITY, но эти new_idзначения игнорируются. Опять же, эти два поля являются взаимоисключающими. И что теперь?

Теперь мы можем понять, зачем нам это было нужно external_id. Учитывая , что можно вставить в IDENTITYколонку с помощью SET IDENTITY_INSERT {table_name} ON;, вы могли бы уйти с не делая никаких изменений схемы на всех, и только изменить код приложения , чтобы обернуть INSERTзаявления / операции в SET IDENTITY_INSERT {table_name} ON;и SET IDENTITY_INSERT {table_name} OFF;заявлении. Затем вам нужно определить, к какому начальному диапазону будет возвращаться IDENTITYстолбец (для вновь сгенерированных значений), так как он должен быть значительно выше значений, которые будет вставлять код приложения, поскольку при вставке более высокого значения следующее автоматически сгенерированное значение будет быть больше, чем текущее значение MAX. Но вы всегда можете вставить значение ниже значения IDENT_CURRENT .

Объединяя old_or_external_idи new_idстолбцы также не увеличивают шансы нарваться значением ситуации , перекрывающей между автоматически сгенерированными значениями и приложениями сгенерированными значениями с целью имеющих 2 или даже 3, колонны, чтобы объединить их в значение первичного ключа, и это всегда уникальные ценности.

При таком подходе вам просто необходимо:

  • Оставьте таблицы как:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Это добавляет 0 байтов к каждой строке вместо 8 или даже 12.

  • Определите начальный диапазон для значений, генерируемых приложением. Они будут больше, чем текущее значение MAX в каждой таблице, но меньше того, что станет минимальным значением для автоматически сгенерированных значений.
  • Определите, с какого значения должен начинаться автоматически сгенерированный диапазон. Должно быть много места между текущим значением MAX и достаточным пространством для роста, зная, что верхний предел составляет чуть более 2,14 миллиарда. Затем вы можете установить это новое минимальное начальное значение с помощью DBCC CHECKIDENT .
  • Оберните код приложения INSERT в SET IDENTITY_INSERT {table_name} ON;и SET IDENTITY_INSERT {table_name} OFF;заявления.

ВТОРАЯ, часть Б

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

При таком подходе вам просто необходимо:

  • Оставьте таблицы как:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Это добавляет 0 байтов к каждой строке вместо 8 или даже 12.

  • Начальный диапазон для сгенерированных приложением значений будет -1.
  • Оберните код приложения INSERT в SET IDENTITY_INSERT {table_name} ON;и SET IDENTITY_INSERT {table_name} OFF;заявления.

Здесь вам все еще нужно сделать IDENTITY_INSERT, но: вы не добавляете новые столбцы, не нуждаетесь в том, чтобы «повторно заполнять» какие-либо IDENTITYстолбцы, и у вас нет будущего риска наложения.

ВТОРАЯ, часть 3

Последним вариантом этого подхода может быть замена IDENTITYстолбцов и использование последовательностей . Причиной такого подхода является наличие возможности вставлять в код приложения значения, которые являются: положительными, выше автоматически сгенерированного диапазона (не ниже) и не нуждаются в этом SET IDENTITY_INSERT ON / OFF.

При таком подходе вам просто необходимо:

  • Создать последовательности, используя CREATE SEQUENCE
  • Скопируйте IDENTITYстолбец в новый столбец, который не имеет IDENTITYсвойства, но имеет DEFAULTограничение, используя функцию NEXT VALUE FOR :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Это добавляет 0 байтов к каждой строке вместо 8 или даже 12.

  • Начальный диапазон для значений, сгенерированных приложением, будет намного выше того, что, как вы думаете, подойдет к автоматически сгенерированным значениям.
  • Оберните код приложения INSERT в SET IDENTITY_INSERT {table_name} ON;и SET IDENTITY_INSERT {table_name} OFF;заявления.

ОДНАКО , из-за требования, чтобы код с одним SCOPE_IDENTITY()или @@IDENTITYвсе еще функционировал должным образом, переключение на Последовательности в настоящее время не вариант, поскольку кажется, что нет эквивалента этих функций для Последовательностей :-(. Грустно!

Соломон Руцкий
источник
Большое спасибо за ваш ответ. Вы поднимаете несколько вопросов, которые обсуждались здесь внутри. К сожалению, некоторые из них не будут работать для нас по нескольким причинам. Наша база данных довольно старая и несколько хрупкая и работает в режиме совместимости 2005 года, поэтому SEQUENCES отсутствуют. Наша передача данных приложения происходит через инструмент загрузки данных, который получает новые записи из очередей компонента Service Broker и передает их через несколько потоков. IDENTITY_INSERT может использоваться только для одной таблицы за сеанс, и в настоящее время мы думаем, что наша архитектура не сможет справиться с этим без существенных изменений. Сейчас я проверяю твое предположение.
Мистер Мус
@MrMoose Да, я обновил свой ответ, чтобы добавить больше информации о последовательностях в конце. Это не сработает в вашей ситуации. И я задавался вопросом о потенциальных проблемах параллелизма с IDENTITY_INSERT, но не проверял это. Не уверен, что вариант № 1 решит вашу общую проблему, это было просто наблюдение, чтобы уменьшить ненужную сложность. Тем не менее, если у вас есть несколько потоков, вставляющих новые «внешние» идентификаторы, как вы можете гарантировать их уникальность?
Соломон Руцкий
@MrMoose На самом деле, что касается " IDENTITY_INSERT может использоваться только для одной таблицы за сеанс ", что именно здесь проблема? 1) вы можете вставлять только в одну таблицу за раз, поэтому вы отключаете ее для TableA перед вставкой в ​​TableB, и 2) я только что проверил и вопреки тому, что я думал, нет проблем с параллелизмом - я смог есть IDENTITY_INSERT ONдля одной и той же таблицы в двух сессиях и вставлял в обе без проблем.
Соломон Руцкий
1
Как вы предложили, изменение 1 мало что изменило. Идентификатор, который мы будем использовать, будет размещен за пределами текущей базы данных и использован для связи записей. Вполне возможно, что мое понимание сессий не совсем верно, поэтому IDENTITY_INSERT может работать. Мне понадобится немного времени, чтобы разобраться в этом, поэтому я не смогу доложить немного позже. Еще раз спасибо за вклад. Это высоко ценится.
Мистер Мус
1
Я думаю, что ваше предложение использовать IDENTITY_INSERT (с высоким начальным значением для существующих приложений) будет работать хорошо. Аарон Бертран привел здесь ответ с хорошим небольшим примером проверки параллелизма. Мы изменили наш инструмент загрузки данных, чтобы иметь возможность обрабатывать таблицы, в которых необходимо указывать значения идентификаторов, и мы перейдем к дальнейшему тестированию в ближайшие недели.
Мистер Мус