Star Schema vs. Snowflake Schema in PostgreSQL für Analytics

Wenn du für eine PostgreSQL-Analytics-Datenbank zwischen einem Star Schema und einem Snowflake Schema wählen musst, kannst du dich zunächst an einer einfachen Grundregel orientieren: Ein Star Schema eignet sich besonders dann, wenn schnelle Abfragen und möglichst einfache ETL-Prozesse im Mittelpunkt stehen. Ein Snowflake Schema ist dagegen sinnvoller, wenn Speichereffizienz, referenzielle Integrität und häufig veränderte Dimensionsattribute eine größere Rolle spielen.

Beide Ansätze basieren auf dimensionaler Modellierung für OLAP-Workloads. PostgreSQL unterstützt beide Varianten ohne zusätzliche Erweiterungen, und beide lassen sich sowohl in selbst betriebenen als auch in verwalteten PostgreSQL-Umgebungen einsetzen. Im Vergleich mit anderen Datenbanksystemen sind die umfangreichen relationalen Funktionen von PostgreSQL einer der Gründe, weshalb die Datenbank häufig für analytische Anwendungen genutzt wird.

Der wesentliche Unterschied zwischen beiden Modellen liegt im Verhältnis zwischen denormalisierten und normalisierten Dimensionstabellen rund um eine zentrale Faktentabelle. Dieses Tutorial zeigt DDL-Beispiele für beide Varianten anhand desselben Einzelhandelsszenarios, vergleicht gleichwertige Abfragen mit EXPLAIN ANALYZE und erläutert Indexierungs- sowie Konfigurationseinstellungen für analytische PostgreSQL-Workloads.

Du erfährst in diesem Tutorial, wie sich Star- und Snowflake-Schemas in PostgreSQL implementieren lassen, wie du ihre Abfrageleistung mit EXPLAIN ANALYZE vergleichst, wie eine PostgreSQL-Umgebung für Analytics-Workloads konfiguriert werden kann und wie du mithilfe eines wiederverwendbaren Entscheidungsmodells zwischen beiden Schemaformen auswählst.

Wichtige Erkenntnisse

  • Ein Star Schema speichert Dimensionsattribute in denormalisierten und flachen Tabellen. Dadurch werden weniger Joins benötigt und Aggregationsabfragen sind häufig schneller, allerdings entstehen redundante Daten.
  • Ein Snowflake Schema verteilt Dimensionshierarchien auf normalisierte Lookup-Tabellen. Dadurch sinken Speicherbedarf und mögliche Update-Anomalien, während Abfragen zusätzliche Joins benötigen.
  • PostgreSQL kann bei beiden Schemaformen Hash Joins für Dimensions-Lookups verwenden. Die Einstellung work_mem wirkt sich unmittelbar auf die Performance aus, wenn Snowflake-Abfragen mit mehreren Joins mehr Arbeitsspeicher benötigen als verfügbar ist.
  • Vergleichbare Abfragen in einem Star Schema benötigen üblicherweise weniger Hash Joins als in einem Snowflake Schema. Mit wachsender Faktentabelle und einem im Verhältnis zu den Zwischenergebnissen niedrigen work_mem wird der Performance-Unterschied deutlicher.
  • BRIN-Indizes auf fortlaufenden Datumsspalten können bei großen, ausschließlich ergänzten Analytics-Faktentabellen deutlich kleiner sein und weniger Wartungsaufwand verursachen als B-Tree-Indizes, wenn die Werte mit der physischen Einfügereihenfolge korrelieren.
  • Viele PostgreSQL-Umgebungen erlauben die Konfiguration von Einstellungen wie work_mem und max_parallel_workers_per_gather, die Planung und Ausführung von Analytics-Abfragen mit mehreren Joins beeinflussen.
  • Mit dem später beschriebenen Entscheidungsmodell kannst du die passende Schemaform anhand der Größe der Faktentabelle, der verwendeten Abfragen und der Reife des ETL-Prozesses auswählen.

Voraussetzungen

Bevor du mit diesem Tutorial beginnst, benötigst du:

  • Eine selbst betriebene oder verwaltete PostgreSQL-Umgebung mit PostgreSQL 15 oder neuer. PostgreSQL 14 kann ebenfalls eingesetzt werden, sofern diese Version in deiner Umgebung noch unterstützt wird.
  • Eine installierte psql-Anwendung mit einer bestehenden Verbindung zum PostgreSQL-Server.
  • Eine Datenbank mit dem Namen analytics: CREATE DATABASE analytics;
  • Grundkenntnisse der SQL-Syntax für SELECT, JOIN und GROUP BY.

Was ist ein Star Schema in PostgreSQL?

Bei einem Star Schema werden analytische Daten in einer zentralen Faktentabelle organisiert, um die mehrere flache Dimensionstabellen angeordnet sind. Die Bezeichnung leitet sich von der Form eines Entity-Relationship-Diagramms ab: Die Faktentabelle befindet sich im Zentrum, während die Dimensionstabellen sternförmig nach außen zeigen.

ER-Diagramm eines Star Schemas: fact_order_items befindet sich im Zentrum und ist über Viele-zu-eins-Beziehungen mit den flachen Dimensionstabellen dim_dates, dim_customers und dim_products verbunden.

Zentrale Bestandteile: Faktentabellen und flache Dimensionstabellen

Ein Star Schema besteht aus einer zentralen Faktentabelle und mehreren flachen Dimensionstabellen. Die Faktentabelle enthält messbare Ereignisse wie Bestellpositionen, Seitenaufrufe oder Sensormessungen. In den Dimensionstabellen befinden sich die beschreibenden Informationen zu diesen Ereignissen, beispielsweise Produkteigenschaften, Kundendaten oder Datumshierarchien.

Dimensionstabellen werden innerhalb eines Star Schemas denormalisiert gespeichert. Sämtliche Merkmale eines bestimmten Objekts, beispielsweise eines Produkts, befinden sich in einer einzigen Tabelle. Dazu gehören beispielsweise sowohl subcategory_name als auch category_name. Eine Abfrage zur Berechnung des Umsatzes nach Produktkategorie muss deshalb lediglich die Faktentabelle mit einer einzigen Produktdimension verbinden. Die Bezeichnung „Star Schema“ beschreibt genau diese Struktur mit der Faktentabelle im Mittelpunkt und den darum angeordneten Dimensionstabellen.

DDL-Beispiel für ein Star Schema im Einzelhandel

CREATE TABLE dim_dates (
    date_key      INTEGER PRIMARY KEY,
    full_date     DATE        NOT NULL,
    day_of_week   VARCHAR(10) NOT NULL,
    month         INTEGER     NOT NULL,
    quarter       INTEGER     NOT NULL,
    year          INTEGER     NOT NULL
);
CREATE TABLE dim_customers (
    customer_key  INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    customer_id   INTEGER      NOT NULL UNIQUE,
    first_name    VARCHAR(100) NOT NULL,
    last_name     VARCHAR(100) NOT NULL,
    email         VARCHAR(255) NOT NULL,
    city          VARCHAR(100),
    country       VARCHAR(100)
);
CREATE TABLE dim_products (
    product_key       INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    product_id        INTEGER      NOT NULL UNIQUE,
    product_name      VARCHAR(255) NOT NULL,
    subcategory_name  VARCHAR(100) NOT NULL,
    category_name     VARCHAR(100) NOT NULL
);
CREATE TABLE fact_order_items (
    order_item_key  BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    order_id        INTEGER       NOT NULL,
    date_key        INTEGER       NOT NULL REFERENCES dim_dates(date_key),
    customer_key    INTEGER       NOT NULL REFERENCES dim_customers(customer_key),
    product_key     INTEGER       NOT NULL REFERENCES dim_products(product_key),
    quantity        INTEGER       NOT NULL,
    unit_price      NUMERIC(10,2) NOT NULL,
    discount        NUMERIC(5,2)  NOT NULL DEFAULT 0,
    revenue         NUMERIC(12,2) NOT NULL
);

