Prompt-Engineering für eine bessere SQL-Codegenerierung mit LLMs

29.05.2024

Raghid Bsat

Stellen Sie sich vor, Sie sind ein Marketing-Manager, der damit beauftragt ist, Werbestrategien zu optimieren, um verschiedene Kundensegmente effektiv anzusprechen…

Stellen Sie sich vor, Sie sind ein Marketing-Manager, der damit beauftragt ist, Werbestrategien zu optimieren, um verschiedene Kunden-Segmente effektiv anzusprechen. Sie sind gefordert, die folgende Frage zu beantworten: „Welche Kunden-Segmente erzeugen den höchsten Return on Investment für unsere Werbekampagne?“. Sie möchten vielleicht die Rentabilität der Kunden-Segmente betrachten und sie mit dem Customer Lifetime Value (CLV) vergleichen, um solche Fragen zu beantworten. Die folgende SQL-Abfrage könnte die Antwort sein:

with customer_segment_profitability as (
    select
        customer_segment,
        avg(order_total) as avg_order_value,
        count(distinct customer_id) as num_customers,
        sum(order_total) as total_sales,
        sum(advertising_cost) as total_ad_cost,
        (sum(order_total) - sum(advertising_cost)) / count(distinct customer_id) as segment_profit
    from
        orders
    group by
        customer_segment
),
customer_clv as (
    select
        customer_segment,
        sum(total_sales) as total_spent,
        count(distinct customer_id) as num_customers,
        DATEDIFF(MAX(order_date), MIN(order_date)) as lifespan_days,
        sum(total_sales) / DATEDIFF(MAX(order_date), MIN(order_date)) as clv_per_day,
        (sum(total_sales) / DATEDIFF(MAX(order_date), MIN(order_date))) * 365 as clv_yearly
    from
        orders
    where
        customer_id is not NULL
    group by
        customer_segment
)
select
    csp.customer_segment,
    csp.avg_order_value,
    csp.num_customers as segment_customers,
    csp.total_sales,
    csp.total_ad_cost,
    csp.segment_profit,
    cc.total_spent as clv_total_spent,
    cc.num_customers as clv_num_customers,
    cc.clv_yearly
from
    customer_segment_profitability csp
join
    customer_clv cc on csp.customer_segment = cc.customer_segment
order by
    csp.segment_profit desc;

Das Erstellen einer solchen komplexen Abfrage erfordert Fachwissen und Kontext, die über den Umfang vieler Geschäftsprofis hinausgehen, und würde mehr Zeit in Anspruch nehmen, um erstellt zu werden. Oft gibt es eine Sprachbarriere zwischen Geschäftsführern und den datengestützten Entscheidungen, die sie treffen müssen. Dies wird typischerweise durch SQL kodiert, das viele Geschäftsprofis als einschüchternd oder kaum praktikabel empfinden. Der Aufstieg von GenAI hat die Tür für KI-Assistenten geöffnet, solche Barrieren zu überwinden, wobei große Sprachmodelle (LLMs) die Fähigkeit gezeigt haben, natürliche Sprache zu verstehen und zu generieren. Die Verwendung von LLMs zur Erstellung von SQL-Anweisungen aus natürlicher Sprache kann das Problem des Datenzugriffs für nicht-technische Profile erleichtern.

