Transaktionale Replikationen können eine wahre Herausforderung sein, wenn es darum geht, die Daten effizient zu den Subscribern zu übertragen. Letzte Woche ergab es sich, dass ein Kunde in einer Datenbank sehr große Datenvolumen änderte. Die Datenbank ist der Publisher in einer Transaktionalen Replikation und die Verarbeitung der geänderten Daten beim Subscriber wurde nur verzögert abgearbeitet. Da der Subscriber ebenfalls Daten aus anderen Publishern verarbeiten musste, konnte eine hohe Schreib- und Leselast auf dem Server festgestellt werden. Es sollte eine Lösungsmöglichkeit gefunden werden, um die hohe Verarbeitungslast zu reduzieren.

Hinweis

Dieser Artikel behandelt nicht die Konzepte einer Transaktionalen Replikation sondern fokussiert ausschließlich auf die Problemstellung, die sich in einer solchen Topologie ergeben kann. Detaillierte Informationen zu den Grundlagen der Replikation mit Microsoft SQL Server und der Transaktionalen Replikation im Besonderen finden sich hier:

Testumgebung / Transaktionale Replikation

Um das Verhalten für diesen Blogartikel zu demonstrieren, wurde die nachfolgende Testumgebung erstellt

Transaktionale Replikation - Versuchsaufbau

Als Publisher wurden zwei Microsoft SQL Server 2019 (CU8) eingerichtet (CL10 und CL11). Ein weiterer Microsoft SQL Server 2019 (CU8) wurde als Distributor eingerichtet (CL12). Der Distributor wird gleichwohl als Subscriber für beide Publikationen verwendet! Die Testdatenbank – CustomerOrders – wurde auf CL10 und CL11 als Publikation eingerichtet. Für die weitere Beschreibung und Demonstration wird ausschließlich der Publisher von CL11 verwendet!

Transaktionale Replikation - Replication Monitor
Replikationsmonitor mit implementierten Replikationspartnern

Für den Test wurden die folgenden Objekte von CL11 über den Distributor CL12 für den Subscriber (CL12) bereitgestellt:

Transaktionale Replikation - Publication
Publikation mit veröffentlichten Artikeln

Neben den Tabellen für die Auftragsverwaltung wird die Stored Procedure [dbo].[ChangeInvoice] auf dem Subscriber veröffentlicht. Diese Stored Procedure dient als Initiator für die Änderung von großen Datenmengen und hat folgenden Inhalt:

CREATE OR ALTER PROCEDURE dbo.ChangeInvoices
	@MaxId INT,
	@NewPrefix VARCHAR(2)
AS
BEGIN
	SET NOCOUNT ON;

	UPDATE dbo.CustomerOrders
	SET    InvoiceNumber = @NewPrefix + CAST(Id AS VARCHAR(10))
	WHERE  Id <= @MaxId;
END
GO

Der für die Prozedur verwendete Parameter @MaxId bestimmt, wie viele Datensätze in den nachfolgenden Beispielen geändert werden sollen.

Problemstellung

Gelegentlich müssen sehr große Datenbestände geändert werden (>=10.000.000 Datensätze). Dieses extreme Datenvolumen muss zunächst vom Log Reader gelesen und anschließend in der Systemdatenbank des Distributors gespeichert werden. Die Publisher nehmen keine Aufgabe im Umfeld der Datenverarbeitung im Rahmen der Transaktionalen Replikation war. Alle Prozesse werden auf dem Subscriber [CL12] ausgeführt.

Transaktionale Replikation - Process Chain

Der Log Reader scannt das Transaktionsprotokoll der publizierten Datenbank und speichert die Transaktionen in der Systemdatenbank des Distributors. Sobald der Subscriber Daten anfordert, werden die Daten aus der Systemdatenbank an den Subscriber übermittelt. Alle Prozesse finden parallel auf dem Server CL12.db-berater.local statt. Hierbei kommt es – insbesondere bei der Verarbeitung großer Datenmengen – immer wieder zu Engpässen, bei denen Transaktionen mit einer Verzögerung von bis zu 4 Stunden verarbeitet werden.

Problembeschreibung

Neben den Standardproblemen (langsames Netzwerk, langsames Storage, schlechtes Datenbankdesign, ….) sidn für die hohen Latenzen Microsoft SQL Server selbst verantwortlich, da er – als Standard – nicht den eigentlichen Befehl für die Datenänderungen an den Distributor übermittelt. Der Log Reader durchsucht das Transaktionsprotokoll des Publishers nach Transaktionseinträgen, die als REPLICATE gekennzeichnet sind und speichert in der Systemdatenbank des Distributors die Anweisung, die auf einem Subscriber ausgeführt werden muss.