Die Tabelle dim_products speichert subcategory_name und category_name unmittelbar in jedem einzelnen Produktdatensatz. Wird der Name einer Kategorie geändert, müssen deshalb alle betroffenen Einträge in dim_products entsprechend aktualisiert werden.

Wann ein Star Schema die richtige Wahl ist

Für viele Analytics-Workloads ist ein Star Schema eine sinnvolle Standardlösung. Führt eine BI-Anwendung hauptsächlich Aggregationen über jeweils eine Dimension aus, ermöglicht die flache Struktur vergleichsweise einfache SQL-Abfragen, ohne dass zusätzliche Hierarchieebenen durchlaufen werden müssen. BI-Werkzeuge, die SQL automatisch generieren, beispielsweise Metabase oder Looker, erzeugen bei Star-Schemas häufig übersichtlichere Ausführungspläne als bei stark normalisierten Datenmodellen.

Ein weiterer wichtiger Faktor ist die Reife der ETL-Pipeline. Da die Daten bereits vor dem Laden denormalisiert werden müssen, sollte die dafür zuständige Pipeline zuverlässig getestet sein. Arbeitet der ETL-Prozess stabil und ändern sich Dimensionsattribute in den Quellsystemen nur selten, bietet die einfachere Aktualisierung eines Snowflake Schemas möglicherweise nur geringe Vorteile. Ein Star Schema ist deshalb häufig die bessere Ausgangsbasis, wenn Einfachheit wichtiger ist als maximale Speichereffizienz.

Was ist ein Snowflake Schema in PostgreSQL?

Ein Snowflake Schema verwendet grundsätzlich denselben Typ von Faktentabelle, normalisiert jedoch die Dimensionen in zusätzliche Lookup-Tabellen. Dadurch werden weniger wiederholte Zeichenketten gespeichert, gleichzeitig steigt die Anzahl der benötigten Joins innerhalb analytischer Abfragen.

ER-Diagramm eines Snowflake Schemas: fact_order_items ist mit dim_products verbunden, dim_products wiederum mit product_subcategory und diese Tabelle anschließend mit product_category. Dadurch entsteht eine normalisierte Dimensionshierarchie mit drei Ebenen.

Wie Normalisierung Dimensionstabellen erweitert

Ein Snowflake Schema normalisiert die Dimensionstabellen. Statt category_name direkt innerhalb von dim_products zu speichern, verweist die Produkttabelle auf product_subcategory. Diese Tabelle referenziert wiederum product_category. Die funktionale Abhängigkeit zwischen subcategory_name und category_name wird dadurch aus dim_products entfernt.

Der Nachteil dieser Struktur liegt in einer höheren Abfragekomplexität. Für eine Aggregation auf Kategorieebene muss PostgreSQL nun die Tabellen fact_order_items, dim_products, product_subcategory und product_category miteinander verknüpfen, anstatt lediglich eine einzige Produktdimension anzubinden.

DDL-Beispiel für ein Snowflake Schema mit demselben Einzelhandelsszenario

CREATE TABLE product_category (
    category_key   INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    category_name  VARCHAR(100) NOT NULL
);
CREATE TABLE product_subcategory (
    subcategory_key   INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    subcategory_name  VARCHAR(100) NOT NULL,
    category_key      INTEGER      NOT NULL REFERENCES product_category(category_key)
);
CREATE TABLE dim_dates (
    date_key      INTEGER PRIMARY KEY,
    full_date     DATE        NOT NULL,
    day_of_week   VARCHAR(10) NOT NULL,
    month         INTEGER     NOT NULL,
    quarter       INTEGER     NOT NULL,
    year          INTEGER     NOT NULL
);
CREATE TABLE dim_customers (
    customer_key  INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    customer_id   INTEGER      NOT NULL UNIQUE,
    first_name    VARCHAR(100) NOT NULL,
    last_name     VARCHAR(100) NOT NULL,
    email         VARCHAR(255) NOT NULL,
    city          VARCHAR(100),
    country       VARCHAR(100)
);
CREATE TABLE dim_products (
    product_key      INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    product_id       INTEGER      NOT NULL UNIQUE,
    product_name     VARCHAR(255) NOT NULL,
    subcategory_key  INTEGER      NOT NULL REFERENCES product_subcategory(subcategory_key)
);
CREATE TABLE fact_order_items (
    order_item_key  BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    order_id        INTEGER       NOT NULL,
    date_key        INTEGER       NOT NULL REFERENCES dim_dates(date_key),
    customer_key    INTEGER       NOT NULL REFERENCES dim_customers(customer_key),
    product_key     INTEGER       NOT NULL REFERENCES dim_products(product_key),
    quantity        INTEGER       NOT NULL,
    unit_price      NUMERIC(10,2) NOT NULL,
    discount        NUMERIC(5,2)  NOT NULL DEFAULT 0,
    revenue         NUMERIC(12,2) NOT NULL
);

Wann ein Snowflake Schema die richtige Wahl ist

Die zusätzliche Komplexität eines Snowflake Schemas kann sich auszahlen, wenn sich Werte innerhalb der Dimensionstabellen regelmäßig verändern. Wird beispielsweise ein Produktkatalog vierteljährlich neu strukturiert, ist die Aktualisierung eines einzigen Datensatzes in product_category deutlich günstiger als ein Batch-Update über 20.000 Produktzeilen. Bei größeren Datenmengen kann dieser Unterschied beim Wartungsaufwand erheblich werden.

Zwei weitere Einsatzbereiche sind gemeinsam genutzte Dimensionen und Anforderungen an Data Governance. Wird dieselbe Tabelle product_category beispielsweise von einer Umsatz-Faktentabelle und einer Bestands-Faktentabelle verwendet, verhindert eine gemeinsam normalisierte Lookup-Tabelle abweichende Kategorienamen zwischen beiden Bereichen. Wenn die referenzielle Integrität direkt durch die Datenbank und nicht ausschließlich auf Anwendungsebene durchgesetzt werden soll, übernimmt die Foreign-Key-Kette eines Snowflake Schemas diese Aufgabe automatisch.

Star Schema vs. Snowflake Schema: Direkter Vergleich

Der praktische Unterschied zwischen beiden Modellen besteht vor allem darin, an welcher Stelle zusätzliche Kosten entstehen. Bei einem Snowflake Schema fällt ein größerer Teil des Aufwands während der Abfrageausführung an, während ein Star Schema mehr Verarbeitung in den ETL-Prozess verlagert. Die folgenden Abschnitte vergleichen diese Unterschiede anhand wichtiger Kriterien für den produktiven Einsatz.

Seitlicher Vergleich eines Star Schemas auf der linken und eines Snowflake Schemas auf der rechten Seite anhand desselben Einzelhandelsszenarios. Das Star Schema verwendet flache Dimensionstabellen, während die Produktdimension im Snowflake Schema auf drei normalisierte Hierarchieebenen verteilt ist.

Abfragekomplexität und Anzahl der Joins

Bei einem Star Schema benötigt eine Abfrage, die fact_order_items mit dim_products und dim_dates verbindet, lediglich zwei Joins, um Umsätze nach Kategorie und Quartal zu berechnen. Die entsprechende Snowflake-Abfrage verwendet dagegen vier Joins: von fact_order_items zu dim_products, von dim_products zu product_subcategory, von product_subcategory zu product_category sowie von fact_order_items zu dim_dates.

PostgreSQL verarbeitet zusätzliche Hash Joins effizient, solange kleinere Dimensionstabellen vollständig im Arbeitsspeicher gehalten werden können. Dennoch erhöht jeder weitere Join den Planungsaufwand und bei steigenden Datenmengen auch das Risiko einer ungünstigen Join-Reihenfolge.

Speicherbedarf und Datenredundanz