Bis heute gab es viele Versuche, einen fortschrittlichen LLM SQL-Abfragegenerator zu erstellen. Ein Beispiel dafür ist MotherDuck’s „AI that Quacks, DUCKDB NSQL-7B“. Dieser Blogbeitrag erklärt die Bemühungen von MotherDuck und Numbers Station, ein Text-zu-SQL LLM zu erstellen. Sie haben ein Open Source LLM-Modell unter Verwendung des Basis-Modells von CodeLLama erstellt und es mit ihrem benutzerdefinierten Trainingssatz verfeinert, der eine modifizierte Version von NSText2SQL ist. Sie haben eine außergewöhnlich gute Leistung bei den verwendeten SQL-Benchmarks berichtet. Während dieses Tool erstaunlich klingt und das Trainieren eines eigenen Open-Source-Modells auf einem benutzerdefinierten SQL-Trainingsdatensatz einen bedeutenden Fortschritt im Vergleich zur bloßen Verwendung vorhandener Modelle darstellen kann, ist es wichtig, dem Modell zusätzlichen Kontext zu liefern. Dies beinhaltet das Erstellen präziser und kontextuell angemessener Eingabeaufforderungen, um die gewünschten Antworten von Sprachmodellen zu erhalten. Mit einer geeigneten Eingabeaufforderung wird dem LLM ein besserer Kontext gegeben, um fehlerfreie Abfragen zu erstellen. In diesem Blogbeitrag werde ich folgendes ansprechen: die Herausforderung der Eingabeaufforderungsgestaltung und die Szenarien, in denen die Nutzung einer Retrieval Augmented Generation (RAG)-Lösung vorteilhaft ist, im Vergleich dazu, alle Informationen direkt in die Eingabeaufforderung aufzunehmen.

Wie erstellt man eine effektive Eingabeaufforderung?

  1. Datenbankschema einbeziehen

Die größte Herausforderung bei der Verwendung eines LLM zur Generierung von SQL-Code besteht darin, das Modell über das Schema der Tabellen, die wir abfragen möchten, zu informieren. Solche Informationen können in einem, was wir als „Kontextfenster“ bezeichnen, bereitgestellt werden, das die Anzahl der Tokens (~Wörter) ist, die das Modell als Eingabe verwenden kann, wenn es Antworten generiert. Im Fall der Generierung von SQL-Code können wir unser Datenbankschema in den Kontext einbeziehen, bevor wir unsere Frage stellen. Der Haken ist, dass LLMs mit einem begrenzten Kontextfenster kommen. Zum Beispiel hat GPT-4 ein Fenster von 128.000 Tokens (~96.000 Wörter). Wenn Ihre Datenbank klein genug ist, können Sie das vollständige Schema als Teil der Eingabeaufforderung bereitstellen.

Dies ist ein Beispiel für eine Eingabeaufforderung mit dem einbezogenen Datenbankschema:

You are a helpful assistant specialising in data analysis in a snowflake warehouse.
Answer the questions by providing SQL code that is compatible with the snowflake environment.
This is the question you are required to answer: 
What is the total number of customers in the Chocolate Haven branch?

Here is the relevant context of the database:
create or replace TABLE CUSTOMER_DETAILS (
    CUSTOMER_ID NUMBER(38,0) NOT NULL,
    FIRST_NAME VARCHAR(255),
    LAST_NAME VARCHAR(255),
    EMAIL VARCHAR(255),
    PHONE VARCHAR(20),
    ADDRESS VARCHAR(255),
    primary key (CUSTOMER_ID)
);

Als ich dies mit dem Snowflake-Beispieldatensatz (TPCH_SF1) ausprobierte, bemerkte ich, dass im Allgemeinen die einfachen Abfragen korrekt beantwortet wurden. Diese Abfragen zeichnen sich durch einfache Filter und mehrere Bedingungen aus:

-- Give me all the customers where the marketing segment is 'MACHINERY'
select *
  from customer
where 
  c_mktsegment = 'MACHINERY';
-- Give me all the orders where the order priority is '3-MEDIUM' and the total price is above 100 000 euros
select *
  from orders
where 
  o_orderpriority = '3-MEDIUM' and o_totalprice > 100000;
-- Give me all the order keys where the order priority is '5-LOW' and the total price is below 250 000 euros and where the status is not 'F'
select o_orderkey
  from orders
where 
  o_orderpriority = '5-LOW' and o_totalprice <= 250000 and o_orderstatus <> 'F';
  1. Benutzerfeedback einbeziehen (Sitzungshistorie)