BEGIN TRANSACTION UpdateOrder
GO
	UPDATE dbo.CustomerOrders
	SET    InvoiceNumber = 'UR' + CAST(Id AS VARCHAR(10))
	WHERE  Id = 526076;
	GO

	SELECT [Current LSN],
	       Operation,
	       Context,
	       Description
	FROM   sys.fn_dblog(NULL, NULL)
	WHERE  [Transaction ID] IN
	       (
		      SELECT [TRANSACTION ID] FROM sys.fn_dblog(NULL, NULL)
		      WHERE  [Transaction Name] = N'UpdateOrder'
	       );
	GO
COMMIT TRANSACTION;
GO
gekennzeichnete Transaktionen in Transaktionsprotokoll

Werden mehrere Datensätze geändert, so wird für JEDEN geänderten Datensatz der Tabelle ein separater Eintrag im Transaktionsprotokoll erstellt. Werden beispielsweise 10 Datensätze geändert, werden 10 korrespondierende Einträge im Transaktionsprotokoll gespeichert:

BEGIN TRANSACTION UpdateOrder
GO
	UPDATE dbo.CustomerOrders
	SET    InvoiceNumber = 'UR' + CAST(Id AS VARCHAR(10))
	WHERE  Id <= 526085;
	GO

	SELECT [Current LSN],
	       Operation,
	       Context,
	       Description
	FROM   sys.fn_dblog(NULL, NULL)
	WHERE  [Transaction ID] IN
	       (
		      SELECT [TRANSACTION ID] FROM sys.fn_dblog(NULL, NULL)
		      WHERE  [Transaction Name] = N'UpdateOrder'
	       );
	GO
COMMIT TRANSACTION;
GO
Update mehrerer Datensätze innerhalb einer Transaktion

Der Log Reader liest diese Einträge aus dem Transaktionsprotokoll und überführt die Aktionen in die Distributionsdatenbank. Diese Informationen kann man mit der Systemprozedur sp_browsereplcmds auf dem Distributor abfragen.

CREATE TABLE #replcmds
(
    xact_seqno					VARBINARY(16)	NULL,
    originator_srvname			sysname	NULL,
    originator_db				sysname	NULL,
    article_id					INT	NULL,
    type						INT	NULL,
    partial_command				BIT	NULL,
    hashkey						INT	NULL,
    originator_publication_id	INT	NULL,
    originator_db_version		INT	NULL,
    originator_lsn				VARBINARY(16)	NULL,
    command						NVARCHAR(1024)	NULL,
    command_id					INT	NULL
);
GO

INSERT INTO #replcmds
EXEC sp_browsereplcmds;
GO

SELECT xact_seqno ,
       article_id ,
       type ,
       originator_lsn ,
       command ,
       command_id
FROM #replcmds
Einträge in Systemdatenbank des Distributors

Die in der Spalte [command] angezeigten Befehle werden auf dem Subscriber ausgeführt. Jede Tabelle, die Teil einer Publikation ist, besitzt auf dem Subscriber entsprechende Stored Procedures, die für die unterschiedlichen DML-Operationen verwendet werden.

Stored Procedures für jede Tabelle für jede DML-Operation

Nun kann man sich natürlich sehr gut vorstellen, was passiert, wenn auf dem Publisher eine sehr große Anzahl von Datensätzen eingetragen, geändert oder gelöscht werden. In diesem Fall werden ALLE DML-Operationen auf den Subscribern reproduziert.

UPDATE dbo.Customers
SET    InsertUser = SUSER_SNAME();
GO
75.000 Änderungen in dbo.Customers

In der oben ausgeführten Transaktion wurden 75.000 Datensätze geändert. Für den Subscriber bedeutet das „Höchstleistungen“, da er jede der 75.000 Änderungen EINZELN (aber in einer Transaktion) verarbeitet!

75.000 Aufrufe der Update-Prozedur für die Tabelle dbo.Customers

Problemlösung