Der Unterschied beim Speicherbedarf lässt sich mit einem einfachen Beispiel verdeutlichen. Enthält dim_products 20.000 Zeilen und werden category_name sowie subcategory_name in jeder Zeile als VARCHAR(100) gespeichert, benötigen allein diese beiden Attribute ungefähr 4 MB Speicher. Werden dieselben Informationen stattdessen in einer product_category-Tabelle mit fünf Datensätzen und einer product_subcategory-Tabelle mit 2.800 Datensätzen gespeichert, sinkt der Speicherbedarf auf weniger als 50 KB. Für diese beiden Attribute entspricht das einer Reduzierung von ungefähr 98 Prozent.

In der Praxis entscheidet dieser Unterschied allein nur selten über die Wahl der Schemaform. Die Faktentabelle verwendet in beiden Varianten ganzzahlige Fremdschlüssel, sodass sich die eigentliche Speicherdifferenz auf die Dimensionstabellen und nicht auf fact_order_items konzentriert. Bedeutender werden die Einsparungen bei Dimensionstabellen mit mehreren hunderttausend Zeilen und mehreren VARCHAR-Attributen mit geringer Kardinalität, wie sie häufig bei Produkt- oder geografischen Hierarchien vorkommen.

ETL-Pipeline und Komplexität beim Laden der Daten

Beim Laden eines Star Schemas ist die ETL-Pipeline für die Denormalisierung verantwortlich. Bevor Daten nach dim_products geschrieben werden können, muss der ETL-Prozess die Produktdaten aus dem Quellsystem mit Kategorie- und Unterkategorieinformationen verbinden und daraus eine einzelne flache Zeile erzeugen. Bei einer ausgereiften und zuverlässig getesteten Pipeline ist dieser Aufwand gut kontrollierbar. Bei einer fehleranfälligen Pipeline können jedoch inkonsistente Kategorien unbemerkt in der Dimensionstabelle landen.

Ein Snowflake Schema verschiebt einen Teil dieses Aufwands in die Datenstruktur. Normalisierte Tabellen ähneln häufig stärker den Strukturen der Quellsysteme, sodass beim Laden der Dimensionen weniger Transformationen erforderlich sein können. Inkrementelle Ladeprozesse müssen jedoch sicherstellen, dass product_category, product_subcategory und dim_products in der richtigen Reihenfolge konsistent aktualisiert werden. Foreign-Key-Constraints erkennen ungültige Beziehungen, beseitigen aber nicht den zusätzlichen Koordinationsaufwand.

Wartung und Update-Anomalien

Ein Star Schema kann anfällig für Update-Anomalien sein. Ändert sich beispielsweise der Name einer Produktkategorie, muss jede Zeile in dim_products, die diesen category_name enthält, angepasst werden. Bei Slowly Changing Dimensions vom Typ 1 kann dies umfangreiche Batch-Updates über große Dimensionstabellen erforderlich machen.

Ein normalisiertes Snowflake Schema vermeidet dieses Problem für ausgelagerte Attribute. Wird category_name in einem einzelnen Datensatz von product_category geändert, gilt der neue Wert über die Fremdschlüsselbeziehungen automatisch für sämtliche damit verbundenen Produkte.

Vergleichstabelle

Schema Abfragegeschwindigkeit Speichereffizienz Join-Komplexität ETL-Komplexität Geeignete Einsatzbereiche
Star Schema Bei Aggregationen häufig schneller, da weniger Joins erforderlich sind Geringere Effizienz, da Dimensionsattribute mehrfach gespeichert werden Niedrig; typische analytische Abfragen benötigen häufig 1 bis 2 Joins Höher; die Pipeline muss Daten vor dem Laden denormalisieren BI-Berichte, Dashboards und weitgehend stabile Dimensionen
Snowflake Schema Bei großen Datenmengen potenziell langsamer, da zusätzliche Joins pro Hierarchieebene entstehen Höher; normalisierte Attribute werden nur einmal gespeichert Höher; Abfragen über Hierarchien können 3 bis 5 Joins erfordern Geringere Transformationsanforderungen, da das Schema stärker normalisierten Quellsystemen entspricht Data-Governance-Umgebungen, häufig aktualisierte Dimensionen und gemeinsam genutzte Lookup-Tabellen

Testdaten für reproduzierbare Benchmarks erzeugen

Die im nächsten Abschnitt gezeigten Ergebnisse von EXPLAIN ANALYZE wurden mit einem produktionsnahen Datensatz erzeugt, dessen Datumsbereich größer war als beim nachfolgenden Generator. Mit den folgenden generate_series-Abfragen kannst du in der Datenbank analytics einen funktional vergleichbaren Datensatz für Schema-Validierungen und Performance-Vergleiche erstellen. Die tatsächlichen Zeilenschätzungen und Ausführungszeiten können abweichen, die relativen Unterschiede zwischen Star- und Snowflake-Schema bleiben bei vergleichbaren Datenmengen jedoch nachvollziehbar.

Fülle dim_dates für jeden Kalendertag zwischen 2020 und 2025 mit einem Datensatz. Dabei entstehen 2.192 Zeilen:

INSERT INTO dim_dates (date_key, full_date, day_of_week, month, quarter, year)
SELECT
    TO_CHAR(d, 'YYYYMMDD')::INTEGER,
    d::DATE,
    TO_CHAR(d, 'FMDay'),
    EXTRACT(MONTH FROM d)::INTEGER,
    EXTRACT(QUARTER FROM d)::INTEGER,
    EXTRACT(YEAR FROM d)::INTEGER
FROM generate_series('2020-01-01'::DATE, '2025-12-31'::DATE, '1 day') d;

Fülle dim_customers mit 50.000 Datensätzen:

INSERT INTO dim_customers (customer_id, first_name, last_name, email, city, country)
SELECT
    i,
    'First' || i,
    'Last' || i,
    'customer' || i || '@example.com',
    (ARRAY['New York','San Francisco','Chicago','Austin','Seattle'])[1 + (i % 5)],
    'US'
FROM generate_series(1, 50000) i;

Fülle dim_products für das Star Schema mit 20.000 Datensätzen:

INSERT INTO dim_products (product_id, product_name, subcategory_name, category_name)
SELECT
    i,
    'Product ' || i,
    (ARRAY['Laptops','Tablets','Phones','Monitors','Accessories',
           'Chairs','Desks','Shelves','Lamps','Rugs',
           'Jackets','Shirts','Pants','Shoes','Hats',
           'Bats','Balls','Nets','Gloves','Helmets',
           'Pans','Knives','Bowls','Plates','Cups'])[1 + (i % 25)],
    (ARRAY['Electronics','Furniture','Clothing','Sports','Kitchen'])[1 + (i % 5)]
FROM generate_series(1, 20000) i;

Fülle fact_order_items mit ungefähr 2,7 Millionen Datensätzen:

INSERT INTO fact_order_items (order_id, date_key, product_key, customer_key, quantity, unit_price, discount, revenue)
SELECT
    (random() * 500000 + 1)::INTEGER,
    TO_CHAR('2020-01-01'::DATE + floor(random() * 2192)::INTEGER, 'YYYYMMDD')::INTEGER,
    (random() * 19999 + 1)::INTEGER,
    (random() * 49999 + 1)::INTEGER,
    v.quantity,
    v.unit_price,
    v.discount,
    ROUND((v.quantity * v.unit_price) * (1 - v.discount), 2) AS revenue
FROM generate_series(1, 2700000)
CROSS JOIN LATERAL (
    SELECT
        (random() * 10 + 1)::INTEGER          AS quantity,
        (random() * 500 + 10)::NUMERIC(10,2) AS unit_price,
        0::NUMERIC(5,2)                      AS discount
) v;

Führe nach dem Laden ANALYZE aus, damit PostgreSQL die Statistiken des Query-Planners vor den Benchmarks aktualisiert:

psql -d analytics -c "ANALYZE dim_dates, dim_customers, dim_products, fact_order_items;"