Im Fall von komplexen Abfragen mit mehreren Joins und Aggregationen war das LLM nicht in der Lage, genaue Ergebnisse zu reproduzieren. Es gab zwei Hauptprobleme, mit denen das LLM konfrontiert war:

  • Semantische Komplexität zwischen Tabellen: Komplexe Abfragen beinhalten oft spezifische Beziehungen zwischen Tabellen, geschachtelte Unterabfragen und bedingte Logik. LLMs könnten Schwierigkeiten haben, die volle semantische Bedeutung dieser komplexen Strukturen zu erfassen, was zu Ungenauigkeiten beim Verständnis und der Reproduktion der beabsichtigten Abfragesemantik führt.

  • Unklarheiten im Geschäft: Es kann bestimmte mehrdeutige Verweise geben, wie z.B. Spaltenalias oder mehrdeutige Tabellenreferenzen. Das Lösen dieser Mehrdeutigkeiten erfordert kontextuelles Verständnis und Inferenzfähigkeiten, die für LLMs eine Herausforderung darstellen können. Im Fall von Abfrage 2 (unten) nahm das LLM an, dass einige Spalten bereits im Schema existieren, was eine ungültige SQL-Anweisung erzeugte.

-- What is for each customer the total amount spent and the average order amount? Show our best customers (= most-paying) first.
-- This query has a join/group by that the LLM failed to identify.
select
  min(c_name) as customer_name,
  sum(coalesce(o_totalprice, 0)) as total_spent,
  avg(o_totalprice) as avg_order
from customer
  left join orders on c_custkey = o_custkey
group by 
  c_custkey
order by 
  total_spent desc;
-- Show me all the customers that have placed more than 40 orders
select
  min(c_name) as customer_name,
  count(*) as num_orders
from customer
  join 
    orders on c_custkey = o_custkey
group by 
  c_custkey
having 
  count(*) > 40;
-- For each nation, show me our top-3 highest-revenue customers in that nation. Don’t consider discounts.
-- There is a subquery to find the rank_in_nation the LLM failed to reproduce. It gave an inaccurate schema 
-- by assuming that rank already exists in the table.
with
customer_with_rank as (
  select
    c_name,
    c_nationkey,
    sum(o_totalprice) as total_revenue,
    rank() over (
      partition by c_nationkey
      order by total_revenue desc
    ) as rank_in_nation
  from customer
  join orders on c_custkey = o_custkey
  join nation on c_nationkey = n_nationkey
  group by c_name, c_nationkey
)
select *
from customer_with_rank
join nation on c_nationkey = n_nationkey
where rank_in_nation <= 3;

Benutzerfeedback kann das LLM in die richtige Richtung „drücken“, was nach einem fehlgeschlagenen Versuch zu einer korrekten Abfrage führt. Dies ist der nächste Schritt in der Eingabeaufforderungsgestaltung: Das Hinzufügen weiterer relevanter kontextueller Informationen neben den Datenbankschemas kann entscheidend für die optimierte SQL-Code-Generierung sein. Sie können die Sitzungshistorie als Teil der Eingabeaufforderung wie folgt einbeziehen:

You are a helpful assistant specialising in data analysis in a snowflake warehouse.
Answer the questions by providing SQL code that is compatible with the snowflake environment.
This is the question you are required to answer: 
What is the total number of customers in the Chocolate Haven branch?

Here is the relevant context of the database:
create or replace TABLE CUSTOMER_DETAILS (
    CUSTOMER_ID NUMBER(38,0) NOT NULL,
    FIRST_NAME VARCHAR(255),
    LAST_NAME VARCHAR(255),
    EMAIL VARCHAR(255),
    PHONE VARCHAR(20),
    ADDRESS VARCHAR(255),
    primary key (CUSTOMER_ID)
);

The user has reported the following feedback:
You should not be looking in the orders table
  1. Geschäftsinformationen einbeziehen

Für tiefergehende Geschäftskontexte neigen große Organisationen dazu, relevante Daten überall in verschiedenen Formaten und Quellen verstreut zu haben. Ich schlage vor, die Daten in drei verschiedene Kategorien zu unterteilen:

  • Fachspezifisches Wissen: Dies ist die Information, die für die Frage relevant sein kann. Zum Beispiel können wir Aussagen wie die folgenden einbeziehen:

