Ändern des Primärschlüssels von IDENTITY in Persistent mit COALESCE

10

Bei dem Versuch, eine Anwendung von unserer monolithischen Datenbank zu entkoppeln, haben wir versucht, die INT IDENTITY-Spalten verschiedener Tabellen in eine PERSISTED-berechnete Spalte zu ändern, die COALESCE verwendet. Grundsätzlich benötigen wir für die entkoppelte Anwendung die Möglichkeit, die Datenbank für gemeinsame Daten, die von vielen Anwendungen gemeinsam genutzt werden, zu aktualisieren, während vorhandene Anwendungen weiterhin Daten in diesen Tabellen erstellen können, ohne dass Code- oder Prozeduränderungen erforderlich sind.

Im Wesentlichen haben wir uns von einer Spaltendefinition von entfernt.

PkId INT IDENTITY(1,1) PRIMARY KEY

zu;

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

In allen Fällen ist die PkId auch ein PRIMARY KEY und in allen bis auf einen Fall ist sie CLUSTERED. Alle Tabellen haben dieselben Fremdschlüssel und Indizes wie zuvor. Im Wesentlichen ermöglicht das neue Format die Bereitstellung der PkId durch die entkoppelte Anwendung (als external_id), ermöglicht jedoch auch, dass die PkId der IDENTITY-Spaltenwert ist, sodass vorhandener Code, der auf der IDENTITY-Spalte basiert, mithilfe von SCOPE_IDENTITY und @@ IDENTITY verwendet wird arbeiten wie früher.

Das Problem, das wir hatten, ist, dass wir auf einige Abfragen gestoßen sind, die früher in einer akzeptablen Zeit ausgeführt wurden, um jetzt vollständig zu verschwinden. Die generierten Abfragepläne, die von diesen Abfragen verwendet werden, sind nicht mehr so ​​wie früher.

Angesichts der Tatsache, dass die neue Spalte ein PRIMARY KEY mit demselben Datentyp wie zuvor und PERSISTED ist, hätte ich erwartet, dass sich Abfragen und Abfragepläne genauso verhalten wie zuvor. Sollte sich die COMPUTED PERSISTED INT PkId im Wesentlichen genauso verhalten wie eine explizite INT-Definition in Bezug darauf, wie SQL Server den Ausführungsplan erstellt? Gibt es andere wahrscheinliche Probleme mit diesem Ansatz, die Sie sehen können?

Der Zweck dieser Änderung sollte es uns ermöglichen, die Tabellendefinition zu ändern, ohne vorhandene Prozeduren und Code ändern zu müssen. Angesichts dieser Probleme habe ich nicht das Gefühl, dass wir diesen Ansatz verfolgen können.

Herr Elch
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Paul White 9

Antworten:

4

ZUERST

Sie müssen wahrscheinlich alle drei Spalten nicht: old_id, external_id, new_id. Als new_idSpalte IDENTITYwird für jede Zeile ein neuer Wert generiert, auch wenn Sie in einfügen external_id. Aber zwischen old_idund schließen external_idsich diese so ziemlich gegenseitig aus: Entweder gibt es bereits einen old_idWert, oder diese Spalte wird in der aktuellen Konzeption nur verwendet, NULLwenn external_idoder verwendet wird new_id. Da Sie einer bereits vorhandenen Zeile (dh einer mit einem old_idWert) keine neue "externe" ID hinzufügen und keine neuen Werte eingehen old_id, kann eine Spalte verwendet werden für beide Zwecke.

Entfernen Sie also die external_idSpalte und benennen Sie old_idsie in etwas Ähnliches old_or_external_idoder was auch immer um. Dies sollte keine wirklichen Änderungen an irgendetwas erfordern, reduziert jedoch einige der Komplikationen. Möglicherweise müssen Sie die Spalte höchstens aufrufen external_id, auch wenn sie "alte" Werte enthält, wenn bereits App-Code zum Einfügen geschrieben wurde external_id.

Das reduziert die neue Struktur auf gerecht:

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

Jetzt haben Sie nur 8 Bytes pro Zeile anstelle von 12 Bytes hinzugefügt (vorausgesetzt, Sie verwenden die SPARSEOption oder Datenkomprimierung nicht). Und Sie mussten keinen Code, T-SQL oder App-Code ändern.

ZWEITE

Wenn wir diesen Weg der Vereinfachung fortsetzen, schauen wir uns an, was wir noch übrig haben:

  • Die old_or_external_idSpalte enthält entweder bereits Werte oder erhält von der App einen neuen Wert oder wird als belassen NULL.
  • Für new_idwird immer ein neuer Wert generiert, aber dieser Wert wird nur verwendet, wenn die old_or_external_idSpalte ist NULL.