Die Definitionen von dim_products unterscheiden sich zwischen Star- und Snowflake-Schema und verwenden nicht dieselben Spalten. Gleichzeitig verweist fact_order_items auf die jeweils vorhandene Variante. Deshalb solltest du die beiden Modelle entweder in separaten Datenbanken testen oder fact_order_items und dim_products entfernen und anhand der Snowflake-DDL neu erstellen, bevor du die folgenden Anweisungen ausführst. Werden beide Schemaformen ohne Neuerstellung der Tabellen in derselben Datenbank verwendet, schlägt das Snowflake-INSERT fehl, weil darin die im Star Schema benötigten Spalten subcategory_name und category_name fehlen.

Für den Snowflake-Benchmark werden die normalisierten Dimensionstabellen mit derselben Struktur aus fünf Kategorien und insgesamt 2.800 Unterkategorien gefüllt. Pro Kategorie werden dabei 560 Unterkategorien angelegt:

INSERT INTO product_category (category_name)
VALUES ('Electronics'), ('Furniture'), ('Clothing'), ('Sports'), ('Kitchen');
INSERT INTO product_subcategory (subcategory_name, category_key)
SELECT
    pc.category_name || ' Sub ' || s,
    pc.category_key
FROM product_category pc
CROSS JOIN generate_series(1, 560) s;
INSERT INTO dim_products (product_id, product_name, subcategory_key)
SELECT
    i,
    'Product ' || i,
    (SELECT subcategory_key
     FROM product_subcategory
     ORDER BY subcategory_key
     OFFSET (i % 2800) LIMIT 1)

Aktualisiere anschließend die Statistiken der normalisierten Dimensionstabellen:

psql -d analytics -c "ANALYZE product_category, product_subcategory;"

PostgreSQL-Abfrageperformance: Was die Messwerte zeigen

Die Schemawahl beeinflusst Ausführungspläne in zwei direkt messbaren Bereichen: bei der Anzahl der Joins und beim Speicherverbrauch der Hash-Tabellen. Die folgenden Beispiele mit EXPLAIN ANALYZE verwenden dieselbe Faktentabelle mit ungefähr 2,7 Millionen Zeilen, sodass sich die Unterschiede unmittelbar miteinander vergleichen lassen.

EXPLAIN ANALYZE für eine Aggregationsabfrage im Star Schema

Die folgenden Ergebnisse von EXPLAIN ANALYZE wurden in einer PostgreSQL-15-Umgebung mit 4 vCPUs, 8 GB Arbeitsspeicher und einem auf 64 MB gesetzten work_mem erzeugt. Die tatsächlichen Zeilenschätzungen und Ausführungszeiten hängen von Tabellenstatistiken, PostgreSQL-Version, verfügbarer Hardware, Arbeitsspeicher und Konfiguration ab. Der relative Unterschied zeigt jedoch das typische Verhalten einer denormalisierten gegenüber einer normalisierten Dimensionshierarchie bei dieser Datenmenge.

Die Abfrage aggregiert den Umsatz nach Produktkategorie und Quartal auf einer Tabelle fact_order_items mit ungefähr 2,7 Millionen Zeilen.

EXPLAIN ANALYZE
SELECT
    dp.category_name,
    dd.year,
    dd.quarter,
    SUM(foi.revenue)             AS total_revenue,
    COUNT(DISTINCT foi.order_id) AS order_count
FROM fact_order_items foi
JOIN dim_products dp ON foi.product_key = dp.product_key
JOIN dim_dates    dd ON foi.date_key    = dd.date_key
WHERE dd.year = 2023
  AND dp.category_name = 'Electronics'
GROUP BY dp.category_name, dd.year, dd.quarter
ORDER BY dd.quarter;

HashAggregate  (cost=84321.50..84325.80 rows=16 width=48)
               (actual time=412.344..412.591 rows=4 loops=1)
  Group Key: dp.category_name, dd.year, dd.quarter
  ->  Hash Join  (cost=1628.90..82944.30 rows=89918 width=32)
                 (actual time=20.344..387.801 rows=89918 loops=1)
        Hash Cond: (foi.date_key = dd.date_key)
        ->  Hash Join  (cost=1592.00..71308.20 rows=540000 width=28)
                       (actual time=18.211..298.112 rows=540000 loops=1)
              Hash Cond: (foi.product_key = dp.product_key)
              ->  Seq Scan on fact_order_items foi
                    (cost=0.00..52130.00 rows=2700000 width=24)
                    (actual time=0.021..142.430 rows=2700000 loops=1)
              ->  Hash  (cost=1592.00..1592.00 rows=4000 width=20)
                    (actual time=10.411..10.412 rows=4000 loops=1)
                    Buckets: 4096  Batches: 1  Memory Usage: 309kB
                    ->  Seq Scan on dim_products dp
                          (cost=0.00..1592.00 rows=4000 width=20)
                          (actual time=0.011..6.322 rows=4000 loops=1)
                          Filter: (category_name = 'Electronics')
                          Rows Removed by Filter: 16000
        ->  Hash  (cost=36.90..36.90 rows=365 width=12)
                  (actual time=2.511..2.512 rows=365 loops=1)
              Buckets: 1024  Batches: 1  Memory Usage: 26kB
              ->  Seq Scan on dim_dates dd
                    (cost=0.00..36.90 rows=365 width=12)
                    (actual time=0.009..1.234 rows=365 loops=1)
                    Filter: (year = 2023)
                    Rows Removed by Filter: 1827
Planning Time: 2.341 ms
Execution Time: 413.019 ms

PostgreSQL entscheidet sich bei beiden Dimensions-Lookups für Hash Joins. Die Bedingung für category_name wird gegen den im Arbeitsspeicher abgelegten Hash von dim_products geprüft. Gleichzeitig werden alle 2,7 Millionen Datensätze der Faktentabelle genau einmal gelesen.

EXPLAIN ANALYZE für die entsprechende Snowflake-Abfrage

EXPLAIN ANALYZE
SELECT
    pc.category_name,
    dd.year,
    dd.quarter,
    SUM(foi.revenue)             AS total_revenue,
    COUNT(DISTINCT foi.order_id) AS order_count
FROM fact_order_items    foi
JOIN dim_products        dp ON foi.product_key     = dp.product_key
JOIN product_subcategory ps ON dp.subcategory_key = ps.subcategory_key
JOIN product_category    pc ON ps.category_key     = pc.category_key
JOIN dim_dates           dd ON foi.date_key        = dd.date_key
WHERE dd.year = 2023
  AND pc.category_name = 'Electronics'
GROUP BY pc.category_name, dd.year, dd.quarter
ORDER BY dd.quarter;