| Die belgische Niederlassung heißt die „Schokoladen-Haven“-Niederlassung
| Die Benelux-Region besteht aus den Niederlanden, Luxemburg und Belgien;
| Marketingkampagnen in der Region „Ardennen“ haben historisch gesehen höhere Konversionsraten gezeigt.

  • Historisch verwendete Abfragen: Dies sind Abfragen, die häufig von Geschäftsanalysten verwendet werden und die Standardabfragen für gängige analytische Bedürfnisse sind: Monatlicher Verkaufsbericht nach Region, Analyse der Kundenabwanderung nach Produktkategorie, meistverkaufte Produkte nach Quartal, Rentabilitätsanalyse nach Kundensegment.

  • Datenumwandlungs- und Anreicherungslogik: Diese Kategorie umfasst die Logik und Regeln, die angewendet werden, um Rohdaten in sinnvolle Geschäftseinblicke zu transformieren und anzureichern. Einige Beispiele sind: Berechnungen für abgeleitete Kennzahlen wie Customer Lifetime Value (CLV), durchschnittlicher Bestellwert (AOV) oder Konversionsraten; Zeitbasierte Berechnungen für Trendanalysen, Saisonalitätsanpassungen oder Kohortenanalysen.

  • Bonus-Kategorie: Geschäftsregeln und -richtlinien: Während dies möglicherweise noch experimentell ist und es keine festgelegte Lösung für die Handhabung von Datenverwaltung mit GenAI gibt, würde ich dies im Allgemeinen mit Vorsicht behandeln. Diese Kategorie umfasst Regeln wie Zugriffssteuerungsrichtlinien, die bestimmen, wer innerhalb der Organisation Berechtigungen hat, um bestimmte Tabellen oder Richtlinien zur Datenaufbewahrung anzusehen oder zu ändern. Ich würde dies stattdessen in ein SQL-säuberungslogikbasiertes Modul verschieben, nachdem das LLM eine Antwort zurückgegeben hat.

Die endgültige Eingabeaufforderung in Aktion:

You are a helpful assistant specialising in data analysis in a snowflake warehouse.
Answer the questions by providing SQL code that is compatible with the snowflake environment.
This is the question you are required to answer: 
What is the total number of customers in the Chocolate Haven branch?

Here is the relevant context of the database:
create or replace TABLE CUSTOMER_DETAILS (
    CUSTOMER_ID NUMBER(38,0) NOT NULL,
    FIRST_NAME VARCHAR(255),
    LAST_NAME VARCHAR(255),
    EMAIL VARCHAR(255),
    PHONE VARCHAR(20),
    ADDRESS VARCHAR(255),
    primary key (CUSTOMER_ID)
);

The user has reported the following feedback:
You should not be looking in the orders table

This is some business context that is relevant to the question:
1. The Belgian branch is called the Chocolate Haven branch.
2. Benelux region is made up of Luxembourg, Netherlands, 
and Belgium branches
 
Historically, business analysts have used these queries 
for answering similar questions:
Query to analyse profitability by customer segment:
WITH customer_profitability AS (
 SELECT
        .... 
)

If certain aggregations or data derived metrics are needed
to answer the user's question, these are the most relevant
pieces of information needed:
Rule to calculate Customer Lifetime Value (CLV):
SELECT
    SUM(total_sales) AS total_spent,
    COUNT(DISTINCT order_id) AS total_orders,
      ...
ORDER BY
    clv_yearly DESC;

Bringen Sie Ihren LLM-SQL-Bot mit Datenengineering auf die nächste Stufe:

Für einen potenziellen zukünftigen Kunden haben wir ein Proof-of-Concept erstellt, das die Möglichkeiten von LLMs mit der Generierung von SQL-Code auf der Basis ihres Data Warehouses zeigt. Wir haben eine einfache Benutzeroberfläche erstellt, mit der Geschäftsprofis mit einem Chatbot interagieren, SQL-Anweisungen generieren und die resultierende Tabelle ausdrucken können. Im Fall der Demo, an der ich gearbeitet habe, enthielt die Datenbank nur 8 Tabellen; mit einer variierenden Schema-Länge zwischen 3 und 18 Spalten. Das Erstellen der Data Definition Language (DDL) SQL-Befehle (CREATE TABLE …) summiert sich auf 1k Tokens. In diesem kleinen Beispiel hatte ich genügend Raum mit dem Token-Limit, um alles im selben Kontext bereitzustellen. Dies mag jedoch nicht der Fall sein für große Organisationen, die tendenziell mehrere hundert Tabellen in ihren Datenbanken haben. Hier kommt die Macht der Retrieval-Augmented Generation (RAG) ins Spiel. Ich habe mich auf die Architektur verlassen, die von Kaarthik in seinem Blogbeitrag vorgeschlagen wurde. Seine Hauptidee war die Verwendung eines RAG, das Informationen aus einer Vektordatenbank abruft, die mit den Tabellenschemas erstellt wurde. Das Ergebnis der Abfrage würde später in die Eingabeaufforderung gegeben werden, die an das LLM über eine API weitergegeben wird. Zusammenfassend ist dies die verfolgte Architektur:

