Зміна первинного ключа з IDENTITY на постійний Обчислений стовпець за допомогою COALESCE


10

Намагаючись роз'єднати додаток з нашої монолітної бази даних, ми намагалися змінити стовпці INT IDENTITY різних таблиць, щоб вони були ПЕРСИСТИЧНИМ обчисленим стовпцем, що використовує 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 бути значенням стовпця ІДЕНТИМНОСТІ, тому дозволяючи існуючому коду, який спирається на стовпець Ідентичність за допомогою використання SCOPE_IDENTITY та @@ IDENTITY працювати так, як раніше

Проблема у нас полягає в тому, що ми зіткнулися з декількома запитами, які використовувались у прийнятний час і тепер повністю вибухають. Генеровані плани запитів, які використовуються цими запитами, є не що інше, як раніше.

З огляду на те, що новий стовпець - це ПЕРШИЙ КЛЮЧ, той самий тип даних, що і раніше, і ПЕРСОНАЛЬНО, я б очікував, що запити та запити плануватимуться так само, як і раніше. Повинен чи обчислюваний ПЕРСИСТОВАНИЙ INT PkId поводитися так само, як явне визначення 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 .
  • Оберніть код програми ВСТАВЛЕННЯ в 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.
  • Оберніть код програми ВСТАВЛЕННЯ в 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.

  • Початковий діапазон значень, створених додатком, буде набагато вище того, що, на вашу думку, наблизиться автоматично створених значень.
  • Оберніть код програми ВСТАВЛЕННЯ в SET IDENTITY_INSERT {table_name} ON;і SET IDENTITY_INSERT {table_name} OFF;заяви.

ВІДПОВІДЬ , через вимогу, що код з будь-якою SCOPE_IDENTITY()або @@IDENTITYвсе-таки функціонує належним чином, перехід на послідовності наразі не є варіантом, оскільки, здається, немає еквівалента цих функцій для послідовностей :-(. Сумно!


Дуже дякую за вашу відповідь. Ви піднімаєте кілька пунктів, які обговорювалися тут всередині. На жаль, деякі з них не спрацюють для нас з кількох причин. Наша база даних досить стара і дещо крихка і працює в режимі сумісності 2005 року, тому SEQUENCES не працює. Наш поштовх даних додатків відбувається за допомогою інструмента завантаження даних, який отримує нові записи з черг сервісного брокера та передає їх через кілька потоків. IDENTITY_INSERT можна використовувати лише для однієї таблиці за сеанс, а сучасне мислення - наша архітектура не може задовольнити це без істотних змін. Я зараз тестую ваше кулакове пропозицію.
Містер Муз

@MrMoose Так, я оновив свою відповідь, щоб включити більше інформації про послідовності наприкінці. У вашій ситуації це все одно не вийде. І мені було цікаво про можливі проблеми з одночасністю IDENTITY_INSERT, але не перевіряли. Не впевнений, що варіант №1 вирішить вашу загальну проблему, це було лише спостереженням, щоб зменшити непотрібність. Однак якщо у вас є кілька потоків, які вставляють нові "зовнішні" ідентифікатори, як ви гарантуєте, що вони унікальні?
Соломон Руцький

@MrMoose Насправді, щодо " IDENTITY_INSERT можна використовувати лише для однієї таблиці за сеанс ", в чому саме полягає проблема? 1) ви можете вставляти лише одну таблицю за один раз, тому ви вимикаєте її для TableA перед тим, як вставляти її в TableB, і 2) Я щойно тестував і всупереч тому, що я думав, немає проблем з одночасністю - я зміг мають IDENTITY_INSERT ONодну і ту ж таблицю в два сеанси і вставляли в обидва без проблем.
Соломон Руцький

1
Як ви запропонували, зміна 1 мало значення. Ідентифікатор, який ми будемо використовувати, буде виділятися за межами поточної бази даних та використовуватись для відновлення записів. Можливо, моє розуміння сеансів не зовсім правильне, тому IDENTITY_INSERT може працювати. Мені знадобиться небагато часу, щоб розглянути це, хоча я не зможу трохи звітувати. Ще раз дякую за вклад. Це дуже цінується.
Містер Муз

1
Я думаю, що ваша пропозиція використовувати IDENTITY_INSERT (з високим значенням насіння для існуючих додатків) буде добре працювати. Аарон Бертран дав відповідь тут, маючи невеликий приклад тестування його з одночасністю. Ми змінили наш інструмент для завантаження даних, щоб мати можливість обробляти таблиці, в яких потрібно вказати значення ідентичності, і ми прийдемо до подальшого тестування в найближчі тижні.
Містер Муз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.