HashAggregate  (cost=99812.40..99816.70 rows=16 width=48)
               (actual time=498.712..498.981 rows=4 loops=1)
  Group Key: pc.category_name, dd.year, dd.quarter
  ->  Hash Join  (cost=505.95..98180.20 rows=89918 width=32)
                 (actual time=22.341..471.229 rows=89918 loops=1)
        Hash Cond: (foi.date_key = dd.date_key)
        ->  Hash Join  (cost=469.05..95100.40 rows=540000 width=28)
                       (actual time=20.114..421.902 rows=540000 loops=1)
              Hash Cond: (ps.category_key = pc.category_key)
              ->  Hash Join  (cost=468.00..88444.30 rows=2700000 width=32)
                             (actual time=12.123..360.112 rows=2700000 loops=1)
                    Hash Cond: (dp.subcategory_key = ps.subcategory_key)
                    ->  Hash Join  (cost=412.00..80305.60 rows=2700000 width=28)
                                   (actual time=8.344..288.112 rows=2700000 loops=1)
                          Hash Cond: (foi.product_key = dp.product_key)
                          ->  Seq Scan on fact_order_items foi
                                (cost=0.00..52130.00 rows=2700000 width=24)
                                (actual time=0.021..142.430 rows=2700000 loops=1)
                          ->  Hash  (cost=412.00..412.00 rows=20000 width=8)
                                (actual time=8.111..8.112 rows=20000 loops=1)
                                Buckets: 32768  Batches: 1  Memory Usage: 940kB
                                ->  Seq Scan on dim_products dp
                                      (cost=0.00..412.00 rows=20000 width=8)
                                      (actual time=0.011..3.902 rows=20000 loops=1)
                    ->  Hash  (cost=56.00..56.00 rows=2800 width=8)
                          (actual time=3.211..3.212 rows=2800 loops=1)
                          Buckets: 4096  Batches: 1  Memory Usage: 142kB
                          ->  Seq Scan on product_subcategory ps
                                (cost=0.00..56.00 rows=2800 width=8)
                                (actual time=0.009..1.512 rows=2800 loops=1)
              ->  Hash  (cost=1.05..1.05 rows=1 width=12)
                        (actual time=0.018..0.019 rows=1 loops=1)
                    Buckets: 1024  Batches: 1  Memory Usage: 9kB
                    ->  Seq Scan on product_category pc
                          (cost=0.00..1.05 rows=1 width=12)
                          (actual time=0.007..0.011 rows=1 loops=1)
                          Filter: (category_name = 'Electronics')
                          Rows Removed by Filter: 4
        ->  Hash  (cost=36.90..36.90 rows=365 width=12)
                  (actual time=2.511..2.512 rows=365 loops=1)
              Buckets: 1024  Batches: 1  Memory Usage: 26kB
              ->  Seq Scan on dim_dates dd
                    (cost=0.00..36.90 rows=365 width=12)
                    (actual time=0.009..1.234 rows=365 loops=1)
                    Filter: (year = 2023)
                    Rows Removed by Filter: 1827
Planning Time: 3.812 ms
Execution Time: 499.621 ms

Die Snowflake-Abfrage benötigt ungefähr 499 ms, während dieselbe Auswertung im Star Schema etwa 413 ms benötigt. Bei denselben 2,7 Millionen Faktendatensätzen entspricht das einem Unterschied von ungefähr 21 Prozent. In diesem Ausführungsplan verbindet PostgreSQL dim_products zunächst mit sämtlichen 2.800 Datensätzen aus product_subcategory, bevor die Einschränkung pc.category_name = 'Electronics' beim Join mit product_category angewendet wird. Abhängig von Statistiken und Konfiguration kann der Query-Planner auch eine andere Reihenfolge wählen, bei der die Kategoriebedingung früher angewendet wird.

Indexstrategien für PostgreSQL-Faktentabellen

Drei Indexierungsstrategien können typische Performance-Engpässe bei fact_order_items reduzieren.

Ein BRIN, also Block Range Index, eignet sich besonders für Spalten, deren Werte mit der physischen Reihenfolge der Datensätze korrelieren. Das ist häufig bei ausschließlich ergänzten Faktentabellen der Fall, die chronologisch geladen werden:

CREATE INDEX idx_fact_order_items_date_brin
    ON fact_order_items USING BRIN (date_key);

Ein partieller Index kann sich ausschließlich auf neuere Zeiträume konzentrieren und dadurch sowohl Speicherverbrauch als auch Wartungsaufwand reduzieren, wenn Auswertungen hauptsächlich aktuelle Daten verwenden:

CREATE INDEX idx_fact_order_items_recent
    ON fact_order_items (date_key, product_key)
    WHERE date_key >= 20240101;

Mit einem Covering Index kann PostgreSQL häufig verwendete Aggregationen direkt aus den Indexdaten beantworten, ohne zusätzlich auf die Heap-Daten zugreifen zu müssen:

CREATE INDEX idx_fact_order_items_covering
    ON fact_order_items (date_key, product_key)
    INCLUDE (revenue, order_id);

Führe nach größeren Ladeprozessen ANALYZE fact_order_items; aus, damit die Optimizer-Statistiken aktualisiert werden. Veraltete Statistiken können dazu führen, dass der Query-Planner ungeeignete Join-Reihenfolgen auswählt, und gehören zu den häufigsten Ursachen unerwarteter Performance-Einbrüche in Analytics-Datenbanken. Führe nach großen Datenimports außerdem VACUUM fact_order_items; aus, damit die Visibility Map aktuell bleibt und PostgreSQL Index-Only-Scans über den Covering Index verwenden kann. Ohne ein aktuelles VACUUM können weiterhin Heap-Zugriffe erforderlich sein, obwohl sämtliche benötigten Spalten bereits im Index enthalten sind.

Analytics-Schema auf PostgreSQL einrichten

Die folgenden Schritte behandeln die Bereitstellung einer PostgreSQL-Umgebung, die Konfiguration wichtiger Analytics-Parameter, die Anwendung des Star- oder Snowflake-Schemas und den Einsatz von Connection Pooling.

PostgreSQL für Analytics-Workloads bereitstellen

Für analytische Anwendungen sollte ein PostgreSQL-Server mit mindestens 4 GB Arbeitsspeicher verwendet werden. Snowflake-Abfragen mit mehreren Joins profitieren besonders von zusätzlichem RAM, da PostgreSQL für Hash- und Sortieroperationen jeweils Speicher über work_mem reservieren kann. Bei überwiegend lesenden Analytics-Workloads kann eine Read Replica eingesetzt werden, um Reporting-Abfragen von transaktionalen Vorgängen auf der primären Datenbank zu trennen.

work_mem und enable_hashjoin für Multi-Join-Abfragen konfigurieren

In PostgreSQL-Umgebungen werden Analytics-relevante Parameter je nach Betriebsmodell entweder direkt über PostgreSQL-Konfigurationsdateien oder über die Konfigurationsoberfläche beziehungsweise API eines verwalteten Datenbankdienstes festgelegt. Besonders relevant für Abfragen mit mehreren Joins sind work_mem, max_parallel_workers_per_gather und enable_hashjoin.

Geeignete Ausgangswerte für eine auf Analytics ausgerichtete Umgebung:

Parameter Empfohlener Ausgangswert Auswirkung
work_mem 256MB Stellt Arbeitsspeicher für einzelne Hash- und Sortieroperationen bereit und kann bei Snowflake-Abfragen mit mehreren Joins das Auslagern auf die Festplatte reduzieren
max_parallel_workers_per_gather 4 Ermöglicht parallele sequenzielle Scans auf großen Faktentabellen
enable_hashjoin on Erlaubt dem Query-Planner weiterhin Hash-Join-Pläne und sollte für Analytics-Workloads in der Regel aktiviert bleiben

Einige verwaltete PostgreSQL-Umgebungen schränken die Verwendung von ALTER SYSTEM ein. In diesem Fall müssen unterstützte Einstellungen über die jeweilige Konfigurationsoberfläche oder eine Verwaltungs-API geändert werden. Ein zu hoher Wert für work_mem kann bei vielen parallel laufenden Verbindungen zu Speicherproblemen führen. Deshalb sollte ein höherer Wert zunächst über eine sitzungsspezifische Einstellung wie SET work_mem = '256MB'; getestet werden.

Für eine Änderung, die ausschließlich für die aktuelle Analytics-Sitzung gilt:

Diese Einstellung gilt ausschließlich für die aktive Verbindung und kann mit PgBouncer verwendet werden, sofern Session Pooling eingesetzt wird.

Star- oder Snowflake-Schema mit psql oder pgAdmin anwenden

Nach dem Aufbau einer Verbindung zur PostgreSQL-Umgebung kannst du die zuvor gezeigten DDL-Anweisungen mit psql ausführen. Dafür muss ein PostgreSQL-Client installiert und für die Verbindung zum Server konfiguriert sein.

psql "postgresql://<username>:<password>@<database-host>:<port>/analytics?sslmode=verify-full&sslrootcert=/path/to/ca-certificate.crt" \
    -f star_schema.sql

In pgAdmin öffnest du das Query Tool, fügst die einzelnen DDL-Anweisungen ein und führst sie aus. Es empfiehlt sich, vor dem Anlegen des Schemas eine eigene Analytics-Datenbank zu erstellen, damit analytische Tabellen von Anwendungsdatenbanken innerhalb derselben PostgreSQL-Umgebung getrennt bleiben.