Nachdem das Problem (na ja – ist ja eigentlich kein Problem!) erkannt wurde, wurden Lösungen gesucht, wie man diesen „Overhead“ vermeiden oder aber besser verteilen kann. Wenn – wie in einem gut geplanten Datenbanksystem erforderlich – mit Stored Procedures auf die Daten zugegriffen wird und Daten manipuliert werden, sollte sorgfältig auf die Struktur und Arbeitsweise der Prozeduren geachtet werden. Eine schlecht programmierte Stored Procedure kann für eine hoch transaktionelle Replikation schnell zu einer Katastrophe werden.

Optimierung von Stored Procedures für DML-Operationen

Der folgende Code zeigt eine Stored Procedure, die so tatsächlich wohl nie im Einsatz sein wird; sie demonstriert aber recht gut, was passiert, wenn eine replizierte Tabelle in einem Aktualisierungsprozess mehrmals beschrieben wird.

CREATE OR ALTER PROCEDURE dbo.ChangeCustomerData
	@Customer_Id	INT,
	@NewName		VARCHAR(200)
AS
BEGIN
	SET NOCOUNT ON;

	-- Update of customer name
	UPDATE dbo.Customers
	SET    Name = @NewName
	WHERE  Id = @Customer_Id;

	UPDATE dbo.Customers
	SET    InsertUser = SUSER_SNAME()
	WHERE  Id = @Customer_Id;

	UPDATE dbo.Customers
	SET    InsertDate = GETDATE()
	WHERE  Id = @Customer_Id;
END
GO

Wenn die oben genannte Prozedur nun ausgeführt wird, werden 3 (!!!) separate Transaktionen initiiert. Diese Transaktionen müssen natürlich in genau dieser Reihenfolge auf dem Subscriber eingespielt werden!

3 Einzeltransaktionen werden auf dem Subscriber eingespielt

Microsoft SQL Server ist jedoch so smart, zu erkennen, ob ein Attribut auch tatsächlich einen neuen Wert erhalten hat. Ist das nicht der Fall, so wird dieser Teil der Transaktion nicht auf den Distributor gesendet!

EXEC dbo.ChangeCustomerData @Customer_Id = 10, @NewName = 'Katharina Ricken';
GO

Wird die Prozedur für den gleichen Datensatz mit dem gleichen Benutzer erneut ausgeführt, so wird der Eintrag für die Spalte 0x04 (3 Spalte in Tabelle = [InsertUser]) nicht auf den Distributor übertragen, da dieser Eintrag sich nicht geändert hat!

Nur tatsächlich geänderte Werte werden zum Distributor gesendet

Die oben beschriebene Prozedur ist natürlich so in der Realität kaum anzutreffen – sie birgt aber erhebliches Optimierungspotential, indem die Anpassungen an einem Datensatz möglichst in einem Statement ausgeführt werden. Somit wird dann auch nur ein Transaktionsschritt an den Distributor gesendet.

CREATE OR ALTER PROCEDURE dbo.ChangeCustomerData
	@Customer_Id	INT,
	@NewName		VARCHAR(200)
AS
BEGIN
	SET NOCOUNT ON;

	-- Update of customer name
	UPDATE dbo.Customers
	SET    Name = @NewName,
	       InsertUser = SUSER_SNAME(),
	       InsertDate = GETDATE()
	WHERE  Id = @Customer_Id;
END
GO

Ausführen von Stored Procedure auf Subscriber

Die – relativ – harmlose Prozedur, die lediglich einen Datensatz anpasst, kann durch Umschreiben optimiert werden. Wie ist es aber mit Prozeduren, die eine große Datenmenge manipuliert?

CREATE OR ALTER PROCEDURE dbo.ChangeInvoices
	@MaxId INT,
	@NewPrefix VARCHAR(2)
AS
BEGIN
	SET NOCOUNT ON;

	UPDATE dbo.CustomerOrders
	SET    InvoiceNumber = @NewPrefix + CAST(Id AS VARCHAR(10))
	WHERE  Id <= @MaxId;
END
GO

Die zweite Stored Procedure ändert das Präfix von Rechnungsnummern. Hierbei wird die letzte Id angegeben, bis zu der eine Anpassung an den Datensätzen erfolgen soll. Somit kann es sich um kleine Datenmengen oder aber um sehr große Datenmengen handeln. Als Standard übermittelt Microsoft SQL Server immer die Transaktionseinträge! Wird beispielsweise die obige Prozedur für 10 Rechnungen gestartet, so ergibt sich ein Transaktionsvolumen von 10 Datensätzen, die zum Distributor übertragen und anschließend auf dem Subscriber angewendet werden.

EXEC dbo.ChangeInvoices @MaxId = 526085, @NewPrefix = 'AB';
GO

