Eine Bestellung wird gespeichert. Danach sollen Summen nachgezogen, Dokumente erzeugt oder Daten für ein anderes System aufbereitet werden. Der Benutzer soll darauf nicht warten, und kein Auftrag darf verloren gehen. Wenn genug davon anfällt, sollen mehrere Worker gleichzeitig daran arbeiten.
Genau dafür bringt SQL Server seit 2005 einen eigenen Nachrichtendienst mit: Service Broker. Nachrichten liegen als Daten in der Datenbank, das Senden gehört zur Transaktion des Senders und das Empfangen zur Transaktion des Empfängers. Dazu braucht es weder einen externen Messagingserver noch verteilte Transaktionen.
Im Lab haben wir auf SQL Server 2022 und 2025 nachgemessen, was davon im Alltag trägt. Mit denselben vier CPUs war ein Batch aus 32 unabhängigen Rechenaufgaben nach 4,6 statt 9,4 Sekunden fertig, sobald vier Worker ihn über eine Queue abarbeiteten. Mit derselben Queue lief ein anderer Aufbau trotz vier Workern streng nacheinander. Den Unterschied machen nicht die Worker, sondern die Dialoge.
Kurzfassung
- Service Broker parallelisiert über Conversation Groups. Vier Leser an einer Queue arbeiten nur dann gleichzeitig, wenn die Arbeit in mindestens vier Gruppen liegt.
- Die Garantie „genau einmal, in Reihenfolge“ gilt innerhalb eines Dialogs. Eine globale Reihenfolge über alle Aufträge gibt es nicht.
- Verlässlich wird es erst, wenn RECEIVE und die eigentliche Arbeit in derselben Transaktion committet werden. Eine leere Queue heißt nicht, dass die Arbeit erledigt ist.
- Der „asynchrone Trigger“ ist synchron. Er beauftragt die Arbeit nur. Erledigt wird sie nach dem Commit von einem anderen Worker.
Das Modell: Akten mit Schloss
Bevor wir messen, brauchen wir ein Bild, mit dem sich jedes Ergebnis vorhersagen lässt.
Stellen wir uns die Queue als Ablage vor, in der Akten liegen. Eine Akte ist eine Conversation Group. Darin stecken ein oder mehrere Dialoge mit ihren Nachrichten. Jeder Worker greift mit RECEIVE in die Ablage und nimmt die nächste Akte, deren Schloss offen ist. Er schließt sie ab, bis er seine Transaktion beendet. Wer danach greift, bekommt eine andere Akte. Ist keine offen, wartet er oder geht.
- Akte ADialog 1Nachr. 1–3Worker 1 hält das Schloss bis COMMIT
- Akte BDialog 2Nachr. 1Schloss: Worker 2
- Akte CDialog 3Nachr. 1Schloss: Worker 3
- Worker 1Akte A
- Worker 2Akte B
- Worker 3Akte C
- Worker 4wartet
Aus dem Bild folgen drei Vorhersagen:
- Wie viele Worker gleichzeitig arbeiten, hängt von der Zahl der offenen Akten ab, nicht von der Zahl der Worker.
- Innerhalb einer Akte geht es streng der Reihe nach. Daraus kommt die Reihenfolgegarantie, und daraus kommt auch die Serialisierung.
- Das Schloss hält bis COMMIT. Wer früher committet, gibt die Akte frei und verliert dabei auch den Schutz, den die Transaktion gegen Fehler bietet.
Das Bild hat eine Grenze: Ohne eigenes Zutun bekommt jeder neue Dialog auf der Empfängerseite seine eigene Gruppe, also seine eigene Akte. Wie man Dialoge absichtlich in eine Akte legt, zeigt Versuch 3.
Die Bausteine, die wir dafür anlegen:
| Baustein | Aufgabe |
|---|---|
| Message Type | Name einer Nachrichtenart und die Validierung des Inhalts, etwa WELL_FORMED_XML. Ersetzt keine fachliche Prüfung. |
| Contract | Welche Nachrichtentypen welche Seite eines Dialogs senden darf. |
| Queue | Hält Nachrichten bereit, bis jemand sie mit RECEIVE entnimmt. Führt selbst keinen Code aus. |
| Service | Adressierbarer Name einer Aufgabe; zeigt auf eine Queue. |
| Dialog (Conversation) | Verbindet zwei Services. Innerhalb eines Dialogs gilt „exactly once in order“. |
| Conversation Group | Die Akte: Einheit für Sperre und Zuteilung auf einer Dialogseite. |
| Activation | Startet bei Bedarf eine hinterlegte Prozedur als Worker. |
Das kleinste lauffähige Beispiel
Achtung: nur auf einer eigenen Testinstanz ausführen. Die Skripte legen eine Datenbank an, schalten Service Broker ein, arbeiten mit ENCRYPTION = OFF und EXECUTE AS OWNER und enthalten bewusst herbeigeführte Fehler, die eine Queue deaktivieren. Nicht auf einer vorhandenen oder produktiven Datenbank einsetzen. Für Anwendungen gehören Rechte, Verschlüsselung, Fehlerablage und Auftragsstatus eigens entworfen.
Die Grundausstattung aus unserem Lab, eine Datenbank, ein Nachrichtentyp, ein Contract und zwei Services mit ihren Queues:
/* PSG Lab · Service Broker · NUR TESTINSTANZ
Legt BrokerLab an und aktiviert Service Broker.
ENCRYPTION = OFF und EXECUTE AS OWNER nur für dieses isolierte Lab. */
CREATE DATABASE BrokerLab;
GO
ALTER DATABASE BrokerLab SET ENABLE_BROKER; -- braucht exklusiven Zugriff auf die Datenbank
GO
USE BrokerLab;
CREATE MESSAGE TYPE [//lab/work] VALIDATION = WELL_FORMED_XML;
CREATE CONTRACT [//lab/contract] ([//lab/work] SENT BY INITIATOR);
CREATE QUEUE dbo.SourceQ; -- Absenderseite: hier landen EndDialog-Antworten
CREATE QUEUE dbo.TargetQ; -- Empfängerseite: hier liegt die Arbeit
CREATE SERVICE [//lab/source] ON QUEUE dbo.SourceQ;
CREATE SERVICE [//lab/target] ON QUEUE dbo.TargetQ ([//lab/contract]);
-- Hier protokolliert der Worker jede erledigte Aufgabe
CREATE TABLE dbo.WorkLog (
run varchar(80) NOT NULL,
item int NOT NULL,
spid int NOT NULL,
grp uniqueidentifier NOT NULL,
seq bigint NOT NULL,
started datetime2(7) NOT NULL,
finished datetime2(7) NOT NULL,
PRIMARY KEY (run, item)
);
Der Absender öffnet einen Dialog und schickt zwölf Aufträge darüber. BEGIN DIALOG und SEND stehen in einer Transaktion: Rollt sie zurück, existiert kein einziger Auftrag.
USE BrokerLab;
SET QUOTED_IDENTIFIER ON; -- nötig für die XML-Methoden
DECLARE @h uniqueidentifier, @body xml, @i int = 1;
BEGIN TRANSACTION;
BEGIN DIALOG CONVERSATION @h
FROM SERVICE [//lab/source]
TO SERVICE N'//lab/target'
ON CONTRACT [//lab/contract]
WITH ENCRYPTION = OFF;
WHILE @i <= 12
BEGIN
SET @body = (SELECT 'one-dialog' AS [@run], @i AS [@id],
CONVERT(bit, CASE WHEN @i = 12 THEN 1 ELSE 0 END) AS [@close]
FOR XML PATH('w'), TYPE);
SEND ON CONVERSATION @h MESSAGE TYPE [//lab/work] (@body);
SET @i += 1;
END;
COMMIT;
Das Attribut close ist eine Vereinbarung nur für dieses Beispiel: Der Empfänger beendet den Dialog nach dem letzten Auftrag, der Absender räumt seine Seite auf, sobald die EndDialog-Nachricht bei ihm ankommt.
Der Worker ist eine gewöhnliche Prozedur. Wichtig ist allein, wo die Transaktion beginnt und endet:
CREATE PROCEDURE dbo.Work
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @h uniqueidentifier, @g uniqueidentifier, @b varbinary(max),
@t nvarchar(256), @s bigint, @x xml, @run varchar(80), @id int,
@close bit, @started datetime2(7), @delay varchar(12);
WHILE 1 = 1
BEGIN
BEGIN TRANSACTION; -- (1) Entnahme und Arbeit: eine Transaktion
SET @h = NULL;
WAITFOR (
RECEIVE TOP (1)
@h = conversation_handle,
@g = conversation_group_id,
@b = message_body,
@t = message_type_name,
@s = message_sequence_number
FROM dbo.TargetQ
), TIMEOUT 500; -- (2) 500 ms keine offene Akte: Worker endet
IF @h IS NULL BEGIN COMMIT; BREAK; END;
IF @t = N'//lab/work'
BEGIN
SET @x = CONVERT(xml, @b);
SET @run = @x.value('(/w/@run)[1]', 'varchar(80)');
SET @id = @x.value('(/w/@id)[1]', 'int');
SET @close = @x.value('(/w/@close)[1]', 'bit');
-- Künstliche Arbeit nur im Lab: Auftrag 1 dauert 600 ms, alle anderen 200 ms
SET @delay = CASE WHEN @id = 1 THEN '00:00:00.600' ELSE '00:00:00.200' END;
SET @started = SYSUTCDATETIME();
WAITFOR DELAY @delay;
INSERT dbo.WorkLog -- (3) der fachliche Effekt
VALUES (@run, @id, @@SPID, @g, @s, @started, SYSUTCDATETIME());
IF @close = 1 END CONVERSATION @h;
END
ELSE IF @t = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'
END CONVERSATION @h;
ELSE IF @t = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'
BEGIN
ROLLBACK; -- Fehlernachricht nicht still verschlucken
THROW 51000, 'Unexpected Broker error: inspect queue', 1;
END;
COMMIT; -- (4) Nachricht weg und Effekt da: gemeinsam
END;
END;
WAITFOR (RECEIVE …), TIMEOUT wartet auf eine Nachricht aus einer offenen Akte. Ohne WHERE-Klausel wählt RECEIVE nur Gruppen, die kein anderes RECEIVE gesperrt hat, und liefert ausschließlich Nachrichten dieser einen Gruppe. Eine gesperrte Akte hält also keinen anderen Worker auf (Microsoft: RECEIVE). Das künstliche WAITFOR DELAY steht für Arbeit; es macht im Versuch sichtbar, wer wann gleichzeitig beschäftigt ist, und misst keine Rechenleistung. Die Zeitstempel kommen aus der Instanz selbst, nicht vom Client.
Versuch 1: Zwölf Aufträge, ein Dialog, vier Worker
Erwartung nach dem Modell: Ein Dialog ist eine Akte. Vier Worker können sie nicht gleichzeitig halten. Es sollte also immer nur ein Auftrag laufen, in der Reihenfolge 1 bis 12.
Aufbau: Die zwölf Aufträge von oben, danach EXEC dbo.Work; in vier Sitzungen gleichzeitig.
Beobachtung: Das Protokoll zeigt es direkt:
SELECT item, spid, seq, started, finished
FROM dbo.WorkLog
WHERE run = 'one-dialog'
ORDER BY started;
| Auftrag | Session | Sequenz im Dialog | Start (ms) | Ende (ms) |
|---|---|---|---|---|
| 1 | 62 | 0 | 0 | 603 |
| 2 | 62 | 1 | 611 | 814 |
| 3 | 62 | 2 | 822 | 1.021 |
| … | 62 | … | … | … |
| 11 | 62 | 10 | 2.490 | 2.693 |
| 12 | 62 | 11 | 2.701 | 2.899 |
Auszug, SQL Server 2025 CU9, erste von drei Wiederholungen; Zeiten relativ zum ersten Start. Vollständig in den Rohdaten.
Kein Auftrag überlappt mit einem anderen. Alle zwölf liefen der Reihe nach und wurden in dieser Wiederholung sogar von derselben Session erledigt. Die drei übrigen Worker kamen nicht zum Zug, weil es nur eine Akte gab. Dasselbe Bild in allen drei Wiederholungen und auf beiden Versionen.
Folgerung: Mehr Worker helfen einem einzelnen Dialog nicht. Das ist kein Fehler, sondern der Preis der Reihenfolgegarantie. Wer alle Arbeit über einen langlebigen Dialog schickt, hat eine serielle Verarbeitung gebaut, egal wie viele Worker laufen.
Versuch 2: Zwölf Aufträge, zwölf Dialoge
Erwartung: Zwölf Dialoge ergeben auf der Empfängerseite zwölf Akten. Vier Worker sollten also vier Aufträge gleichzeitig bearbeiten.
Änderung: BEGIN DIALOG wandert in die Schleife, und jeder Auftrag trägt close = 1, damit der Empfänger jeden Dialog nach seinem einzigen Auftrag beendet. Den Dialog bitte nicht schon beim Absender direkt nach dem SEND schließen. Nach END CONVERSATION kann diese Seite auf dem Dialog nichts mehr empfangen, auch keine Fehlernachricht des Empfängers.
WHILE @i <= 12
BEGIN
BEGIN DIALOG CONVERSATION @h
FROM SERVICE [//lab/source]
TO SERVICE N'//lab/target'
ON CONTRACT [//lab/contract]
WITH ENCRYPTION = OFF;
SET @body = (SELECT 'many-dialogs' AS [@run], @i AS [@id],
CONVERT(bit, 1) AS [@close]
FOR XML PATH('w'), TYPE);
SEND ON CONVERSATION @h MESSAGE TYPE [//lab/work] (@body);
SET @i += 1;
END;
Beobachtung:
| Auftrag | Session | Start (ms) | Ende (ms) |
|---|---|---|---|
| 1 | 54 | 0 | 603 |
| 2 | 60 | 8 | 208 |
| 3 | 64 | 144 | 342 |
| 4 | 62 | 144 | 342 |
| 5 | 60 | 216 | 417 |
| … | … | … | … |
| 12 | 60 | 632 | 834 |
Auszug, SQL Server 2025 CU9, erste Wiederholung.
Versuch 1 · ein Dialoghöchstens 1 gleichzeitig · fertig nach 2,90 s
- Session 62
- 3 weitere Workerkein Auftrag erhalten
Versuch 2 · zwölf Dialogehöchstens 4 gleichzeitig · fertig nach 0,83 s
- Session 54
- Session 60
- Session 62
- Session 64
dbo.WorkLog, die Ziffer seine Auftrags-ID; orange ist der absichtlich langsame Auftrag 1. Beide Male vier gestartete Worker, SQL Server 2025 CU9, erste Wiederholung. Künstliche Arbeit per WAITFOR, keine Leistungsmessung.Vier Sessions arbeiten gleichzeitig. Statt nach 2,9 Sekunden waren alle zwölf Aufträge nach 0,83 Sekunden erledigt. Der absichtlich langsame Auftrag 1 wurde als siebter fertig. Die Abschlussreihenfolge lautete 2, 3, 4, 5, 6, 7, 1, 8, 9, 10, 11, 12.
Dieselbe Queue mit vier Produzenten, die gleichzeitig je drei Aufträge auf eigenen Dialogen schicken, ergab ebenfalls vier gleichzeitig laufende Aufträge. Viele Absender, ein Empfänger, das sogenannte Fan-in, heißt also nicht seriell. Es kommt allein darauf an, in wie vielen Akten die Arbeit liegt.
Folgerung: Unabhängige Aufträge gehören auf unabhängige Dialoge. Dann verteilt Service Broker sie von selbst auf freie Worker, auch bei unterschiedlich langen Aufträgen. Über Dialoge hinweg gibt es aber keine Reihenfolge, weder beim Start noch beim Abschluss.
Versuch 3: Akten gezielt zusammenlegen
Manchmal ist Serialisierung gewollt, etwa für alle Änderungen an derselben Bestellung. Dann sollen deren Dialoge in eine Akte.
Erwartung: Liegen zwölf Dialoge in einer Gruppe, verhalten sie sich wie Versuch 1: ein Auftrag nach dem anderen.
Änderung: Zwölf Dialoge wie in Versuch 2, aber vor dem Start der Worker legen wir sie auf der Empfängerseite mit MOVE CONVERSATION in eine vorhandene Gruppe:
DECLARE @group uniqueidentifier, @handle uniqueidentifier;
SELECT TOP (1) @group = conversation_group_id FROM dbo.TargetQ;
DECLARE dialogs CURSOR LOCAL FAST_FORWARD FOR
SELECT DISTINCT conversation_handle
FROM dbo.TargetQ
WHERE conversation_group_id <> @group;
OPEN dialogs;
FETCH NEXT FROM dialogs INTO @handle;
WHILE @@FETCH_STATUS = 0
BEGIN
MOVE CONVERSATION @handle TO @group;
FETCH NEXT FROM dialogs INTO @handle;
END;
CLOSE dialogs;
DEALLOCATE dialogs;
Die Zielgruppe muss existieren und zum selben Service gehören; eine frisch erzeugte GUID akzeptiert MOVE CONVERSATION nicht. Ein SELECT auf eine Queue liest übrigens nur und entnimmt nichts. So lässt sich vorher prüfen, wie viele Akten anliegen:
SELECT conversation_group_id, COUNT(*) AS messages
FROM dbo.TargetQ
GROUP BY conversation_group_id;
Beobachtung: Vorher zwölf Gruppen, danach eine. Vier Worker gestartet, eine Session erledigte alle zwölf Aufträge nacheinander. Keine Überlappung, in allen Wiederholungen beider Versionen.
Gegenprobe: Viele lesen in der Doku von RELATED_CONVERSATION_GROUP und legen damit die Dialoge beim Absender in eine Gruppe:
DECLARE @g uniqueidentifier = NEWID();
-- in der Schleife:
BEGIN DIALOG CONVERSATION @h
FROM SERVICE [//lab/source]
TO SERVICE N'//lab/target'
ON CONTRACT [//lab/contract]
WITH RELATED_CONVERSATION_GROUP = @g, ENCRYPTION = OFF;
Ergebnis: Beim Empfänger lagen trotzdem zwölf Gruppen an, und vier Worker arbeiteten gleichzeitig. Conversation Groups gelten nur auf ihrer eigenen Dialogseite. Die Sendergruppe hilft etwa, Antworten beim Absender zusammenzuführen. Für die Empfängerseite überträgt sie nichts.
Folgerung: Die Akte wird dort gebildet, wo gelesen wird. Eine Anwendung legt die Zuordnung fachlich fest, bevor konkurrierende Worker zugreifen, zum Beispiel einen Dialog je Bestellung. MOVE CONVERSATION vor dem Start der Worker ist eine Laborabkürzung, kein Anwendungsmuster. Auch eine gemeinsame Gruppe verschiedener Dialoge sichert nur den gegenseitigen Ausschluss, keine fachliche Gesamtreihenfolge.
Alle Varianten im Überblick, jeweils vier gestartete Worker außer in der ersten Zeile:
| Aufbau | Akten beim Empfänger | Gleichzeitig laufende Aufträge |
|---|---|---|
| Zwölf Dialoge, ein Worker | 12 | 1 |
| Ein Dialog mit zwölf Nachrichten | 1 | 1 |
| Zwölf unabhängige Dialoge | 12 | 4 |
| Zwölf Dialoge, beim Empfänger zusammengelegt | 1 | 1 |
| Zwölf Dialoge, nur beim Absender gruppiert | 12 | 4 |
| Vier Produzenten mit je drei Dialogen | 12 | 4 |
SQL Server 2022 CU27 und 2025 CU9, je drei Wiederholungen: 36 Läufe, 432 bestätigte Aufträge. In jedem Lauf genau zwölf Ergebnisse, keine Duplikate, Queue danach leer; in jeder Wiederholung derselbe Wert.
Praxis: Dieselben CPUs besser auslasten
Die bisherigen Versuche zeigen Koordination mit künstlicher Wartezeit. Die Frage aus der Praxis lautet anders: Ein Batch aus vielen gleichartigen, voneinander unabhängigen Aufgaben läuft seriell und beschäftigt dabei etwa einen Kern, während die anderen frei sind. Wird er mit mehreren Workern früher fertig?
Dafür haben wir echte CPU-Arbeit verwendet. Jede Aufgabe berechnet doppelte SHA2-256-Hashes über 8.192 Zeilen, bezogen auf ihre eigene Auftrags-ID. Das sind rund 250 ms CPU, mit MAXDOP 1, damit nur die Parallelität zwischen den Aufgaben wirkt:
CREATE PROCEDURE dbo.TaskBody @task int, @passes int, @result bigint OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @i int = 0, @v bigint;
SET @result = 0;
WHILE @i < @passes
BEGIN
SELECT @v = SUM(CONVERT(bigint, CHECKSUM(HASHBYTES('SHA2_256', HASHBYTES('SHA2_256',
CONCAT(payload, CONVERT(varchar(12), @task), ':', CONVERT(varchar(12), @i)))))))
FROM dbo.Seed
OPTION (MAXDOP 1);
SET @result += @v;
SET @i += 1;
END;
END;
Jeder der 32 Aufträge liegt auf einem eigenen Dialog. Der Kern des Workers ist derselbe wie oben: Entnehmen, rechnen, Ergebnis schreiben, Dialog beenden, alles in einer Transaktion.
-- Auszug aus dbo.BatchWorker (Queue-Zweig der Schleife);
-- Startbarriere, CPU-Messung und direkter Vergleichszweig weggelassen
BEGIN TRAN;
SET @h = NULL;
WAITFOR (RECEIVE TOP (1) @h = conversation_handle, @type = message_type_name,
@body = message_body
FROM dbo.TargetQ), TIMEOUT 100;
IF @h IS NULL BEGIN COMMIT; BREAK; END;
IF @type <> N'//lab/work'
BEGIN ROLLBACK; THROW 51100, 'Unexpected message type in task queue', 1; END;
SET @x = CONVERT(xml, @body);
SET @task = @x.value('(/w/@id)[1]', 'int');
SET @start = SYSUTCDATETIME();
EXEC dbo.TaskBody @task, @passes, @result OUTPUT;
SET @finished = SYSUTCDATETIME();
INSERT dbo.BatchResult VALUES (@run, @task, @result, @start, @finished, @@SPID);
END CONVERSATION @h;
COMMIT;
Beobachtung, Median aus drei Läufen, in allen Varianten dieselben vier zugewiesenen CPUs:
32 unabhängige Rechenaufgaben · Median aus drei Läufen · kürzerer Balken = früher fertig
Mehr Worker nutzen freie CPU, die Rechenarbeit insgesamt steigt. Ein gemeinsamer Schreibpunkt reiht die Arbeit wieder auf. SQL Server 2022 CU27, MAXDOP 1 je Abfrage. Methode und Spannweiten ↓
| Ausführung | Batch fertig nach | Container-CPU | CPU-Zeit der Worker gesamt |
|---|---|---|---|
| Direkt nacheinander, ohne Queue | 9,38 s | 30 % | 8,97 CPU-s |
| Queue, 1 Worker | 9,71 s | 30 % | 9,09 CPU-s |
| Queue, 2 Worker | 9,66 s | 31 % | 9,35 CPU-s |
| Queue, 4 Worker | 4,60 s | 73 % | 12,11 CPU-s |
| Queue, 8 Worker | 4,14 s | 81 % | 13,27 CPU-s |
SQL Server 2022 CU27, 32 Aufgaben je Batch, Ergebnisse in jedem Lauf identisch mit der direkten Referenz. Spannweiten in der Methode.
Vier Worker halbierten die Batchdauer mit denselben vier CPUs. Die freie Kapazität wurde genutzt. Drei Dinge gehören direkt dazu:
- Die einzelne Aufgabe wird nicht schneller. Ihre Bearbeitungszeit stieg von 273 ms direkt auf 821 ms bei acht Workern. Der Batch ist früher fertig, weil Aufgaben überlappen.
- Die Rechenarbeit insgesamt steigt. Vier Worker brauchten 35 % mehr CPU-Sekunden, acht 48 % mehr. Kürzer heißt hier nicht billiger.
- Die Queue selbst beschleunigt nichts. Ein Worker mit Queue war etwas langsamer als der direkte Aufruf. Der Gewinn kommt allein aus der Parallelität.
Auffällig sind die zwei Worker: kaum Gewinn. In zwei von drei Läufen lagen beide auf demselben SQL-Scheduler, einer rechnete, der andere wartete auf CPU, während andere Scheduler frei waren. Der dritte Lauf verteilte sich besser und war nach 6,0 s fertig. Freie CPU im Durchschnitt garantiert nicht, dass jeder Worker gerade einen freien Scheduler erwischt.
Gegenprobe mit gemeinsamem Engpass: Dieselben Aufgaben, aber jede erhöht vor der Berechnung einen gemeinsamen Zähler, in derselben Transaktion:
UPDATE dbo.HotCounter SET n = n + 1 WHERE id = 1; -- X-Lock bis COMMIT
EXEC dbo.TaskBody @task, @passes, @result OUTPUT;
Jetzt dauerte der Batch mit einem, vier oder acht Workern gleichermaßen etwa 9,5 Sekunden. Die zusätzlichen Worker standen in LCK_M_X und warteten auf die Zählerzeile. Logisch unabhängige Aufgaben brauchen eine Implementierung, die diese Unabhängigkeit auch zulässt.
Folgerung für die Praxis: Mehrere Worker lohnen sich, wenn genug unabhängige Aufgaben anliegen, CPU tatsächlich frei ist und kein gemeinsamer Engpass die Arbeit wieder aufreiht. Die beste Workerzahl ist eine Messfrage. Vergleichen Sie Batchdauer, vollständige Ergebnisse, CPU je erledigter Aufgabe und Wartezustände. Im Mischbetrieb gehören die Antwortzeiten der übrigen Anwendungen dazu. Freie CPU kann auch die Reserve für Lastspitzen sein. Und wenn eine einzelne Abfrage bereits parallel läuft, teilen sich Abfrage- und Aufgabenparallelität dieselben Kerne.
Aktivierung: SQL Server startet die Worker selbst
Statt Worker von außen zu starten, kann die Queue eine Prozedur selbst aktivieren:
ALTER QUEUE dbo.TargetQ
WITH ACTIVATION (
STATUS = ON,
PROCEDURE_NAME = dbo.Work,
MAX_QUEUE_READERS = 4,
EXECUTE AS OWNER -- nur im Lab; für Anwendungen Identität gezielt wählen
);
Mit 24 unabhängigen Dialogen erreichten beide Versionen zwei gleichzeitig arbeitende Worker, obwohl vier erlaubt waren. MAX_QUEUE_READERS ist eine Obergrenze für aktivierte Instanzen. Der Queue-Monitor prüft alle paar Sekunden und bei Ereignissen wie neuen Nachrichten, ob die laufenden Leser nicht hinterherkommen, und startet erst dann eine weitere Instanz. Das passt zum Modell: Liegt alle Arbeit in einem einzigen Dialog, startet er keinen weiteren Leser, sobald ein zweiter wartet. Leser, die eine Anwendung selbst startet, sind von dieser Grenze unabhängig.
Zwei Regeln für die Prozedur: Sie muss sich beenden, wenn nichts mehr anliegt. In dbo.Work erledigt das WAITFOR (RECEIVE …), TIMEOUT 500. Und STATUS = OFF verhindert nur neue Aktivierungen; bereits laufende Instanzen arbeiten weiter, bis sie selbst enden. Im Lab haben wir deshalb nach dem Abschalten gewartet, bis sys.dm_broker_activated_tasks leer war.
Der „asynchrone Trigger“: synchron beauftragen, später erledigen
Ein Trigger läuft immer in der Transaktion des auslösenden Statements. Daran ändert Service Broker nichts. Das Muster besteht darin, dass der Trigger die teure Arbeit nicht selbst erledigt, sondern nur beauftragt:
CREATE TRIGGER dbo.OrdersAsync ON dbo.Orders AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Die ganze inserted-Menge, nicht nur eine Zeile
DECLARE @h uniqueidentifier,
@body xml = (SELECT id, note FROM inserted
FOR XML PATH('order'), ROOT('orders'));
BEGIN DIALOG CONVERSATION @h
FROM SERVICE [//lab/source]
TO SERVICE N'//lab/trigger'
ON CONTRACT [//lab/contract]
WITH ENCRYPTION = OFF;
SEND ON CONVERSATION @h MESSAGE TYPE [//lab/work] (@body);
END;
Ein Trigger feuert einmal pro Statement, nicht pro Zeile. Ein INSERT kann drei oder dreißigtausend Zeilen bringen. Deshalb packt der Trigger die gesamte inserted-Menge in eine Nachricht. Wer SELECT @id = id FROM inserted schreibt, verliert alle Zeilen bis auf eine, auch bei perfekter Zustellung. Bei großen Mengen gehören statt der Zeilen eher Schlüssel oder ein Batchverweis in die Nachricht.
Der Worker auf der Gegenseite:
CREATE PROCEDURE dbo.TriggerWork
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @h uniqueidentifier, @b varbinary(max), @t nvarchar(256), @x xml;
BEGIN TRAN;
WAITFOR (RECEIVE TOP (1) @h = conversation_handle, @b = message_body,
@t = message_type_name
FROM dbo.TriggerQ), TIMEOUT 500;
IF @h IS NULL BEGIN COMMIT; RETURN; END;
IF @t = N'//lab/work'
BEGIN
SET @x = CONVERT(xml, @b);
WAITFOR DELAY '00:00:01'; -- künstliche teure Folgearbeit (Lab)
INSERT dbo.OrderEffects
SELECT n.value('(id/text())[1]', 'int'), SYSUTCDATETIME()
FROM @x.nodes('/orders/order') AS a(n);
END CONVERSATION @h;
END
ELSE IF @t = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'
END CONVERSATION @h;
ELSE
BEGIN
ROLLBACK;
THROW 51002, 'Unexpected TriggerQ message', 1;
END;
COMMIT;
END;
Was wir damit geprüft haben:
- 01 · URSPRUNGINSERT der Bestellung
- 02 · NOCH SYNCHRONTrigger: BEGIN DIALOG und SEND
- 03 · COMMITBestellung und Auftrag gemeinsam bestätigt
- 04 · ANWENDUNGBenutzer kann weiterarbeiten
- 05 · NACH COMMITWorker empfängt den Auftrag
- 06 · EIGENE TRANSAKTIONFolgearbeit, END CONVERSATION, COMMIT
| Probe | Beobachtung |
|---|---|
INSERT, dann ROLLBACK |
Keine Bestellung, keine Nachricht. Beauftragung und Daten verschwinden gemeinsam. |
INSERT offen, zweite Sitzung ruft den Worker |
Kein Empfang. Erst nach dem Commit war der Auftrag sichtbar und wurde verarbeitet. |
Drei Zeilen in einem INSERT |
Eine Nachricht, danach drei Folgeeffekte. |
| Synchroner Vergleichstrigger mit derselben 1-s-Arbeit | INSERT dauerte 1.011 ms. Mit Brokertrigger 17 ms (2022) beziehungsweise 8 ms (2025); die Sekunde fiel später beim Worker an. Je Version eine Probe. |
| Nachrichtentyp passt nicht zum Contract | SEND scheitert, und mit ihm das INSERT. Die Bestellung wurde nicht gespeichert. |
| Empfänger-Queue deaktiviert | Die Bestellung wurde gespeichert, der Auftrag nach Reaktivierung verarbeitet. |
Die Fehlermeldung des gescheiterten SEND, wörtlich aus dem Lab (nur der Containername ist gekürzt):
Msg 8431, Level 16, State 1, Server …, Procedure OrdersAsync, Line 1
The message type '//lab/invalid' is not part of the service contract.
The statement has been terminated.
Folgerung: Die Ursprungstransaktion bezahlt weiterhin Trigger, Nachrichtenaufbau und SEND. Ein Fehler darin lässt das ursprüngliche Statement scheitern. Dafür bekommt sie einen wertvollen Vertrag: Wird die Bestellung committet, bleibt der Auftrag erhalten, bis ihn ein Worker erfolgreich verarbeitet hat oder er ausdrücklich als Fehler behandelt wird. Die Arbeit selbst wird nicht billiger. Sie läuft später und konkurriert dann um dieselben Ressourcen. Für die Oberfläche heißt das: „Auftrag angenommen“ und „Dokument fertig“ sind zwei verschiedene Zustände. Wer nach dem SEND „fertig“ meldet, hat einen Anwendungsfehler gebaut.
Microsoft: typische Anwendungsfälle, Trigger mit mehreren Zeilen
Wenn etwas schiefgeht
Der eigentliche Test ist nicht der glückliche Durchlauf, sondern die Unterbrechung zwischen Entnahme und Commit.
Rollback: Die Nachricht kommt zurück
BEGIN TRANSACTION;
DECLARE @handle uniqueidentifier;
RECEIVE TOP (1) @handle = conversation_handle FROM dbo.TargetQ;
INSERT dbo.ProbeEffects (id) VALUES (1);
ROLLBACK;
SELECT COUNT(*) AS messages FROM dbo.TargetQ; -- 1: wieder da
SELECT COUNT(*) AS effects FROM dbo.ProbeEffects; -- 0: nichts übrig
Die Nachricht lag wieder in der Queue, der halbe Effekt war verschwunden. Der nächste Aufruf von dbo.Work erledigte den Auftrag genau einmal. Ein Worker darf seinen Code also wiederholt durchlaufen. Zählt, was gemeinsam committet wird.
Harter Abbruch der Instanz
Wir haben eine Nachricht empfangen, den Effekt in der offenen Transaktion geschrieben und dann die eigene Testinstanz hart beendet (SIGKILL auf den Container). Nach dem Neustart und der Recovery: Nachricht wieder empfangbar, Effekt zurückgerollt, erneute Verarbeitung genau einmal. Das ist die gewöhnliche Transaktionsrecovery, angewandt auf die Queue.
Die Abkürzung, die Arbeit verliert
-- ANTIMUSTER, nur zur Demonstration
DECLARE @h uniqueidentifier;
BEGIN TRAN;
RECEIVE TOP (1) @h = conversation_handle FROM dbo.TargetQ;
END CONVERSATION @h;
COMMIT; -- Nachricht ist weg ...
-- ... und die eigentliche Arbeit kommt nie
Ergebnis: Queue leer, Arbeit fehlt. Dasselbe passiert, wenn ein Worker sofort nach RECEIVE committet und erst danach arbeitet. Service Broker kann einen bereits bestätigten Anwendungsfehler nicht reparieren. Eine leere Queue bedeutet nicht, dass die Arbeit erledigt ist.
Die Nachricht, an der jeder scheitert
Ein Auftrag ist syntaktisch gültig, lässt sich fachlich aber nicht verarbeiten. Der Worker scheitert, rollt zurück und bekommt dieselbe Nachricht erneut. Service Broker schützt standardmäßig davor, dass sich das endlos wiederholt:
-- NUR TESTINSTANZ: deaktiviert die Queue absichtlich
BEGIN TRAN;
DECLARE @h uniqueidentifier;
RECEIVE TOP (1) @h = conversation_handle FROM dbo.TargetQ;
ROLLBACK;
-- fünfmal hintereinander, nach jedem Versuch:
SELECT is_receive_enabled FROM sys.service_queues WHERE name = 'TargetQ';
| Rollback | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
is_receive_enabled |
1 | 1 | 1 | 1 | 0 |
Identisch auf SQL Server 2022 und 2025.
Nach dem fünften aufeinanderfolgenden Rollback ist die Queue für RECEIVE gesperrt. Damit steht auch jeder gesunde Auftrag dahinter. Ein reflexhaftes ALTER QUEUE dbo.TargetQ WITH STATUS = ON beseitigt die Ursache nicht. Ein belastbarer Worker fängt fachliche Fehler selbst ab, begrenzt die Wiederholungen, legt den Auftrag mit Fehlerdaten in einer eigenen Tabelle ab und committet die Entnahme. Oder er beendet den Dialog mit einer ausdrücklichen Fehlerantwort an den Absender. Diese eigene Fehlerablage haben wir im Lab nicht gebaut.
Zwei Werkzeuge nennt die Doku dafür, beide im Lab nicht geprüft: Mit POISON_MESSAGE_HANDLING (STATUS = OFF) an CREATE QUEUE oder ALTER QUEUE bleibt die Queue nach fünf Rollbacks offen; dann muss die eigene Fehlerbehandlung Endlosschleifen sicher verhindern. Und über eine Event Notification FOR BROKER_QUEUE_DISABLED lässt sich das Abschalten melden, statt es erst am Rückstau zu bemerken.
Microsoft: Poison Messages, CREATE QUEUE: POISON_MESSAGE_HANDLING, CREATE EVENT NOTIFICATION, transaktionale Nachrichten
Dialoge ordentlich beenden
EndDialog- und Error-Nachrichten gehören in jeden Empfangscode, auf beiden Seiten; unser Absender räumt seine Queue mit einer kleinen Prozedur dbo.DrainSource auf. Ein END CONVERSATION direkt hinter jedem SEND ersetzt kein Abschlussprotokoll. Service Broker beendet einen Dialog nie von selbst; beide Seiten müssen END CONVERSATION ausführen. END CONVERSATION … WITH CLEANUP ist laut Doku ein Werkzeug für Administratoren, wenn ein Dialog nicht mehr normal enden kann, und gehört ausdrücklich nicht in Anwendungscode. Nach unseren Läufen standen in sys.conversation_endpoints keine offenen Dialoge mehr, nur Endpunkte im Zustand CLOSED. Viele alte offene Dialoge sind dagegen ein Befund.
Arbeit verteilen oder mehrere Empfänger informieren?
„Fan-out“ meint zwei verschiedene Dinge.
Arbeit verteilen: Zwölf Aufträge, vier Worker, jeder Auftrag soll genau einmal erledigt werden. Das sind konkurrierende Leser an einer Queue, wie in Versuch 2.
Mehrere Empfänger informieren: Eine Bestellung interessiert Lager und Buchhaltung, beide sollen eine eigene Nachricht erhalten. Vier Leser derselben Queue leisten das nicht, sie teilen sich die Nachrichten. Jeder Empfänger braucht einen eigenen Service mit eigener Queue und seinen eigenen Dialog:
-- Zwei ausdrücklich adressierte Zustellungen
BEGIN DIALOG CONVERSATION @h1 FROM SERVICE [//lab/source]
TO SERVICE N'//lab/target' ON CONTRACT [//lab/contract] WITH ENCRYPTION = OFF;
BEGIN DIALOG CONVERSATION @h2 FROM SERVICE [//lab/source]
TO SERVICE N'//lab/subscriber' ON CONTRACT [//lab/contract] WITH ENCRYPTION = OFF;
SEND ON CONVERSATION @h1 MESSAGE TYPE [//lab/work] (@body);
SEND ON CONVERSATION @h2 MESSAGE TYPE [//lab/work] (@body);
Im Lab lag danach je eine Nachricht in TargetQ und in SubscriberQ. Seit SQL Server 2012 kann SEND auch mehrere Handles in einem Statement bedienen, SEND ON CONVERSATION (@h1, @h2) …; laut Doku ist es dann atomar. Diese Form haben wir nicht getestet. Microsoft: SEND
Im Betrieb: Wo hängt es?
Vier Ebenen trennen die meisten Fragen sauber:
- Beauftragung: Ist Broker in der Datenbank aktiv? Wird gesendet und committet?
- Zustellung: Hängen Nachrichten in
sys.transmission_queue, und mit welchem Status? - Verarbeitung: Ist die Queue für
RECEIVEoffen? Läuft die Aktivierung? Wie viele Akten liegen an? - Fachlicher Abschluss: Wie alt ist der älteste offene Auftrag? Was ist fehlgeschlagen? Das beantwortet nur ein eigener Auftragsstatus der Anwendung.
-- 1. Broker in dieser Datenbank aktiv?
SELECT name, is_broker_enabled, service_broker_guid
FROM sys.databases
WHERE database_id = DB_ID();
-- 2. Hängende Zustellungen
SELECT to_service_name, transmission_status, COUNT(*) AS messages
FROM sys.transmission_queue
GROUP BY to_service_name, transmission_status;
-- 3a. Queue-Zustand und Aktivierung
SELECT name, is_receive_enabled, is_enqueue_enabled,
is_activation_enabled, max_readers, activation_procedure
FROM sys.service_queues
WHERE is_ms_shipped = 0;
-- 3b. Wie viele Akten liegen an? (liest nur, entnimmt nichts)
SELECT COUNT(DISTINCT conversation_group_id) AS groups, COUNT(*) AS messages
FROM dbo.TargetQ;
-- 3c. Laufende aktivierte Worker
SELECT * FROM sys.dm_broker_activated_tasks WHERE database_id = DB_ID();
-- Dialoge nach Zustand: viele offene alte Dialoge sind ein Befund
SELECT state_desc, COUNT(*) AS endpoints
FROM sys.conversation_endpoints
GROUP BY state_desc;
Diese Abfragen ersetzen keinen Auftragsstatus. Sie trennen aber „es wurde nichts beauftragt“, „die Zustellung hängt“ und „der Worker arbeitet nicht“ voneinander. Für die DMVs sind passende Rechte nötig. Microsoft: Zustellprobleme untersuchen
Und in der Cloud?
Service Broker ist eine Fähigkeit der SQL-Server-Engine, die nicht jeder verwaltete Dienst anbietet. Das gehört vor einer Migration auf die Prüfliste. Dokumentationsstand 27.10.2026:
| Zielplattform | Service Broker |
|---|---|
| SQL Server auf einer Azure-VM | Verfügbar wie in der gewählten Version und Edition. |
| Azure SQL Managed Instance | Unterstützt, mit Einschränkungen: instanzübergreifend nur zwischen Managed Instances, Transport- statt Dialogsicherheit. Ist Broker in der Quelldatenbank deaktiviert, lässt er sich nach der Migration nicht per ENABLE_BROKER einschalten. |
| Azure SQL Database | Nicht unterstützt. |
| AWS RDS, Google Cloud SQL for SQL Server | Service-Broker-Endpoints laut Anbieter nicht unterstützt. Welche lokalen Brokerfunktionen bleiben, im Einzelfall prüfen. |
Featurevergleich SQL Database/Managed Instance, Brokerunterschiede Managed Instance, Migrationshinweis, SQL Server auf Azure-VMs, AWS, Google
Wo Broker fehlt, muss dieselbe Zusage anders gebaut werden. Drei Wege passen zum Szenario unabhängiger Aufträge:
| Weg | Passt, wenn … | Selbst zu lösen |
|---|---|---|
| Jobtabelle in der Datenbank plus Worker | die Arbeit SQL-Arbeit bleibt und Beauftragung und Datenänderung in einer Transaktion liegen sollen. | Aufträge atomar beanspruchen, abgelaufene Reservierungen, Wiederanlauf, Fehlerablage. Ein SELECT, dann UPDATE ohne sicheren Claim genügt nicht. |
| Transactional Outbox plus Azure Service Bus | Aufträge an externe Worker oder andere Anwendungen gehen. | Weiterleitung der Outbox, doppelte Zustellung, wiederholungssichere Verarbeitung; Reihenfolge je Schlüssel bei Bedarf über Sessions. |
| Azure Storage Queues plus Worker | eine einfache externe Aufgabenliste genügt. | Mehrfache Zustellung, keine FIFO-Zusage; die atomare Beauftragung aus SQL heraus ebenfalls über eine Outbox. |
Der wichtigste Unterschied: Azure Service Bus nimmt nicht an der SQL-Transaktion teil. Ein committetes Update und ein danach gescheiterter Sendeaufruf hinterlassen eine Änderung ohne Auftrag, umgekehrt ist es genauso. Das Outbox-Muster schreibt deshalb Datenänderung und Auftrag gemeinsam in die Datenbank. Ein separater Prozess leitet ihn nach dem Commit weiter. Er kann dabei doppelt senden, der Empfänger muss Wiederholungen also an einer Auftrags-ID erkennen. Und wenn externe Worker dieselben Prozeduren aufrufen, rechnet weiterhin die Datenbank-CPU. Das sind Architekturvorschläge, im Lab nicht implementiert.
Service Bus: Transaktionsumfang, Outbox-Prinzip, Empfangsmodi, Duplikate und Idempotenz, Sessions, Storage Queues und Service Bus im Vergleich, Wiederholung und Idempotenz
Was bleibt
Service Broker lohnt sich, wenn die Verbindlichkeit einer SQL-Transaktion auch für die Beauftragung von Hintergrundarbeit gelten soll. Wie sich die Arbeit verhält, entscheidet das Design:
- Ein Dialog je unabhängigem Auftrag verteilt die Arbeit auf freie Worker.
- Eine Akte je Geschäftsvorgang serialisiert, was zusammengehört, und lässt Verschiedenes parallel laufen.
- RECEIVE, Arbeit und Commit in einer Transaktion machen aus der Zustellgarantie eine Verarbeitungsgarantie, solange die Effekte in der Datenbank bleiben. Für E-Mails oder HTTP-Aufrufe braucht das Ziel zusätzlich eine Auftrags-ID, an der es Wiederholungen erkennt.
- Mehr Worker helfen genau so weit, wie freie CPU und unabhängige Ressourcen reichen. Wie weit das ist, zeigt eine Messung.
Methode und Grenzen
Aufbau: Eigene Container auf dem PSG-Labhost, nacheinander; SQL Server 2022 CU27 (16.0.4295.3, Developer, Ubuntu 22.04) und SQL Server 2025 CU9 (17.0.5005.3, Enterprise Developer, Ubuntu 24.04). Vier zugewiesene vCPUs, 5 GiB Container-RAM, 3.072 MB max server memory, kein Netzwerk nach außen. Gemeinsamer Host mit anderen Diensten, keine exklusive Hardwaremessung und kein Versionsvergleich.
Koordinationsversuche (1 bis 3, Aktivierung, Trigger, Fehlerfälle): Je Version sechs Varianten mit zwölf Aufträgen und drei Wiederholungen in wechselnder Reihenfolge nach eigenem Warm-up, zusammen 36 Läufe und 432 bestätigte Aufträge; Aktivierung zusätzlich mit 24 Aufträgen je Version. Die Arbeit ist ein künstliches WAITFOR von 200 ms, für Auftrag 1 von 600 ms. Gleichzeitigkeit wird aus den Start- und Endzeitstempeln der Instanz bestimmt, nicht aus der Zahl offener Sitzungen. Der Triggerzeitvergleich ist je Version eine einzelne Probe und zeigt die Verlagerung, keinen Beschleunigungsfaktor. 158 Laufprüfungen und 494 unabhängige Auswertungsprüfungen bestanden.
CPU-Batch: Nur SQL Server 2022 CU27. 32 Aufgaben je Batch, etwa 250 ms CPU je Aufgabe, serieller Plan (MaxDOPSetToOne), warmer Cache. Direkter Aufruf und 1, 2, 4, 8 Worker, je mit und ohne gemeinsamen Zähler, drei Wiederholungen: 30 Läufe, 960 bestätigte Aufgaben. Batchdauer vom Öffnen einer gemeinsamen Startbarriere bis zum Ende aller Worker samt Commit. Container-CPU aus der cgroup, einschließlich Hintergrund- und Messarbeit; die CPU-Zeit der Worker separat aus den Requestzählern. Spannweiten der Batchdauer: direkt 9,17–9,42 s, ein Worker 9,42–9,91 s, zwei 6,04–9,67 s, vier 4,48–6,16 s, acht 3,86–4,95 s. Die Medianwerte verschiedener Spalten stammen nicht zwingend aus derselben Wiederholung. 153 Laufprüfungen und 289 Auswertungsprüfungen bestanden.
Nicht geprüft: Remote-Dialoge, Routen, Zertifikate und Verschlüsselung, Verfügbarkeitsgruppen, Windows-spezifisches Verhalten, produktive Payloadgrößen und lange Queues, Mischlast mit anderen Anwendungen, eigene Retry- und Fehlerablage, externe Effekte wie Mail oder HTTP, die Multicast-Form von SEND sowie alle Cloudalternativen. Der Lab-Worker ist ein kleiner, prüfbarer Consumer, kein Betriebsframework.
Die gezeigten Skripte entsprechen dem Labaufbau, sind für die Lesbarkeit formatiert und, wo im Kommentar vermerkt, gekürzt. Die Ergebnisse stammen aus dem automatisierten Lauf mit denselben Objekten. Rohintervalle, Session-IDs, Sequenznummern und Prüfprotokolle liegen zu jedem Lauf vor.
Quellen: Microsoft: Was Service Broker leistet · Conversation Groups · Gruppensperren · RECEIVE · Wann Aktivierung erfolgt · Transaktionale Nachrichten · Poison Messages · END CONVERSATION. Weitere Belege stehen direkt bei den Aussagen; Herstellerdokumentation geprüft am 27.10.2026.
Prüfen Sie Ihr Modell
1. Vier Worker laufen, aber WorkLog zeigt nur eine Session, die alles nacheinander erledigt. Woran liegt das?
Auflösung: Die Arbeit liegt in einer einzigen Conversation Group, etwa weil alles über einen langlebigen Dialog gesendet wird oder die Dialoge beim Empfänger zusammengelegt wurden. Die Zahl der Akten begrenzt die Parallelität, nicht die Zahl der Worker. Prüfen mit SELECT COUNT(DISTINCT conversation_group_id) FROM dbo.TargetQ.
2. Die Queue ist leer, die Folgetabelle aber unvollständig. Was prüfen Sie zuerst?
Auflösung: Ob der Worker RECEIVE und die Arbeit in getrennten Transaktionen committet oder einen Dialog ohne Arbeit beendet. Eine leere Queue heißt nur „entnommen“, nicht „erledigt“. Zusätzlich die Transaktionsgrenzen im Fehlerpfad und einen eigenen Auftragsstatus prüfen.
3. Von vier auf acht Worker wird der Batch kaum schneller. Ist acht falsch?
Auflösung: Nicht zwingend. Typische Gründe sind ein gemeinsamer Engpass wie eine heiße Zeile oder das Log, bereits ausgelastete CPU, ungünstige Schedulerverteilung oder Abfragen, die selbst schon parallel laufen. Batchdauer, CPU je Aufgabe und Wartezustände vergleichen; die Workerzahl ist eine Messfrage.
4. Seit Einführung des „asynchronen Triggers“ schlagen manche Bestellungen fehl, obwohl der Worker gar nicht läuft. Wie kann das sein?
Auflösung: BEGIN DIALOG und SEND laufen in der Transaktion des INSERT. Scheitert das Senden, etwa an einem Nachrichtentyp außerhalb des Contracts (Fehler 8431), scheitert die Bestellung mit. Ein gestoppter oder deaktivierter Worker dagegen lässt die Bestellung durch, der Auftrag wartet.
Stand: 27.10.2026. Getestet mit SQL Server 2022 CU27 und SQL Server 2025 CU9.
Hintergrundarbeit, die verlässlich und schnell erledigt wird
Wir sehen uns Ihren Ablauf an: wo Folgearbeit die Transaktion bremst, wie Dialoge und Worker zugeschnitten sind und wo die Arbeit tatsächlich wartet. Vorschläge prüfen wir mit Messungen statt nach Faustregel.