Tabellenpartitionierung und Zusammenspiel mit dem Schema

Eine Range-Partitionierung nach date_key funktioniert sowohl mit Star- als auch mit Snowflake-Schemas und wird häufig für Faktentabellen mit mehr als 50 Millionen Zeilen eingesetzt. Die Faktentabelle kann beispielsweise nach Jahren oder Quartalen über PARTITION BY RANGE aufgeteilt werden:

CREATE TABLE fact_order_items (
    order_item_key  BIGINT GENERATED BY DEFAULT AS IDENTITY,
    order_id        INTEGER       NOT NULL,
    date_key        INTEGER       NOT NULL,
    product_key     INTEGER       NOT NULL,
    customer_key    INTEGER       NOT NULL,
    quantity        INTEGER       NOT NULL,
    unit_price      NUMERIC(10,2) NOT NULL,
    discount        NUMERIC(5,2)  NOT NULL DEFAULT 0,
    revenue         NUMERIC(12,2) NOT NULL
) PARTITION BY RANGE (date_key);
CREATE TABLE fact_order_items_2024
    PARTITION OF fact_order_items
    FOR VALUES FROM (20240101) TO (20250101);
CREATE TABLE fact_order_items_2025
    PARTITION OF fact_order_items
    FOR VALUES FROM (20250101) TO (20260101);

Ein BRIN-Index auf date_key innerhalb jeder Partition kann den Indexspeicher weiter reduzieren. Jede Partition besitzt ihren eigenen Index und deckt einen kleineren Wertebereich ab als die vollständige Tabelle. Star- und Snowflake-Schemas profitieren gleichermaßen von dieser Partitionierung, da die Struktur der Faktentabelle in beiden Modellen identisch ist. Die Dimensionstabellen werden in diesem Beispiel nicht partitioniert.

PostgreSQL unterstützt deklarative Partitionierung nativ. Partitionstabellen lassen sich sowohl über psql als auch über pgAdmin mit einem Datenbankkonto anlegen, das über die notwendigen Berechtigungen verfügt.

Connection Pooling mit PgBouncer

PgBouncer kann als Connection Pooler für PostgreSQL eingesetzt werden. Für analytische Verbindungen, die von sitzungsspezifischen Einstellungen abhängig sind, eignet sich Session Pooling besser als Transaction Pooling.

Beim Transaction Pooling wird eine Verbindung nach jeder Transaktion wieder an den Pool zurückgegeben. Dadurch können sitzungsspezifische Einstellungen wie SET work_mem oder das Prepared-Statement-Verhalten bestimmter BI-Anwendungen beeinträchtigt werden. Für analytische Abfragen kann deshalb ein eigener Connection Pool im Session-Modus verwendet werden, dessen Poolgröße anhand der erwarteten Anzahl paralleler Analytics-Abfragen festgelegt wird.

Normalisierung vs. Denormalisierung: Der zentrale Zielkonflikt

Keines der beiden Modelle ist grundsätzlich besser. Die richtige Entscheidung hängt davon ab, wie häufig sich Dimensionsattribute ändern, wie viele Faktentabellen dieselben Dimensionen verwenden und welche Datenstruktur die ETL-Pipeline zuverlässig erzeugen kann.

Wann Denormalisierung Analytics-Workloads unterstützt

Durch Denormalisierung sinkt die Anzahl der Tabellen, die für eine analytische Abfrage verarbeitet werden müssen. Bei stark lesenden OLAP-Systemen, deren Daten stündlich oder nachts in Batches geladen und anschließend nur selten verändert werden, ist die redundante Speicherung von Dimensionsattributen häufig ein akzeptabler Preis für einfachere und schnellere Abfragen.

Eine flache Tabelle dim_products kann außerdem für BI-Anwendungen mit eingeschränkter SQL-Optimierung vorteilhaft sein. Einige tabellenbasierte Integrationen und eingebettete Analytics-Lösungen erzeugen einfache Aggregationsabfragen und können mehrstufige Tabellenhierarchien ohne zusätzliche Konfiguration nicht automatisch durchlaufen.

Wann Normalisierung langfristig Speicher- und Update-Kosten reduziert

Normalisierung ist besonders dann sinnvoll, wenn Dimensionsattribute eine geringe Kardinalität besitzen und häufig wiederholt werden. Enthält dim_products beispielsweise 100.000 Zeilen, während category_name nur fünf unterschiedliche Werte annehmen kann, ist es ineffizient, den vollständigen Kategorienamen 100.000-mal zu speichern. Eine separate Tabelle product_category mit fünf Datensätzen und ganzzahligen Fremdschlüsseln in dim_products benötigt deutlich weniger redundanten Speicher.

Auch bei SCD-Type-1-Strukturen bietet die Normalisierung Vorteile. Dabei überschreiben neue Attributwerte die bisherigen Werte. Eine einzelne Aktualisierung in product_category ist günstiger und weniger fehleranfällig als ein Batch-Update über beispielsweise 10.000 Datensätze in dim_products.

Hybride Ansätze mit teilweise normalisierten Dimensionen

Ein hybrides Schema kann häufig geänderte oder mehrfach verwendete Dimensionsattribute normalisieren und gleichzeitig stabile Merkmale in flachen Tabellen belassen. Beispielsweise lassen sich product_category und product_subcategory auslagern, weil sie gelegentlich geändert und von mehreren Faktentabellen gemeinsam verwendet werden. Adressinformationen können dagegen direkt in dim_customers bleiben, da sie überwiegend kundenspezifisch sind und nur selten als analytische Hierarchie verwendet werden.

Ein solcher Mischansatz wird häufig in produktiven PostgreSQL-Data-Warehouses eingesetzt, in denen unterschiedliche Dimensionen verschiedene Aktualisierungsfrequenzen und betriebliche Eigenschaften besitzen.

Star Schema oder Snowflake Schema wählen: Entscheidungsmodell

Die folgenden Kriterien helfen dir sowohl beim Aufbau eines neuen Analytics-Projekts als auch bei der Bewertung eines bestehenden Datenmodells, das möglicherweise mehr betriebliche Komplexität verursacht als notwendig.

Die Grundregel lässt sich folgendermaßen zusammenfassen: Wenn häufig ausgeführte BI-Abfragen eine Dimensionshierarchie auf einer Faktentabelle mit mehr als 50 Millionen Zeilen durchlaufen, kann ein Star Schema gegenüber einem vergleichbaren Snowflake Schema auf derselben PostgreSQL-Umgebung einen messbaren Performance-Vorteil bieten. Unterhalb von ungefähr 10 Millionen Zeilen wird der Unterschied dagegen häufig stärker von Indexqualität und work_mem-Konfiguration bestimmt als von der eigentlichen Schemaform.

Checkliste für die Schemaentscheidung

  • Häufigkeit von Dimensionsänderungen: Wenn sich Dimensionsattribute häufiger als monatlich ändern, kann ein Snowflake Schema Update-Anomalien reduzieren.
  • Größe der Faktentabelle: Unter 10 Millionen Zeilen sind Performance-Unterschiede bei guter Indexierung häufig gering. Oberhalb von 50 Millionen Zeilen solltest du beide Modelle mit repräsentativen Abfragen benchmarken, bevor du dich festlegst.
  • Automatisch erzeugtes SQL durch BI-Tools: Wenn die BI-Plattform Abfragen automatisch generiert, solltest du prüfen, ob mehrstufige Joins effizient erzeugt werden. Star-Schemas sind bei eingeschränkter Query-Optimierung häufig robuster.
  • Gemeinsam verwendete Dimensionen: Wenn dieselben Lookup-Daten in mehreren Faktentabellen verwendet werden, können normalisierte gemeinsame Dimensionstabellen auseinanderlaufende Attributwerte verhindern.
  • Reife der ETL-Pipeline: Denormalisierung setzt einen zuverlässigen und getesteten ETL-Prozess voraus. Befindet sich die Pipeline noch in einer frühen Entwicklungsphase, kann zunächst ein Snowflake Schema verwendet und später denormalisiert werden.
  • SQL-Erfahrung des Teams: Snowflake-Schemas erfordern komplexere Joins. Haben Analysten wenig Erfahrung mit Abfragen über mehrere Tabellen, kann ein Star Schema die Arbeit vereinfachen.