Es gibt nie eine Zeit, in der Sie Werte in old_or_external_idund benötigen würden new_id. Ja, es wird Zeiten geben, in denen beide Spalten Werte haben, weil new_idsie ein sind IDENTITY, aber diese new_idWerte werden ignoriert. Auch diese beiden Felder schließen sich gegenseitig aus. So was nun?

Jetzt können wir untersuchen, warum wir das überhaupt brauchten external_id. In Anbetracht der Tatsache, dass das Einfügen in eine IDENTITYSpalte mithilfe von möglich ist SET IDENTITY_INSERT {table_name} ON;, können Sie keine Schemaänderungen vornehmen und nur Ihren App-Code ändern, um die INSERTAnweisungen / Operationen SET IDENTITY_INSERT {table_name} ON;und SET IDENTITY_INSERT {table_name} OFF;Anweisungen einzuschließen. Sie müssen dann bestimmen, auf welchen Startbereich die IDENTITYSpalte zurückgesetzt werden soll (für neu generierte Werte), da dieser deutlich über den Werten liegen muss, die der App-Code einfügt, da das Einfügen eines höheren Werts den nächsten automatisch generierten Wert verursacht größer sein als der aktuelle MAX-Wert. Sie können jedoch jederzeit einen Wert einfügen, der unter dem Wert IDENT_CURRENT liegt.

Das Kombinieren der Spalten old_or_external_idund new_iderhöht nicht auch die Wahrscheinlichkeit, dass eine automatisch überlappende Wertsituation zwischen automatisch generierten Werten und von der App generierten Werten auftritt, da die Absicht besteht, die Spalten 2 oder sogar 3 zu einem Primärschlüsselwert zu kombinieren. und das sind immer eindeutige Werte.

Bei diesem Ansatz müssen Sie nur:

  • Lassen Sie die Tabellen wie folgt:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Dies fügt jeder Zeile 0 Bytes hinzu, anstatt 8 oder sogar 12.

  • Bestimmen Sie den Startbereich für von der App generierte Werte. Diese sind größer als der aktuelle MAX-Wert in jeder Tabelle, aber kleiner als der Mindestwert für die automatisch generierten Werte.
  • Bestimmen Sie, bei welchem ​​Wert der automatisch generierte Bereich beginnen soll. Zwischen dem aktuellen MAX-Wert und viel Platz zum Wachsen sollte viel Platz sein, da die Obergrenze bei etwas mehr als 2,14 Milliarden liegt. Sie können diesen neuen minimalen Startwert dann über DBCC CHECKIDENT festlegen .
  • Wrap App-Code INSERTs in SET IDENTITY_INSERT {table_name} ON;und SET IDENTITY_INSERT {table_name} OFF;Anweisungen.

ZWEITENS, Teil B.

Eine Variation des direkt oben erwähnten Ansatzes wäre, dass der App-Code Werte einfügt, die mit -1 beginnen und von dort nach unten gehen . Dadurch bleiben die IDENTITYWerte als die einzigen , die gehen nach oben . Der Vorteil hierbei ist, dass Sie nicht nur das Schema nicht komplizieren, sondern sich auch keine Gedanken über überlappende IDs machen müssen (wenn die von der App generierten Werte in den neuen automatisch generierten Bereich laufen). Dies ist nur eine Option, wenn Sie noch keine negativen ID-Werte verwenden (und es scheint ziemlich selten zu sein, dass Benutzer negative Werte für automatisch generierte Spalten verwenden, sodass dies in den meisten Situationen wahrscheinlich sein sollte).

Bei diesem Ansatz müssen Sie nur:

  • Lassen Sie die Tabellen wie folgt:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Dies fügt jeder Zeile 0 Bytes hinzu, anstatt 8 oder sogar 12.

  • Der Startbereich für von der App generierte Werte ist -1.
  • Wrap App-Code INSERTs in SET IDENTITY_INSERT {table_name} ON;und SET IDENTITY_INSERT {table_name} OFF;Anweisungen.

Hier müssen Sie noch IDENTITY_INSERTFolgendes tun , aber: Sie fügen keine neuen Spalten hinzu, müssen keine IDENTITYSpalten "neu säen" und haben kein zukünftiges Risiko von Überlappungen.

ZWEITENS, Teil 3

Eine letzte Variante dieses Ansatzes wäre, möglicherweise die IDENTITYSpalten auszutauschen und stattdessen Sequenzen zu verwenden . Der Grund für diesen Ansatz besteht darin, dass der App-Code folgende Werte einfügen kann: positiv, über dem automatisch generierten Bereich (nicht unter) und nicht erforderlich SET IDENTITY_INSERT ON / OFF.