Durch die Verwendung eines RAG müssten Sie nicht den gesamten Kontext in die Eingabeaufforderung einfügen, sondern nur die relevantesten Teile. Die Idee eines RAG besteht darin, auf vorhandenes Wissen oder Daten zuzugreifen, um genaue Antworten zu geben. Die relevanten Informationen werden abgerufen und in die Eingabeaufforderung des LLM eingefügt, um bessere Antworten zu generieren. Im Fall dieser Demo waren es die Tabellenschemata (Datenbankschema-Abfragen), die als readme-Dateien mit zusätzlichem Kontext für jede Tabelle in die Vektordatenbank eingefügt wurden. Relevante Schemata werden mit einer Ähnlichkeitssuche abgerufen, bevor sie in die Eingabeaufforderung eingearbeitet und an das LLM als API-Aufruf weitergegeben werden. Vektoren werden in der Vektordatenbank erstellt, indem die Eingabeabfrage als numerischer Vektor dargestellt wird und mit dem gespeicherten numerischen Vektor verglichen wird. Die Abruffunktion würde N-ähnliche Datensätze zurückgeben. Hier können Sie mehr darüber lesen, wie Sie Ähnlichkeiten zwischen Vektor-Embeddings hier speichern und abrufen können.

Das Speichern und Indizieren von Geschäftskontextkategorien, Datenbankschemas und Benutzerfeedback effizient in einer Vektordatenbank ist, wo die Magie des Datenengineerings geschehen sollte: Ob die Datenquellen PDFs, Word-Dokumente, Textdateien oder Datenbanken sind, sie können in eine zentrale Datenbank aufgenommen werden, in der sie strukturiert, indiziert und durchsuchbar gemacht werden.

Fachspezifisches Wissen kann in einem Wissensgraphen gespeichert werden, in dem Entitäten und ihre Beziehungen als Knoten und Kanten dargestellt werden. Historisch verwendete Abfragen können in einer Abfragetabelle gespeichert werden, in der sie kategorisiert und nach ihrem Zweck und ihrer Nutzung gekennzeichnet werden. Die Logik der Datenumwandlung und -anreicherung kann in einer Regelbuchdatenbank gespeichert werden. Als Dateningenieur streben wir danach, Systeme zu bauen, die nicht nur effizient, sondern auch flexibel und reaktionsschnell auf Veränderungen sind. Durch die Nutzung der Power von ETL (Extract, Transform, Load)-Pipelines können wir dynamische Prozesse erstellen, die unsere Geschäftswissensdatenbanken automatisch aktualisieren, wenn neue Einsichten entstehen und sich Geschäftskontexte weiterentwickeln. Dies kann ein Überwachungs-ETL-Pipeline umfassen, die kontinuierlich diese verschiedenen Datenquellen beobachtet, die notwendigen Transformationen anwendet und die verbundenen Datenbanken aktualisiert.

Große Kontextfenster

Google’s Gemini 1.5 hat eine Kontextfenstergröße von 128k Tokens integriert. Jedoch können bereits eine begrenzte Anzahl von Entwicklern ihre verfeinerte Version ausprobieren, die ein Fenster von bis zu 1 Million Tokens enthält. Dies wirft die Frage auf, ob zu diesem Zeitpunkt wirklich ein RAG benötigt wird? Mit einer so großen Fenstergröße können diese Modelle 10-mal mehr Informationen innerhalb einer Eingabeaufforderung festhalten, ohne die Notwendigkeit, relevante Informationen in einer Vektordatenbank abzurufen. Dies kann zu einem effizienteren Inferenz- und Trainingsprozess führen, da der Mittelsmann eines RAG entfällt und das Modell direkten Zugriff auf alle wichtigen Kontexte hat.