Anzeichen für einen Wechsel zwischen den Schemaformen

Ein Wechsel vom Star Schema zum Snowflake Schema kann sinnvoll sein, wenn Aktualisierungen in Dimensionstabellen aufgrund stark wiederholter Attribute zunehmend teuer werden oder wenn mehrere Faktentabellen dieselben Dimensionsdaten verwenden und die Synchronisierung nicht mehr zuverlässig funktioniert.

Ein Wechsel vom Snowflake Schema zum Star Schema kann sinnvoll sein, wenn EXPLAIN ANALYZE bei häufig ausgeführten BI-Berichten regelmäßig die zusätzlichen Joins als zentralen Performance-Engpass zeigt und die ETL-Pipeline Dimensionen zuverlässig vor dem Laden denormalisieren kann.

Zwischen Star- und Snowflake-Schema ohne Ausfallzeit migrieren

Für eine Migration von einem Snowflake Schema zu einem Star Schema ohne Abschaltung der Datenbank oder Unterbrechung von Lesezugriffen kann ein View-Swap-Verfahren eingesetzt werden. Der Ablauf besteht aus folgenden Schritten:

  1. Erstelle eine denormalisierte View über die bestehende Snowflake-Dimensionshierarchie.
  2. Überprüfe, ob die View dieselben erwarteten Zeilenanzahlen und Spaltenwerte wie die bestehenden Abfragen liefert.
  3. Erstelle eine neue physische Star-Schema-Tabelle mit denselben Spaltennamen und Datentypen wie die View.
  4. Befülle die neue Tabelle innerhalb einer Transaktion aus der View.
  5. Benenne die bisherigen Tabellen innerhalb einer einzelnen Transaktion um und ersetze sie durch die neue Star-Schema-Tabelle.
  6. Entferne die normalisierten Lookup-Tabellen erst, nachdem sichergestellt wurde, dass sämtliche Abfragen korrekt mit dem neuen Schema arbeiten.

Die View bildet während der Migration eine stabile Schnittstelle zwischen dem alten und dem neuen Datenmodell. Bestehende Abfragen können während des Migrationsfensters weiterhin auf die View zugreifen, während der physische Tabellentausch auf DDL-Ebene unmittelbar durchgeführt werden kann.

Häufig gestellte Fragen

Ist ein Star Schema in PostgreSQL immer schneller als ein Snowflake Schema?

Nein. Die Performance hängt unter anderem von der Anzahl der Joins, der Tabellengröße, der Indexabdeckung und der Einstellung work_mem ab. Bei derselben Faktentabelle benötigt ein Star Schema für hierarchieübergreifende Aggregationen weniger Hash Joins als die entsprechende Snowflake-Abfrage und ist deshalb häufig schneller. Bei kleinen Datenmengen kann der Unterschied jedoch kaum messbar sein. PostgreSQL bestimmt die Reihenfolge von Hash Joins anhand der Statistiken, die durch ANALYZE gepflegt werden. Veraltete Statistiken können stärkere Performance-Einbußen verursachen als die Wahl des Schemas selbst. Bei kleinen Datenmengen kann regelmäßiges ANALYZE nach Bulk Loads deshalb wichtiger sein als die Entscheidung zwischen Star und Snowflake.

Kann PostgreSQL Snowflake-Joins auch bei großen Datenmengen effizient verarbeiten?

Bei hohen Zeilenzahlen kann ein zu niedriges work_mem dazu führen, dass Zwischenergebnisse von Hash-Tabellen auf die Festplatte ausgelagert werden. Da Snowflake-Abfragen meist mehr Joins enthalten, steigt die Wahrscheinlichkeit solcher Auslagerungen und damit auch der Performance-Unterschied zu einem Star Schema. Eine Erhöhung von work_mem auf ungefähr 128 bis 256 MB kann Disk-Spilling reduzieren, sofern genügend Gesamtspeicher vorhanden ist. Der tatsächliche Effekt hängt von Größe der Faktentabelle, Anzahl der Joins und Parallelität der Abfragen ab. Nach dem Laden neuer Dimensionsdaten sollte außerdem ANALYZE ausgeführt werden. Veraltete Statistiken können eine ungeeignete Join-Reihenfolge verursachen, was Snowflake-Abfragen aufgrund ihrer größeren Anzahl an Joins stärker beeinträchtigen kann.

Können Star- und Snowflake-Schemas in derselben PostgreSQL-Datenbank verwendet werden?

Ja. Unterschiedliche Fachbereiche innerhalb derselben Datenbank können verschiedene Schemaformen verwenden. Beispielsweise kann der Vertriebsbereich ein Star Schema mit einer flachen Tabelle dim_products verwenden, während der Lagerbereich ein Snowflake Schema mit normalisierten Tabellen product_subcategory und product_category nutzt. Eine normalisierte Tabelle product_category kann außerdem als gemeinsame Dimension dienen, indem eine Fremdschlüsselbeziehung von einer teilweise denormalisierten Produkttabelle darauf verweist. Ein solcher hybrider Ansatz ist sinnvoll, wenn unterschiedliche Bereiche verschiedene Abfragemuster und Aktualisierungsfrequenzen besitzen.

Wie unterscheidet sich der Speicherbedarf von Star- und Snowflake-Schemas in PostgreSQL?

Ein Beispiel verdeutlicht den Unterschied: Eine Tabelle dim_products mit 20.000 Datensätzen und zwei wiederholt gespeicherten VARCHAR(100)-Attributen benötigt für diese Spalten ungefähr 4 MB. Werden dieselben Werte in eine product_category-Tabelle mit fünf Zeilen und eine product_subcategory-Tabelle mit 2.800 Zeilen normalisiert, kann der Speicherbedarf auf weniger als 50 KB sinken. Da die Faktentabelle in beiden Modellen ganzzahlige Fremdschlüssel verwendet, entsteht der Unterschied hauptsächlich in den Dimensionstabellen und nicht in fact_order_items. Bei vielen Analytics-Schemas beträgt die Gesamtersparnis durch normalisierte Dimensionen weniger als ein Prozent des gesamten Datenbankvolumens, sofern die Faktentabelle sinnvoll aufgebaut ist. Größere Einsparungen entstehen vor allem bei Dimensionstabellen mit mehr als ungefähr 500.000 Zeilen und mehreren häufig wiederholten Attributen mit geringer Kardinalität.

Welche PostgreSQL-Indextypen eignen sich für Faktentabellen in einem Star Schema?

BRIN-Indizes eignen sich gut für date_key-Spalten in ausschließlich ergänzten Faktentabellen, wenn die gespeicherten Werte mit der Einfügereihenfolge korrelieren. Ein BRIN-Index kann deutlich kleiner als ein B-Tree-Index auf derselben Spalte sein und verursacht bei großen Inserts weniger Wartungsaufwand. B-Tree-Indizes auf Fremdschlüsselspalten wie product_key und customer_key sind sinnvoll, wenn Abfragen selektiv nach diesen Werten filtern und der Query-Planner Nested-Loop- oder Merge-Joins verwendet. Covering Indizes mit INCLUDE können außerdem Abfragen verbessern, die Umsätze nach date_key und product_key aggregieren. Sind alle benötigten Werte im Index enthalten, kann PostgreSQL auf Heap-Zugriffe verzichten und dadurch den I/O-Aufwand auf großen Faktentabellen reduzieren.

Wie lässt sich ein Snowflake Schema in PostgreSQL ohne Ausfallzeit zu einem Star Schema migrieren?