Bei diesem Ansatz müssen Sie nur:

  • Erstellen Sie Sequenzen mit CREATE SEQUENCE
  • Kopieren Sie die IDENTITYSpalte in eine neue Spalte, die nicht über die IDENTITYEigenschaft verfügt, jedoch eine DEFAULTEinschränkung aufweist, indem Sie die Funktion NEXT VALUE FOR verwenden:

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

    Dies fügt jeder Zeile 0 Bytes hinzu, anstatt 8 oder sogar 12.

  • Der Startbereich für von der App generierte Werte liegt weit über dem, was Ihrer Meinung nach die automatisch generierten Werte erreichen werden.
  • Wrap App-Code INSERTs in SET IDENTITY_INSERT {table_name} ON;und SET IDENTITY_INSERT {table_name} OFF;Anweisungen.

JEDOCH , aufgrund der Anforderung , dass Code entweder mit SCOPE_IDENTITY()oder @@IDENTITYnach wie vor einwandfrei funktioniert, um Sequenzen Schalt ist nicht eine Option , da es scheint , dass es keine Entsprechung dieser Funktionen für Sequenzen ist :-(. Sad!

Solomon Rutzky
quelle
Vielen Dank für Ihre Antwort. Sie sprechen einige Punkte an, die hier intern erörtert wurden. Leider funktionieren einige davon aus mehreren Gründen nicht für uns. Unsere Datenbank ist ziemlich alt und etwas spröde und läuft im Kompatibilitätsmodus 2005, sodass SEQUENCES nicht verfügbar sind. Unser App-Daten-Push erfolgt über ein Datenladetool, das neue Datensätze aus Service Broker-Warteschlangen abruft und diese über mehrere Threads überträgt. IDENTITY_INSERT kann nur für eine Tabelle pro Sitzung verwendet werden, und derzeit wird davon ausgegangen, dass unsere Architektur dies nicht ohne wesentliche Änderungen berücksichtigen kann. Ich teste jetzt Ihren ersten Vorschlag.
Herr Moose
@ MrMoose Ja, ich habe meine Antwort aktualisiert, um am Ende weitere Informationen zu Sequenzen aufzunehmen. In Ihrer Situation würde es sowieso nicht funktionieren. Ich habe mich über mögliche Parallelitätsprobleme gewundert IDENTITY_INSERT, diese aber nicht getestet. Sie sind sich nicht sicher, ob Option 1 Ihr Gesamtproblem lösen wird. Es war nur eine Beobachtung, um unnötige Komplexität zu reduzieren. Wie können Sie jedoch sicherstellen, dass mehrere Threads, die neue "externe" IDs einfügen, eindeutig sind?
Solomon Rutzky
@MrMoose Was genau ist hier in Bezug auf " IDENTITY_INSERT kann nur für eine Tabelle pro Sitzung verwendet werden " das Problem? 1) Sie können jeweils nur eine Tabelle einfügen, also deaktivieren Sie sie für Tabelle A, bevor Sie sie in Tabelle B einfügen. 2) Ich habe sie gerade getestet und entgegen meiner Meinung gibt es keine Parallelitätsprobleme - ich konnte habe IDENTITY_INSERT ONfür die gleiche Tabelle in zwei Sitzungen und wurde ohne Probleme in beide eingefügt.
Solomon Rutzky
1
Wie Sie vorgeschlagen haben, hat Änderung 1 kaum einen Unterschied gemacht. Die ID, die wir verwenden, wird außerhalb der aktuellen Datenbank zugewiesen und zum Verknüpfen von Datensätzen verwendet. Es kann durchaus sein, dass mein Verständnis von Sitzungen nicht ganz richtig ist, sodass IDENTITY_INSERT möglicherweise funktioniert. Es wird allerdings einige Zeit dauern, bis ich das untersucht habe, sodass ich mich für eine Weile nicht melden kann. Nochmals vielen Dank für die Eingabe. Es wird sehr geschätzt.
Herr Moose
1
Ich denke, Ihr Vorschlag, IDENTITY_INSERT (mit einem hohen Startwert für vorhandene Apps) zu verwenden, wird gut funktionieren. Aaron Bertrand zur Verfügung gestellt , eine Antwort hier mit einem guten kleinen Beispiel auf sie mit Nebenläufigkeit zu testen. Wir haben unser Datenladetool so geändert, dass es Tabellen verarbeiten kann, in denen Identitätswerte angegeben werden müssen, und wir werden in den kommenden Wochen weitere Tests durchführen.
Herr Moose