Ich würde jedoch argumentieren, dass ein RAG in diesem Fall weiterhin relevant wäre: Das Ziel besteht darin, gründlich zu verstehen, welchen Kontext das LLM-Modell als „relevant“ betrachtete, als es eine Abfrage generierte. Es könnte nicht ausreichen, eine Abfrage für einen Geschäftsanalysten magisch zu generieren. Das Abrufen des relevanten Kontexts kann zurückgegeben werden, um zu erklären, wie diese bestimmte Abfrage generiert wurde, damit der Mensch sie bewerten kann.

Schlussfolgerung:

Zusammenfassend empfehle ich für einen LLM-SQL-Bot, Ihr eigenes Open Source-Modell (ähnlich dem von DuckDB NSQL) zu trainieren und auch die in diesem Blogbeitrag erwähnten Geschäftskategorien einzubeziehen. Dies stellt sicher, dass das Modell über wichtige Kontexte beim Generieren von Antworten gut informiert ist. Motherduck’s NSQL-7B macht einen guten Job darin, zu validieren, ob die generierte SQL-Anweisung im bereitgestellten Schema funktioniert oder nicht. Dieser Validierungsprozess fungiert im Wesentlichen als Sanitärschritt für Ihre Abfrageanweisungen und hilft, mögliche Fehler oder Unstimmigkeiten zu erkennen, bevor sie Probleme in Ihrer Datenbank verursachen. Abhängig von dem Datenvolumen in der Organisation könnte man entweder entscheiden, alle Informationen direkt in die Eingabeaufforderung aufzunehmen oder eine RAG-Lösung zu erstellen.

Es ist ebenso wichtig, die Bedeutung von sauberen und bedeutungsvollen Daten sowie eine gut gestaltete Eingabeaufforderung zu betonen, die den relevanten Geschäftskontext erfasst. Wenn Organisationen dazu bereit sind, ihren eigenen LLM-SQL-Bot zu erstellen, empfehle ich, die Datenqualität, die Datenerfassung und die Datenbereinigung zu priorisieren. Das Normalisieren der Daten in ein strukturiertes Format hat sich ebenfalls als besseres Format für das LLM erwiesen, um es zu verstehen.

Latest

Portable by design: Rethinking data platforms in the age of digital sovereignty
Portable by design: Rethinking data platforms in the age of digital sovereignty
Portable by design: Rethinking data platforms in the age of digital sovereignty

Portable by design: Rethinking data platforms in the age of digital sovereignty

Build a portable, EU-compliant data platform and avoid vendor lock-in—discover our cloud-neutral stack in this deep-dive blog.

Cloud-Unabhängigkeit: Test eines europäischen Cloud-Anbieters gegen die Giganten
Cloud-Unabhängigkeit: Test eines europäischen Cloud-Anbieters gegen die Giganten
Cloud-Unabhängigkeit: Test eines europäischen Cloud-Anbieters gegen die Giganten

Cloud-Unabhängigkeit: Test eines europäischen Cloud-Anbieters gegen die Giganten

Kann ein europäischer Cloud-Anbieter wie Ionos AWS oder Azure ersetzen? Wir testen es – und finden überraschende Vorteile in Bezug auf Kosten, Kontrolle und Unabhängigkeit.

Hören Sie auf, schlechte Qualitätsdaten zu laden
Hören Sie auf, schlechte Qualitätsdaten zu laden
Hören Sie auf, schlechte Qualitätsdaten zu laden

Vermeide schlechte Daten von Anfang an

Das Erfassen aller Daten ohne Qualitätsprüfungen führt zu wiederkehrenden Problemen. Priorisieren Sie die Datenqualität von Anfang an, um nachgelagerte Probleme zu vermeiden.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Belgien

Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen


USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm,
Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland


© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.