Mit dem Ausführen der Prozedur werden die Rechnungsnummern der ersten 10 Aufträge mit dem Präfix „AB“ versehen. Auf dem Distributor wird die Ausführung wie folgt verarbeitet:

Das Ausführen EINER Stored Procedure verursache 10 Replikationseinträge

Die Ursache für dieses Verhalten wurde bereits weiter oben erläutert: Microsoft SQL Server übermittelt nicht den Befehl für die Datenänderungen an den Distributor sondern den Inhalt des Transaktionsprotokolls. Nun kann sich jeder vorstellen, was passiert, wenn 1.000.000 Rechnungen angepasst werden müssen.

Leider wird eine Option häufig bei der Einrichtung der Publikation übersehen – sie ist ein wesentlicher Garant für die Optimierung von Transaktionsprozessen auf dem Subscriber. Wird eine Stored Prozedure als Artikel in eine Publikation übernommen, kann durch die Konfiguration bestimmt werden, wie Datenänderungen, die durch die Stored Procedure verursacht werden, auf dem Subscriber verarbeitet werden.

Stored Procedure Definition only

In diesem Fall wird lediglich die Definition der Stored Procedure zum Subscriber übertragen. Transaktionen auf dem Publisher werden jedoch als Einzeltransaktionen zum Subscriber übermittelt!

Execution of the Stored Procedure

Mit dieser Option ist es möglich, dass die Ausführung auf alle Subscriber repliziert werden kann, unabhängig davon, ob einzelne Anweisungen in der gespeicherten Prozedur erfolgreich waren. Da Änderungen, die durch die gespeicherte Prozedur an Daten vorgenommen werden, innerhalb mehrerer Transaktionen auftreten können, stimmen die Daten bei den Abonnenten möglicherweise nicht mit den Daten beim Publisher überein.

Hinweis

Wenn Transaktionen im READ UNCOMMITTED Isolation Level ausgeführt werden, werden die Daten erneut als eine Reihe von DML-Anweisungen an die Subscriber übermittelt.

https://docs.microsoft.com/en-us/sql/relational-databases/replication/transactional/publishing-stored-procedure-execution-in-transactional-replication

Execution in a serialized transaction of the SP

Die „Serialized“ Option wird empfohlen, da sie die Prozedurausführung nur repliziert, wenn die Prozedur im Kontext einer Serialized Transaktion ausgeführt wird. Wird die Stored Procedure von außerhalb einer serialized Transaction ausgeführt wird, werden Änderungen an den Subscriber als eine Reihe von DML-Anweisungen repliziert.

EXEC dbo.ChangeInvoices
	@MaxId = 526085,
	@NewPrefix = 'SQ';

Wird die Stored Procedure mit der Option „Execution of the Stored Procedure“ ausgeführt, ergibt sich für die zum Subscriber gesendeten Befehle ein vollständig anderes Bild:

Transactional Replication und Change Data Capture

Wenn CDC (Change Data Capture) für eine Datenbank aktiviert ist, die als Publikation für eine Transaktionale Replikation verwendet wird, funktioniert die Replikation der Ausführung gespeicherter Prozeduren nicht! Diese Einschränkung ist „by design“ und kann auch nicht deaktiviert werden. CDC (Change Data Capture) unterstützt die Nachverfolgung auf Ausführungsebene gespeicherter Prozeduren nicht. Dies bedeutet, dass einzelne Zeilen, die durch die Ausführung gespeicherter Prozeduren protokolliert werden, mit dem Bit REPLICATE gekennzeichnet werden müssen, sodass die Transaktionsreplikation nur die Ausführung gespeicherter Prozeduren replizieren kann.

Generische Prozedur für Ausführung von Massenänderungen

Die Einschränkungen von Change Data Capture kann man umgehen, indem man mit Hilfe eines Tricks die Ausführung einer Stored Procedure auf dem Subscriber „erzwingt“. Das Stichwort heißt „generische DML Operationen“. Hierbei wird zunächst eine Stored Procedure erstellt, die ein beliebiges SQL-Statement als Parameter verwendet.

CREATE OR ALTER PROCEDURE dbo.Distribute_DML_Operations
	@stmt	NVARCHAR(4000)
AS
BEGIN
	SET NOCOUNT ON;

	EXEC sp_executesql @stmt;
END
GO

Diese Stored Procedure wird anschließend zur Publikation hinzugefügt. Dabei wird jedoch darauf geachtet, dass die Option „Execution of the Stored Procedure“ für den Artikel verwendet/konfiguriert wird!