Verwende dazu den folgenden Ablauf:

  1. Erstelle eine flache View der normalisierten Snowflake-Dimension.
  2. Validiere die neue View anhand bestehender Abfragen.
  3. Erstelle eine neue physische Tabelle für die Star-Dimension.
  4. Befülle die neue Tabelle mit den Daten aus der View.
  5. Benenne die bisherige Tabelle innerhalb einer einzelnen Transaktion um und ersetze sie durch die neue Tabelle.
  6. Entferne die normalisierten Tabellen erst, nachdem überprüft wurde, dass alle Abfragen korrekt mit der neuen Struktur funktionieren.

Erstelle im ersten Schritt eine denormalisierte View der Produktdimension:

CREATE VIEW v_dim_products_flat AS
SELECT dp.product_key, dp.product_id, dp.product_name,
       ps.subcategory_name, pc.category_name
FROM dim_products        dp
JOIN product_subcategory ps ON dp.subcategory_key = ps.subcategory_key
JOIN product_category    pc ON ps.category_key    = pc.category_key;

Erstelle im dritten Schritt die physische Dimensionstabelle für das Star Schema:

CREATE TABLE dim_products_star (
    product_key       INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    product_id        INTEGER      NOT NULL UNIQUE,
    product_name      VARCHAR(255) NOT NULL,
    subcategory_name  VARCHAR(100) NOT NULL,
    category_name     VARCHAR(100) NOT NULL
);

Befülle im vierten Schritt die neue Tabelle mit den Daten aus der View:

INSERT INTO dim_products_star
SELECT product_key, product_id, product_name, subcategory_name, category_name
FROM v_dim_products_flat;

Vergleiche nach dem Backfill die Anzahl der Datensätze in v_dim_products_flat und dim_products_star, bevor du im fünften Schritt den eigentlichen Austausch der Tabellen durchführst.

Unterstützen PostgreSQL-Umgebungen die für Analytics benötigten Konfigurationsänderungen?

Viele PostgreSQL-Umgebungen erlauben die Anpassung wichtiger PostgreSQL-Parameter entweder direkt über die Serverkonfiguration oder über eine Administrationsoberfläche beziehungsweise API. Häufig relevante Einstellungen sind:

  • work_mem: Bestimmt den verfügbaren Speicher pro Sortier- oder Hash-Operation. Eine Erhöhung kann verhindern, dass Snowflake-Abfragen mit vielen Joins Zwischenergebnisse auf die Festplatte auslagern müssen.
  • max_parallel_workers_per_gather: Bestimmt die Anzahl paralleler Worker für unterstützte Scans und Aggregationen. Eine Erhöhung von beispielsweise 2 auf 4 oder 8 kann auf ausreichend großen Servern die Scan-Zeit großer Faktentabellen reduzieren.
  • enable_hashjoin: Ist normalerweise standardmäßig aktiviert und bestimmt, ob PostgreSQL Hash Joins in Ausführungsplänen berücksichtigen darf.

In einigen PostgreSQL-Umgebungen werden Parameter wie shared_buffers automatisch anhand der verfügbaren Serverressourcen festgelegt oder können nur mit erhöhten Berechtigungen geändert werden. Auch die maximal erlaubte Anzahl gleichzeitiger Verbindungen kann durch die verfügbaren Ressourcen oder die Konfiguration der Umgebung begrenzt sein.

Was ist der Unterschied zwischen einem Snowflake Schema und der dritten Normalform in einem Data Warehouse?

Die dritte Normalform, kurz 3NF, ist ein Normalisierungsstandard, der häufig für transaktionale Datenbanken eingesetzt wird, um Update-Anomalien zu vermeiden. Ein Snowflake Schema verwendet ein ähnliches strukturelles Prinzip, wendet die Normalisierung jedoch gezielt auf Dimensionstabellen innerhalb analytischer Datenmodelle an. In einem strikt nach 3NF aufgebauten Transaktionsmodell muss jedes Nichtschlüsselattribut ausschließlich vom Primärschlüssel seiner Tabelle abhängig sein.

Ein analytisches Snowflake Schema bringt dagegen nicht das gesamte Data Warehouse in die dritte Normalform. Die Faktentabelle speichert bewusst additive Kennzahlen wie revenue und quantity zusammen mit mehreren Fremdschlüsseln, weil diese Kennzahlen von einer Kombination verschiedener Dimensionen abhängen und nicht von einem einzelnen Schlüssel. Die Dimensionshierarchien können normalisiert sein, das vollständige Datenmodell entspricht jedoch nicht strikt 3NF. Ziel eines Snowflake Schemas ist daher nicht die vollständige Normalisierung des gesamten Analytics-Modells, sondern die kontrollierte Normalisierung bestimmter Attributhierarchien, bei denen Update-Anomalien oder redundanter Speicher den zusätzlichen strukturellen Aufwand rechtfertigen.

Fazit

Star- und Snowflake-Schemas sollten weniger als konkurrierende Standards verstanden werden, sondern vielmehr als zwei unterschiedliche Punkte innerhalb eines Zielkonflikts zwischen einfachen analytischen Abfragen und stärkerer Normalisierung des Datenmodells. Dieses Tutorial hat beide Varianten vollständig behandelt – von den Tabellenstrukturen bis zu den Ergebnissen von EXPLAIN ANALYZE auf einer Faktentabelle mit ungefähr 2,7 Millionen Datensätzen. Außerdem wurde gezeigt, wie zusätzliche Hash Joins in Snowflake-Abfragen den Ausführungsplan verändern, wie sich Performance-Probleme mit passenden Indizes und Konfigurationen reduzieren lassen und welche Eigenschaften eines Workloads für die eine oder andere Schemaform sprechen.

Mit den gezeigten DDL-Beispielen und dem Entscheidungsmodell kannst du eine PostgreSQL-Analytics-Umgebung vorbereiten, die passende Schemaform für deinen Workload auswählen und Einstellungen wie work_mem, Connection Pooling und Indizes für analytische Abfragen konfigurieren. Die Vergleichstabelle und die Checkliste bieten einen wiederverwendbaren Ansatz, mit dem sich die Schemaentscheidung auch bei wachsendem Datenvolumen oder veränderten Anforderungen erneut bewerten lässt.

Als praktischen nächsten Schritt solltest du die in diesem Tutorial gezeigten EXPLAIN ANALYZE-Abfragen für beide Schemaformen ausführen und anschließend zwei weitere repräsentative Reports testen: eine Aggregation über nur eine Dimension sowie eine gefilterte GROUP BY-Abfrage über mehrere Dimensionen mit einer Datumsbereichsbedingung. Vergleiche jeweils Planungszeit und Ausführungszeit. Dadurch lässt sich feststellen, ob die Schemaform für deinen Workload der entscheidende Performance-Faktor ist oder ob Indexqualität und work_mem-Konfiguration einen größeren Einfluss besitzen. Zusätzlich kann über einem PostgreSQL-Analytics-Schema eine Natural-Language-Abfrageschicht eingesetzt werden, die Text-to-SQL-Abfragen für verbundene Datenbanken erzeugt und Analysten dadurch ermöglicht, beide Schemaformen ohne manuell geschriebenes SQL abzufragen.

Quelle: digitalocean.com

Jetzt 200€ Guthaben sichern

Registrieren Sie sich jetzt in unserer ccloud³ und erhalten Sie 200€ Startguthaben für Ihr Projekt.

Das könnte Sie auch interessieren:

Moderne Hosting Services mit Cloud Server, Managed Server und skalierbarem Cloud Hosting für professionelle IT-Infrastrukturen

Wildcard SSL-Zertifikate mit Certbot & Let’s Encrypt

Security, Tutorial
Vijonavor 44 Minuten Wildcard-SSL-Zertifikate mit Certbot und Let’s Encrypt erstellen und erneuern Für die Ausstellung eines Wildcard-Zertifikats für Namen wie *.example.com mit Certbot und Let’s Encrypt ist immer die ACME-DNS-01-Challenge erforderlich.…