Warum und wie bekomme ich manchmal den "fehlenden Tabellenfehler", während es definitiv eine Sch-M-Sperre gibt?

7

Ich habe einen synthetischen Test, der einige Fehler reproduziert, die wir in der Produktionsumgebung haben. Hier sind die 2 Skripte, um es zu reproduzieren:

1

DBCC TRACEOFF(-1,3604,1200) WITH NO_INFOMSGS;
SET NOCOUNT ON;
IF @@TRANCOUNT > 0 ROLLBACK
IF object_id('test') IS NOT NULL 
    DROP TABLE test
IF object_id('TMP_test') IS NOT NULL 
    DROP TABLE TMP_test
IF object_id('test1') IS NOT NULL 
    DROP TABLE test1
CREATE TABLE test(Id INT IDENTITY PRIMARY KEY)
GO
INSERT test DEFAULT VALUES
GO 2000
WHILE 1 = 1
BEGIN
    CREATE TABLE TMP_test(Id INT PRIMARY KEY)
    INSERT TMP_test SELECT * FROM test

    WAITFOR DELAY '0:00:00.1'
    BEGIN TRAN

    EXEC sp_rename 'test', 'test1'
    EXEC sp_rename 'TMP_test', 'test'
    EXEC sp_rename 'test1', 'TMP_test'


    DROP TABLE TMP_test
    commit
END

2 ..

SET NOCOUNT ON;

DECLARE @c INT
WHILE 1 = 1
BEGIN
    SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
    SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK

    /* and repeat this 10-20 times more*/
    SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
END

Das Problem ist also, dass ich diese Art von Fehler erhalten kann, wenn ich das erste Skript in einer Sitzung ausführe und es laufen lasse und dann das zweite in einer separaten Sitzung ausführe:

Meldung 208, Ebene 16, Status 1, Zeile 13 Ungültiger Objektname 'Test'.

Die Frage ist - WARUM sehe ich diesen Fehler für den Fall, dass COMMITam Ende des 1. Skripts ein Fehler auftritt und ich keinen bekomme, wenn es einen gibt ROLLBACK?

Ich habe das Gefühl, dass es irgendwie mit der Situation zusammenhängt, wenn das Skript festschreibt, gibt es immer noch die Tabelle mit dem Namen, testaber es ist ein anderes Objekt und das 2. Skript muss sich neu kompilieren. Und das ist in Ordnung. Aber warum wird der fehlende Tabellenfehler angezeigt? AFAIK - wenn ich die Tabelle innerhalb einer Transaktion umbenenne - hält sie die Sch-M-Sperre bis zum Ende?

Kann mir jemand antworten oder mich zu den technischen Artikeln führen, die ich gründlich lesen und den Grund verstehen kann?

Oleg Dok
quelle
1
Auf welche Isolationsstufe ist eingestellt?
Mark Wilkinson
Ich habe die Testskripte ursprünglich als READ COMMITTED ausgeführt, dachte aber auch, dass die Isolationsstufe von Bedeutung sein könnte. Es stellt sich jedoch heraus, dass der Fehler auch bei SERIALIZABLE auftritt.
Geoff Patterson

Antworten:

5

Wirklich interessante und schwierige Frage. Ich konnte keine offizielle Dokumentation dieses Verhaltens finden, und ich vermute, dass es keine gibt (obwohl ich es lieben würde, wenn mich jemand korrigiert!).

Meine Forschung lässt mich glauben, dass es der Planerstellungsschritt ist, der für diese Rennbedingung anfällig ist. Beachten Sie, dass ich Ihre Testabfragen eine Stunde lang fehlerfrei ausführen konnte. Wenn ich jedoch wiederholt Ihren Prozess starte, wird ein Teil der Zeit sofort ein Fehler angezeigt. Wenn ein Fehler auftritt, geschieht dies bei der Kompilierung des Plans immer sofort. Alternativ können Sie dem COUNT (*) in der Schleife "OPTION RECOMPILE" hinzufügen, um bei jedem Versuch einen neuen Plan zu kompilieren. Bei diesem Ansatz wird der Fehler fast sofort angezeigt, wenn ich Ihre Skripte ausgeführt habe.

Ich war in der Lage, den Fehler mit einer kontrollierten Reihe von Schritten zu reproduzieren, die den Fehler bei jedem Versuch zu treffen scheinen, wodurch die Notwendigkeit beseitigt wurde, eine Schleife einzurichten und sich auf Zufälligkeit zu verlassen.

Ich habe auch einen möglichen Fix eingebaut (um ALTER TABLE ... SWITCH zu verwenden), den Sie möglicherweise in Ihrer Produktionsumgebung ausprobieren können. Nun zu den Details!

Hier sind die Schritte zum Reproduzieren:

-- Instructions: Process each step one at a time, following any instructions in that step

-- (1) Initial setup: Clean any relics and create the test table
DBCC TRACEOFF(-1,3604,1200) WITH NO_INFOMSGS;
SET NOCOUNT ON;
IF @@TRANCOUNT > 0 ROLLBACK
IF object_id('test') IS NOT NULL 
    DROP TABLE test
IF object_id('TMP_test') IS NOT NULL 
    DROP TABLE TMP_test
IF object_id('test1') IS NOT NULL 
    DROP TABLE test1
CREATE TABLE test(Id INT IDENTITY CONSTRAINT PK_test PRIMARY KEY)
GO
INSERT test DEFAULT VALUES
GO 2000

