Blog
Friday, 12. April 2024

Verwendung von speicheroptimierten Tabellen

Rainer
IT-Consultant

Voraussetzungen

SQL Server bietet speicheroptimierte Tabellen seit Version 2014 an, zunächst nur in der Enterprise- Evaluation- und Developer-Edition, ab 2016 in allen Editionen. Dementsprechend muss der Kompatibilitätsmodus der Datenbank, in der diese Tabellen verwendet werden sollen, mindestens auf 130 eingestellt sein. Da diese Version jedoch schon sehr alt ist und eine produktive Datenbank grundsätzlich auf dem neuesten Stand sein sollte, gehen wir davon aus, dass diese Voraussetzung in Ihrer Umgebung erfüllt ist. Sollten Sie nicht sicher sein, so können Sie die Version über folgendes Statement abfragen:

select compatibility_level from sys.databases where name = '<DB-Name>'

alter database <DB-Name> set compatibility_level = <gewünschte Kompatibilitätsstufe, mindestens aber 130>
 Ermitteln der Kompatibilitätsstufe im SSMS über die DB-Optionen (hier 2016/130)
  1. Speicheroptimierte Tabellen werden i.W. im Hauptspeicher gehalten, so dass je nach Verwendung dieses Tabellentyps ausreichend Hauptspeicher verfügbar sein muss. Dies bedeutet insbesondere, dass der Einsatzbereich dieses Tabellentyps im OLTP-Bereich liegt und typischerweise nicht bei der Verwendung extrem großer Tabellen, wie sie beispielsweise im DataWarehouse Umfeld zu finden sind. Es gibt jedoch bei der Verwendung speicheroptimierter Tabellen kein Größenlimit, so dass der limitierende Faktor bezüglich der Größe immer nur der verfügbare Hauptspeicher ist. Bei Version 2014 empfiehlt Microsoft jedoch, nicht mehr als 250 GB für speicheroptimierte Tabellen zu verwenden.

  2. Sofern Sie nativ gespeicherte Prozeduren verwenden (Details hierzu unter Native Kompilierung von Tabellen und gespeicherten Prozeduren SQL Server oder sich Zugriffe innerhalb einer Transaktion sowohl auf speicheroptimierte als auch auf herkömmliche Tabellen beziehen, muss das Transaktions-Isolations-Level der speicher-optimierten Tabellen auf SNAPSHOT eingestellt werden. Dies wird mit dem folgenden Statement erreicht:

alter database current set memory_optimized_elevate_to_snapshot=ON;
  1. Speicheroptimierte Tabellen liegen nicht in “normalen” Datenfiles (.mdf, .ndf), sondern in Filestream-Containern. Bei einem solchen Container handelt es sich um ein Verzeichnis mit einer archivierbaren Datei mit dem Namen filestream.hdr, dem Filestream Header, und den Unterverzeichnissen $FSLOG bzw. $HKv2. Diese beiden Verzeichnisse enthalten Dateien die, grob gesagt, die gleiche Aufgabe haben wie die Transaktionslog- bzw. Datendateien für reguläre Tabellen.
Mainzer Datenfabrik - Verwendung von speicheroptimierten Tabellen
  1. Wegen der internen Struktur von speicheroptimierten Tabellen können diese nicht als Clustered Tables definiert werden.

  2. Abweichend von der Syntax für reguläre Tabellen können Indizes in dieser Tabellenart nicht mit der gewohnten Syntax CREATE/DROP/ALTER INDEX, sondern nur mit der Syntax ALTER TABLE <Tabellenname> ADD/DROP/ALTER INDEX administriert werden. Allgemein weicht auch die Syntax zum Erstellen speicheroptimierter Tabellen von der regulärer Tabellen ab. Details hierzu finden Sie auf der Microsoft Seite der Syntaxspezifikation für Create Table Statements im Abschnitt Syntax für speicheroptimierte Tabellen.

  3. Beim Anlegen von speicheroptimierten Tabellen können Sie über den DURABILITY-Parameter angeben, ob nur die Tabellenstruktur oder auch die Daten persistiert werden sollen. Entscheiden Sie sich nur für die Tabellenstruktur (SCHEMA_ONLY), so gehen die gespeicherten Daten beim Herunterfahren der Datenbank verloren und müssen dann ggf. beim Start initial neu geladen werden. Der Default-Fall ist SCHEMA_AND_DATA.

  4. Um die volle Performance speicheroptimierter Tabellen zu erreichen, sollte der SQL Server Dienst-User die Instant File Initialization (IFI) Berechtigung haben. Diese wird in den meisten Fällen bereits im Rahmen der MSSQL Installation eingestellt. Falls Sie unsicher sind, ob diese Berechtigung bei Ihrer Installation erfolgt ist oder falls Sie diese Berechtigung nachträglich vergeben möchten, können sie secpol.msc entsprechend der Dokumentation in Schnelle Datenbankdateiinitialisierung - SQL Server verwenden.

  5. Obwohl speicheroptimierte Objekte in allen neueren MSSQL Editionen verfügbar sind, muss die folgende Einschränkung beachtet werden (Angaben für MSSQL 2022):

  • Enterprise-Edition: Unlimitiert
  • Standard-Edition: max. 32 GB
  • Web-Edition: max. 16 GB

Weitere Einschränkungen, die sich aus dem Aufruf des Memory Optimization Advisor im SQL Server ergeben und die unter learn.microsoft.com/de-de/sql/relational-databases/in-memory-oltp/transact-sql-support-for-in-memory-oltp genauer beschrieben werden, sind:

  • Benutzertransaktionen, die auf speicheroptimierte Tabellen arbeiten, können nicht auf weitere Datenbanken zugreifen

Einige Hints werden von speicheroptimierten Tabellen nicht unterstützt, nämlich

  • HOLDLOCK

  • PAGLOCK

  • READCOMMITTED

  • READPAST

  • READUNCOMMITTED

  • ROWLOCK

  • TABLOCK

  • TABLOCKX

  • UPDLOCK

  • XLOCK

  • NOWAIT

  • TRUNCATE TABLE und MERGE Statements können nicht für speicheroptimierte Tabellen verwendet werden.

  • Dynamische und Keyset Cursor werden im Zusammenhang mit speicheroptimierten Tabellen automatisch in statische Cursor umgewandelt.

  • Einige Features auf Datenbankebene werden für speicheroptimierte Objekte nicht unterstützt. Zu diesen Einschränkungen wird von Microsoft der folgende Link von In-Memory OLTP nicht unterstützte Transact-SQL-Konstrukte angeboten.

  • Der Zugriff auf speicheroptimierte Tabellen unter Verwendung der READ COMMITTED-Isolationsstufe wird nur für Autocommit-Transaktionen, nicht jedoch für explizite oder implizite Transaktionen unterstützt.

Sie sehen, es sind viele Punkte zu berücksichtigen und es gibt viele Einschränkungen. Im Rahmen der Vorbereitung dieses Artikels war geplant, eine Umwandlung von sechs Tabellen aus der AdventureWorks2019 Datenbank in speicheroptimierte Tabellen durchzuführen und dann einen Performancevergleich durchzuführen. Details dazu weiter unten.

Vorsicht Bug

Sofern Sie SQL Server 2022 in einer ungepatchten Version verwenden, werden Sie bei dem Versuch, Memory Optimized Tables zu konfigurieren, die Fehlernummer 35221 erhalten. Hier müssen Sie zunächst - möglichst das neueste - Cumulative Update installieren. Alternativ können Sie aber auch das Trace-Flag 12324 setzen, was wir aber nicht empfehlen. Details finden Sie in Versionshinweise zu SQL Server 2022 - SQL Server.

Schritt 1: Erstellen eines Filestream-Containers

Die folgenden Code-Zeilen wurden verwendet, um in einer Datenbank mit dem Namen MEM_OPT_TEST eine Filestream-Gruppe mit dem Namen MEM_OPT_FG in dem neu anzulegenden Container MEM_OPT_FG_DIR anzulegen, der in diesem Beispiel im Verzeichnis C:\Program Files\Microsoft SQL Server\MSSQL15.MADAFA\MSSQL\DATA liegen soll. Größe und Wachstum werden in diesem Script mit Default-Werte belegt, können alternativ aber auch mit den gewünschten Parametern versehen werden.

-- Da hier die Datenbank nicht explizit genannt, sondern
-- das keyword current verwendet wurde, muss die aktuelle
-- Session mit der Datenbank verbunden sein, in der man die neue Filegruppe
-- einrichten möchte.
-- - die Instanz (nicht die Datenbank!) heißt in diesem Fall MADAFA
-- - die einzurichtende Filegruppe MEM_OPT_FG
-- - das Verzeichnis in dem weitere Verzeichnisse und Dateien angelegt werden MEM_OPT_FG_DIR
ALTER DATABASE current
ADD FILEGROUP [MEM_OPT_FG] CONTAINS MEMORY_OPTIMIZED_DATA 
ALTER DATABASE current 
ADD FILE ( NAME = N'MEM_OPT_FG',
FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL15.MADAFA\MSSQL\DATA\MEM_OPT_FG_DIR')
TO FILEGROUP [MEM_OPT_FG]
GO

Alternativ Anlegen des Containers im Management Studio

Anlegen der Filegruppe MEM_OPT_FG

Anschließend steht die Zuordnung eines neuen “Files” zu dieser Gruppe an. Hier muss jedoch beachtet werden: Möchte man eine analoge Operation wie im Codeblock zuvor haben, das heißt Verwendung des logischen Namens MEM_OPT_FG und abspeichern der Daten in einem Verzeichnis mit dem Namen MEM_OPT_FG_DIR , so kann dies über die GUI nur in zwei Schritten erfolgen:

  1. Im ersten Schritt wird das Filestream-File mit dem Verzeichnisnamen MEM_OPT_FG_DIR erstellt. Hierdurch wird implizit der gleiche logische Filename eingerichtet.
  2. Im zweiten Schritt wird der logische Name in den gewünschten Namen MEM_OPT_FG geändert.
Zuordnung eines neuen Verzeichnis zu der zuvor erstellten Filegruppe
Änderung des logischen Daten”file”-Namens (=Löschen der “_DIR” Endung)

Migration

Um bestehende reguläre Objekte und T-SQL Prozeduren in speicheroptimierte Objekte zu überführen, bietet das Management Studio einen entsprechenden Wizard an. In diesem Artikel wird jedoch lediglich die Migration von Tabellen, also nicht die von T-SQL Prozeduren, behandelt.

Demo

Einrichtung der Demo-Datenbank

Zur Demonstration des erwähnten Wizard werden einige ausgewählte Tabellen der AdventureWorks2019 Datenbank verwendet, die unter AdventureWorks-Beispieldatenbanken - SQL Server heruntergeladen werden kann. Es wurde für diesen Artikel die OLTP Variante gewählt.

Zur Erstellung eines Test-Szenario war ursprünglich geplant, einen Join über die folgenden sechs Tabellen für Performancebetrachtungen zu migrieren, nämlich

  1. Production.WorkOrder
  2. Production.WorkOrderRouting
  3. Production.Product
  4. Production.ProductModel
  5. Production.ProductModelProductDescriptionCulture
  6. Production.ProductDescription

Das zugehörige Statement:

select P.Name
     , P.ProductNumber
	 , P.Color
	 , PM.Name as [Model Name]
	 , PMPDC.CultureID
	 , PD.Description
from Production.WorkOrder WO inner join Production.WorkOrderRouting WOR
on WO.WorkOrderID = WOR.WorkOrderID
inner join Production.Product P on P.ProductID = WO.ProductID
inner join Production.ProductModel PM on PM.ProductModelID = P.ProductModelID
inner join Production.ProductModelProductDescriptionCulture PMPDC on PMPDC.ProductModelID = P.ProductModelID
inner join Production.ProductDescription PD on PD.ProductDescriptionID = PMPDC.ProductDescriptionID

Prüfen der Migrationsbedingungen

Die Prüfung, ob eine Datenbank oder bestimmte Objekte der Datenbank in speicheroptimierte Objekte umgewandelt werden können, erfolgt durch Selektion der Datenbank und Verwendung der rechten Maustaste. Es öffnet sich ein Kontextmenü, in dem unter “Tasks” der Menüeintrag “Prüflisten für die Migration zum In-Memory-OLPT erstellen” ausgewählt wird.

Aktivierung des Migrations-Wizard für In-Memory

Es öffnet sich ein Tool, mit dem die Überprüfung der Migrationsfähigkeit der Datenbank oder ausgewählter Objekte durchgeführt werden kann.

Startseite der Migrationsprüfung

Auf der nächsten Seite besteht die Möglichkeit, alle Objekte (Default) oder bestimmte Objekte prüfen zu lassen. Im hier vorgestellten Beispiel werden nur die o.a. sechs Tabelle geprüft. Die Ergebnisse der Prüfung werden als html-Dateien in dem angegebenen Verzeichnis abgelegt.

Auswahl der Prüfkandidaten

Wie bei den meisten Wizards wird auch hier zum Abschluss eine Übersicht der gewählten Einstellungen angezeigt:

Abschließender Überblick über die Anforderung

Auf das in der vorhergehenden Abbildung angezeigte Zielverzeichnis kommen wir in wenigen Zeilen zurück. Hier wird ein Verzeichnis mit dem Namen der analysierten Datenbank angelegt, in dem die objektspezifischen Auswertungsreports unter dem Namen MigrationAdvisorChecklistReport.html abgelegt werden.

Abschließende Übersicht der Prüfungsergebnisse

Die hier durchgeführte Prüfung suggeriert, alle Objekte seien bereit für die Migration. Das ist aber leider nicht korrekt; die Ergebnisse beziehen sich nur auf den Abschluss der Prüfungen. Sieht man sich nämlich die HTML-Auswertungsreports (s.o.) an, so werden u.a. die Fremdschlüssel als Hindernis für die Migration protokolliert. In der Spalte Überprüfungsergebnis existieren teilweise Links auf entsprechende Hinweise bei Microsoft.

Beispielreport für die Umwandlung der Tabelle PRODUCT (Teil A)
Beispielreport für die Umwandlung der Tabelle PRODUCT (Teil B)

Insgesamt treten für die verwendeten sechs Tabellen die folgenden Fehlermeldungen bzw. Migrationshemmnisse auf:

Mainzer Datenfabrik - Verwendung von speicheroptimierten Tabellen

Insgesamt sind wir also bei einer so geringen Anzahl wie sechs Tabellen auf 42 Hindernisse bzw. Einschränkungen gestoßen.

Fehlerbehebung

Der Link “Failed: More Information” zu den Fremdschlüsselbeziehungen führt zu der folgenden Seite Plan your adoption of in-memory OLTP - SQL Server | Microsoft Learn, auf der die zur Behebung des Problems erforderlichen Aktionen beschrieben stehen, die i.W. aus den folgenden Schritten bestehen:

  1. Verhindern, dass die Tabellendaten geändert werden (Deaktivieren der Applikation)
  2. Erstellen eines vollständigen Backups
  3. Umbenennen der umzuwandelnden Tabellen
  4. Anlegen der speicheroptimierten Tabellen mit dem Namen und den Spalten der ursprünglichen Tabelle
  5. Einfügen der Daten der “normalen” in die speicheroptimierte Tabelle
  6. Löschen der umbenannten Tabelle
  7. Erstellen eines weiteren vollständigen Backups
  8. Aktivieren der Applikation

Eingeschränkte Demo

Wegen des großen Aufwands für die Umstellung der o.a. sechs Tabellen wurde ein Test mit einer einzelnen eigens zu diesem Zweck eingerichteten Tabelle durchgeführt. Diese repräsentiert eine stark vereinfachte Tabelle eines Rollenspielanbieters mit den Spalten

  • ID - Künstlicher Primärschlüssel der Tabelle
  • SpielerName - Name des Spielers
  • Pseudonym - Name des Spieler im Spiel
  • RollenID - ID einer Rolle die der Spieler in dem Spiel einnimmt
  • Rolle - Rolle des Spielers
  • Skill - Im Verlauf der Spiele gesammelte Erfahrung
  • SkillLevel - Skill-Ebene zu der gesammelten Erfahrung

Das Ganze als Statement:

CREATE TABLE [dbo].[Spieler](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[SpielerName] [nvarchar](50) NULL,
	[Pseudonym] [nvarchar](50) NULL,
	[RollenID] [int] NULL,
	[Rolle] [nvarchar](20) NULL,
	[Skill] [float] NULL,
	[SkillLevel] [int] NULL,
PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Die Tabelle wurde mit 1.000.000 generierten Einträgen gefüllt. Anschließend wurde eine Migration durchgeführt, bei der die Quelltabelle in SpielerClustered umbenannt wurde. Dies wird in den folgenden Abbildungen wiedergegeben:

Schritt 1: Start des Migrationsvorgangs über den “Ratgeber für Speicheroptimierung” (s.o.)
Schritt 2: Ausgabe von Warnmeldungen
Schritt 3: Umbenennung der Quelltabelle und Datenübernahme in die neue Tabelle

Da speicheroptimierte Tabellen i.W. für den OLTP-Betrieb vorgesehen sind, wird auf den folgenden Wizard-Seiten die Standard-Option “NONCLUSTERED Hash-Index” beibehalten. Die Anzahl der Buckets orientiert sich grob an der Anzahl unterschiedlicher Werte in der betroffenen Spalte und wird hier unverändert gelassen.

Schritt 4: Parametrisierung der Zieltabelle
Schritt 5: Parametrisierung der Indizes (hier nur ein Index)
Schritt 6: Übersicht über die Einstellungen
Schritt 7: Fortschritt bzw. Erfolgsmeldung der Migration

Sofern Sie das Skript gegenüber der GUI bevorzugen:

USE [AdventureWorks2019]
GO

EXEC dbo.sp_rename @objname = N'[dbo].[Spieler]', @newname = N'SpielerClustered', @objtype = N'OBJECT'
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Spieler]
(
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[SpielerName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[Pseudonym] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[RollenID] [int] NULL,
	[Rolle] [nvarchar](20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[Skill] [float](53) NULL,
	[SkillLevel] [int] NULL,

INDEX [IX_SpielerClustered_SkillLevel] NONCLUSTERED HASH 
(
	[SkillLevel]
)WITH ( BUCKET_COUNT = 16),
 CONSTRAINT [Spieler_primaryKey]  PRIMARY KEY NONCLUSTERED HASH 
(
	[Id]
)WITH ( BUCKET_COUNT = 1048576)
)WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA )
GO

SET IDENTITY_INSERT [AdventureWorks2019].[dbo].[Spieler] ON 
GO

INSERT INTO [AdventureWorks2019].[dbo].[Spieler] ([Id], [SpielerName], [Pseudonym], [RollenID], [Rolle], [Skill], [SkillLevel]) SELECT [Id], [SpielerName], [Pseudonym], [RollenID], [Rolle], [Skill], [SkillLevel] FROM [AdventureWorks2019].[dbo].[SpielerClustered] 
GO

SET IDENTITY_INSERT [AdventureWorks2019].[dbo].[Spieler] OFF 
GO

Performance / Statistik

Zum Vergleich der Performance von Zugriffen auf die üblichen Clustertabellen mit denen auf speicheroptimierte, wurden Testreihen mit delete-, insert-, select- und update-Statements durchgeführt. Jedes dieser DML-Statements wurde mehrfach in mehreren parallelen Sessions jeweils für die Tabelle Spieler und SpielerClustered, in den folgenden Codeblöcken durch “” repräsentiert, ausgeführt.

Zum Aufruf wurde SQLQueryStress von Erik Eljskov Jensen verwendet, wobei Buffer und Cache jeweils zu Beginn eines Testlaufs gelöscht wurden (das sollte man in einer produktiven Umgebung natürlich nicht machen). Es wurden jeweils hundert Iterationen mit zehn parallelen Sessions eingestellt. In jedem der verwendeten Statements werden 10.000 Datensätze selektiert, aktualisiert, eingefügt bzw. gelöscht.

Verwendete Statements

select-Statements

DECLARE @Counter as INT
      , @Zufall  INT
SET @Counter=1
WHILE ( @Counter <= 10000)
BEGIN
    set @Zufall = floor( rand() * 1000000) + 1
	select * from <Spielertabelle> where id = @Zufall
    SET @Counter  = @Counter  + 1
END

select mit Join

select * 
from dbo.<SpielerTabelle> a
   , dbo.<SpielerTabelle> b
where a.id = b.id

update-Statements

DECLARE @Counter as INT
      , @Zufall  INT
SET @Counter=1
WHILE ( @Counter <= 10000)
BEGIN
    set @Zufall = floor( rand() * 1000000) + 1
	update <Spielertabelle> set Skill = Skill * 0.0001 where id = @Zufall
    SET @Counter  = @Counter  + 1
END

insert-Statements

DECLARE @Counter as INT
      , @Zufall  INT
SET @Counter=1
WHILE ( @Counter <= 10000)
BEGIN
    insert into <SpielerTabelle> (SpielerName,Pseudonym,RollenID,Rolle,Skill,SkillLevel)
    values('Dummy','Dummy',1,'Dummy',0,0.0)
    SET @Counter  = @Counter  + 1
END

Im Anschluss an diese Inserts wurden die zuvor so eingefügten neuen Datensätze gelöscht, bevor es mit den eigentlichen Löschoperationen des folgenden Codeblocks weitergeht. Zu beachten ist hier, dass die jeweilige Tabelle im Verlauf der Löschoperationen (10.000 Schleifendurchläufe, 100 Iterationen, 10 Sessions) nahezu komplett geleert wird. Das heißt insbesondere, dass mit fortschreitender Anzahl der Iterationen diese immer schneller abgeschlossen sind.

DECLARE @Counter as INT
      , @Zufall  INT
SET @Counter=1
WHILE ( @Counter <= 10000)
BEGIN
    set @Zufall = floor( rand() * 1000000) + 1
	delete from <SpielerTabelle> where id = @Zufall
    SET @Counter  = @Counter  + 1
END

Im Fall der delete-Statements kam es in einigen Fällen (ca. 20) zu Exceptions beim Löschen in der speicheroptimierten Tabelle. Dies ist darauf zurückzuführen, dass wir zehn parallele Sessions verwendet haben und einige dieser Sessions im Verlauf der while-Schleife zufällig versucht haben, den gleichen Datensatz zu löschen. Dieser Fall sollte grundsätzlich von der Applikation durch ein sauberes Exception-Handling abgefangen sein, so dass er in diesem Zusammenhang irrelevant ist, er zeigt aber ein voneinander abweichendes Transaktionshandling.

Mainzer Datenfabrik - Verwendung von speicheroptimierten Tabellen

Fazit

Die Verwendung von speicheroptimierten Tabellen stellt ein großes Potential zur Verbesserung der Performance einer MSSQL Datenbank dar. Obwohl es einen Wizard für die automatische Umstellung gibt, wird man eine Umwandlung klassischer in speicheroptimierte Tabellen in den meisten Fällen manuell vornehmen müssen (Foreign-Key- und Check-Constraints). Ferner existieren viele weitere Einschränkungen, die im Vorfeld einer Umwandlung berücksichtigt werden müssen. Sofern in der Datenbank Stored Procedures verwendet werden, existiert auch hier Optimierungspotential, indem man diese in nativen Code übersetzt, das heißt, die im Normalfall interpretierten T-SQL-Statements werden durch Maschinencode realisiert.

Wenn Sie mehr zu diesem Thema erfahren möchten, stehen Ihnen unsere Experten gerne zur Verfügung. Vereinbaren Sie unverbindlich ein Beratungsgespräch über unser Kontaktformular. Wir helfen gerne weiter!

Interesse geweckt?

Unsere Expert:innen stehen Ihnen bei allen Fragen rund um Ihre IT Infrastruktur zur Seite.

Kontaktieren Sie uns gerne über das Kontaktformular und vereinbaren ein unverbindliches Beratungsgespräch mit unseren Berater:innen zur Bedarfsevaluierung. Gemeinsam optimieren wir Ihre Umgebung und steigern Ihre Performance!
Wir freuen uns auf Ihre Kontaktaufnahme!

Kontaktdaten
Taunusstraße 72
55118 Mainz
info@madafa.de
+49 6131 3331612
Bürozeiten
Montag bis Donnerstag:
9:00 - 17:00 Uhr MEZ

Freitags:
9:30 - 14:00 Uhr MEZ
Wir sind Ihre SQL Expert:innen!
Noch Fragen? - Wir haben immer die passende Antwort für Sie!