Nachdem ein erneuter Snapshot generiert und der Subscriber neu initialisiert wurde, steht diese Stored Procedure auch auf dem Subscriber zur Verfügung. Der Aufruf der Prozedur wird dann ebenfalls auf dem Subscriber ausgeführt.

EXEC dbo.Distribute_DML_Operations
	@stmt = N'UPDATE dbo.Customers SET InsertUser = ''Donald Duck'';';
GO
USE distribution;
GO

CREATE TABLE #replcmds
(
    xact_seqno					VARBINARY(16)	NULL,
    originator_srvname			sysname	NULL,
    originator_db				sysname	NULL,
    article_id					INT	NULL,
    type						INT	NULL,
    partial_command				BIT	NULL,
    hashkey						INT	NULL,
    originator_publication_id	INT	NULL,
    originator_db_version		INT	NULL,
    originator_lsn				VARBINARY(16)	NULL,
    command						NVARCHAR(1024)	NULL,
    command_id					INT	NULL
);
GO

INSERT INTO #replcmds WITH (TABLOCK)
EXEC sp_browsereplcmds;
GO

SELECT xact_seqno ,
       article_id ,
       type ,
       originator_lsn ,
       command ,
       command_id
FROM #replcmds
GO

DROP TABLE #replcmds;
GO

Der Vorteil dieser Variante ist, dass sie auch mit aktiviertem Change Data Capture für die Tabelle funktioniert! Bedingt durch die Ausführung des Codes mit Hilfe von sp_executesql wird keine unmittelbare DML-Operation in der Stored Procedure erkannt und somit kann der Code der Stored Procedure auf dem Subscriber ausgeführt werden!

USE CustomerOrders;
GO

EXEC sys.sp_cdc_enable_db;
GO

EXEC sys.sp_cdc_enable_table @source_schema = N'dbo',
                             @source_name = N'Customers',
                             @capture_instance = N'Customers',
                             @supports_net_changes = 1,
                             @role_name = N'cdc_admin',
                             @index_name = N'pk_Customers_Id';
GO

Das obige Skript aktiviert CDC für die publizierte Datenbank und im Speziellen für den publizierten Artikel [dbo].[Customers]. Wird dann die Stored Procedure für generischen Code ausgeführt, werden die Änderungen in der Historie (CDC) der Tabelle gespeichert, aber auf dem Subscriber werden keine Einzeltransaktionen ausgeführt sondern der Aufruf der Stored Procedure initiiert.

-- Ausführung auf dem Publisher
EXEC dbo.Distribute_DML_Operations
	@stmt = N'UPDATE dbo.Customers SET InsertUser = ''Mickey Maus'';';
GO

Die Änderungshistorie der CDC-enabled Tabelle kann mit dem folgenden Code auf dem Publisher abgefragt werden:

-- Ausführung auf dem Publisher
DECLARE	@from_lsn BINARY(10) = sys.fn_cdc_get_min_lsn('Customers'),
		@to_lsn BINARY(10) = sys.fn_cdc_get_max_lsn();

SELECT Id ,
       Name ,
       InsertUser ,
       InsertDate
FROM   cdc.fn_cdc_get_all_changes_Customers
       (
          @from_lsn,
          @to_lsn,
          N'ALL'
       );  
GO

Die Überprüfung der tatsächlich vom Distributor verteilten Transaktionen kann abschließend mit oben genannten T-SQL-Code erneut überprüft werden:

Hinweis

Die hier gezeigte Lösung muss sorgfältig überprüft werden, da sie SQL-Statements entgegen nimmt. Folgende Punkte sollten bei einer möglichen Implementierung überdacht werden. Es sollte auf jeden Fall ein Parsing innerhalb der Prozedur stattfinden und das Dienstkonto für den Distributor sollte über keine erweiterten Serverberechtigungen (z. B. sysadmin) besitzen. Damit wird der Subscriber angreifbar.

Ideal wäre es, den auszuführenden Code auf eine Tabelle zu beschränken und auf separate Parameter für Schema, Tabelle, Attribut, Neuer Wert und Bedingung aufzuteilen. So ist das Parsing deutlich einfacher durchzuführen!

Subscription Stream