-- (2) Run the COUNT(*)
-- This will acquire the Sch-S lock, and we use HOLDLOCK to retain that lock.
-- This simulates a race condition where this query is running at the time the first rename occurs.
BEGIN TRANSACTION
SELECT COUNT(*) FROM Test WITH (HOLDLOCK)
GO

-- (3) In another window, run the block of code that performs the renames
-- This will be blocked, waiting for a Sch-M lock on "test" until we complete step 4 below
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
EXEC sp_rename 'test', 'test1'
EXEC sp_rename 'TMP_test', 'test'
EXEC sp_rename 'test1', 'TMP_test'
DROP TABLE TMP_test
GO

-- (4) Now, commit the original COUNT(*) and immediately fire off the query again
-- We use OPTION (RECOMPILE) to make sure we need to compile a new query plan
-- The COMMIT releases the Sch-S lock, allowing (3) to acquire the Sch-M lock
-- This batch will now be waiting for the Sch-S lock again, on the same object_id,
-- but that object_id will no longer point to the correct object by the time the lock
-- is acquired.
COMMIT
SELECT COUNT(*) FROM Test OPTION (RECOMPILE)
GO

Mit diesen Schritten können wir etwas genauer untersuchen, was den Fehler verursacht. Zu diesem Zweck habe ich eine Ablaufverfolgung mit den folgenden Ereignissen ausgeführt: SP: StmtStarting, SP: StmtCompleted, Sperre: Erworben, Sperre: Freigegeben.

Was ich gefunden habe, ist, dass Folgendes in der richtigen Reihenfolge auftritt (wobei dazwischen einige Details weggelassen werden):

  1. Der erste COUNT (*) erhält eine Sch-S-Sperre
  2. Der erste COUNT (*) erhält einige Sperren für Statistiken (vermutlich, um einen Abfrageplan zu erstellen).
  3. Der erste COUNT (*) gibt eine Sch-S-Sperre frei (Ende der Planerstellung)
  4. Der erste COUNT (*) erhält eine IS-Sperre (Beginn der Ausführung) und wird dann erfolgreich ausgeführt
  5. Der sp_rename läuft bis zu dem Punkt, an dem eine Sch-M-Sperre erforderlich ist, und wird dann durch den ersten COUNT (*) blockiert, der noch nicht festgeschrieben ist
  6. Der erste COUNT (*) wird festgeschrieben
  7. Die sp_renames erwerben die Sch-M-Sperre
  8. Das zweite COUNT (*) fordert eine Sch-S-Sperre an und wird der Blockierungskette hinter dem sp_rename hinzugefügt
  9. Die sp_renames sind vollständig (zu diesem Zeitpunkt wartet der zweite COUNT (*) auf eine Sch-S-Sperre auf eine object_id, die nicht mehr die richtige object_id ist).
  10. Der zweite COUNT (*) erwirbt das Sch-S-Schloss
  11. Der zweite COUNT (*) erwirbt eine Sch-S-Sperre für sys.sysschobjs, vermutlich als Überprüfung der Integrität, da festgestellt wurde (oder bestätigt werden muss), dass etwas mit der Sch-S-Sperre, die für die Objekt-ID gewährt wurde, nicht stimmt sei "Test"
  12. Der Fehler "Test" des ungültigen Objektnamens wird ausgelöst

Wenn jedoch eine ähnliche Abfolge von Ereignissen auftritt, wenn keine Plankompilierung erforderlich ist (z. B. beim Ausführen Ihrer Schleife), ist SQL Server tatsächlich intelligent genug, um zu erkennen, dass sich die Objekt-ID geändert hat. Ich habe auch diesen Fall verfolgt und eine Situation gefunden, in der der zweite COUNT (*) die folgende Sequenz ausführte

  1. Erwirbt die Sch-S-Sperre (für die object_id, die früher "test" war)
  2. Erwirbt eine Sch-S-Sperre für sys.sysschobjs
  3. Löst diese beiden Sperren auf
  4. Erwirbt eine IS-Sperre für eine andere object_id; die neue object_id von "test"!
  5. Läuft erfolgreich bis zum Abschluss

Es sieht also so aus, als ob diese adaptive Logik vorhanden ist, wie Sie vermutet haben. Es sieht jedoch so aus, als ob es nur für die Ausführung von Abfragen und nicht für die Kompilierung von Abfragen vorhanden ist.

Wie versprochen, ist hier ein alternatives Snippet für Abschnitt (3), das eine Möglichkeit zum Umbenennen von Tabellen bietet, mit der das Problem der Parallelität behoben wird (zumindest auf meinem Computer!):

-- (3) In another window, run the block of code that performs the "renames"
-- Instead of sp_rename, we use ALTER TABLE...SWITCH
-- This appears to be a more robust way of performing the same logic
CREATE TABLE test1(Id INT PRIMARY KEY)
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
ALTER TABLE test SWITCH TO test1
ALTER TABLE TMP_test SWITCH TO test
ALTER TABLE test1 SWITCH TO TMP_test
DROP TABLE TMP_test
DROP TABLE test1
GO

Zum Schluss noch ein weiterer Link, den ich nützlich fand, um mir Gedanken darüber zu machen, wie man im Kontext von Tabellen, deren Namen sich ändern, über das Sperren nachdenken kann:

Geoff Patterson
quelle
Vielen Dank, ich markiere Ihre Antwort als relevant, aber beachten Sie, dass ich die Antwort ändern kann, wenn jemand ein offizielles Whitepaper vorlegt. Aber - neuerdings - das Kopfgeld gehört dir
Oleg Dok