Wenn als Bottleneck der Subscriber erkannt wird, gibt es eine effiziente Möglichkeit, die Daten zum Subscriber zu senden. Hierzu werden die Eigenschaften des Auftrags für die Verteilung der Daten vom Distributor zum Subscriber modifiziert. Der Parameter „SubscriptionStream“ verringert die Latenz beim Verschieben von Daten vom Distributor zum Subscriber, indem mehrere parallele Writer-Threads verwendet werden.

Auftrag für den Distributor-Task
Eigenschaften des Schritts für die Ausführung des Distributor-Tasks

Schaut man sich den Befehl für die Ausführung des Auftragsschritts an, kann die Option einfach am Ende des Befehls angefügt werden. Der numerische Wert gibt an, wie viele Prozesse für die Verteilung der Daten zum Subscriber verwendet werden sollen.

-Publisher [CL11] -PublisherDB [CustomerOrders] -Publication [CustomerOrders]
    -Distributor [CL12]
    -SubscriptionType 1
    -Subscriber [CL12]
    -SubscriberSecurityMode 1
    -SubscriberDB [CustomerOrders]
    -Continuous
    -SubscriptionStreams 4

Nachdem die Option „SubscriptionStream“ als Startparameter hinzugefügt wurde, muss der Auftrag einmal neu gestartet werden und mit Hilfe der nachfolgenden Abfrage kann man sehen, dass mehrere Prozesse für die Verteilung der Daten zum Subscriber erstellt wurden.

SELECT status,
       program_name,
       host_process_id,
       cpu_time,
       last_request_start_time,
       last_request_end_time,
       row_count
FROM   sys.dm_exec_sessions
WHERE  is_user_process = 1
       AND program_name = 'CL11_CustomerOrders_CustomerOrders';
GO
4 parallele Prozesse verarbeiten Transaktionen aus des Distributors

Wenn der Distributor eine Transaktion auf dem Subscriber nicht ausführen kann, brechen alle Verbindungen den aktuellen Batch ab und der Agent verwendet wieder einen einzelnen Stream, um die fehlgeschlagenen Transaktionen erneut zum Subscriber zu übertragen. Um erneut mit mehreren Threads die Daten zum Subscriber zu übertragen, muss der Job neu gestartet werden!

Um herauszufinden, welcher Fehler dazu geführt hat, dass von mehreren Streams auf einen einzelnen Stream zurückgefallen wurde, muss der Parameter „Verbose Log“ im Distribution Job aktiviert werden! um das Agentenprotokoll abzurufen.

Persönliche Worte

Ich betreibe mein Unternehmen und meine Webpräsenz seit 2007. Viele Artikel wurden geschrieben, viele Workshops und viele Konferenzen besucht und viele Reisen unternommen. Für mich stand – und steht auch jetzt noch – der persönliche Kontakt zur großen #sqlfamily im Vordergrund. Meinen Blog habe ich damals bewusst in Deutsch geplant und geschrieben, da es viele sehr gute Blogs in englischer Sprache gibt.

Das Jahr 2020 hat vieles geändert:

  • Konferenzen finden nur noch virtuell statt
  • Reisen werden schwierig bis unmöglich
  • Persönliche Kontakte werden vermieden

Zu allem Überdruss wurde dann dieses Jahr bekannt, dass die „Global Pass“ im Januar 2021 ihren Geschäftsbetrieb einstellt und somit für die vielen User Groups auf der ganzen Welt auch 2021 neue Herausforderungen – neben COVID 19 – vor der Türe stehen.

Dieser Artikel wird das Jahr 2020 für mich beruflich abschließen. Ich möchte allen Lesern, Freunden in der Community und deren Familien ein schönes Weihnachtsfest und einen guten Start in das Jahr 2021 wünschen. Bleibt alle gesund und lasst euch von der aktuellen Situation nicht unterkriegen.

Ich gebe zu, dass mir in 2020 BESONDERS die vielen persönlichen Kontakte zu Freunden in der #sqlfamily fehlten. Ein virtuelles Event kann den persönlichen Kontakt niemals ersetzen; ich hoffe sehr, dass wir alle uns in 2021 wieder sehen und gemeinsam feiern können.

In diesem Sinne möchte ich gerne unseren derzeitigen Bundespräsidenten zitieren:

Wir sehnen uns nach Beständigkeit,
wir sehnen uns nach Gewissheit.

Aber wären wir Menschen nicht auch mutig und offen für das Unerwartete,
dann wären schon die Hirten vor Bethlehem auseinander gelaufen.

Frank-Walter Steinmeyer – Weihnachtsansprache 2017

Vielen Dank fürs Lesen!