SQL · MySQL · MariaDB · Magento
Denormalisierung vs. Normalisierung:
Reporting-Datenbanken richtig bauen

Wann Normalisierung Reporting ausbremst, wann Denormalisierung die richtige Antwort ist und wie Magento mit Aggregationstabellen das Problem elegant löst.

13 Min. Lesezeit Normalisierung · Reporting · Star Schema MySQL 8 · MariaDB 10.6+

1NF, 2NF, 3NF: Normalformen kurz erklärt

Normalisierung ist ein Prozess zur strukturellen Organisation von Datenbankdaten, um Redundanzen zu minimieren und Datenintegrität zu gewährleisten. Die drei gebräuchlichsten Normalformen bauen aufeinander auf und beschreiben, wie stark die Daten bereinigt wurden. Denormalisierung ist das bewusste Aufbrechen dieser Normalformen zugunsten von Performance oder Einfachheit — ein gezielter Kompromiss, kein Fehler.

1. Normalform (1NF): Jede Tabellenspalte enthält nur atomare (nicht teilbare) Werte. Keine Wiederholungsgruppen — keine mehrwertigen Felder wie "Produkt1, Produkt2, Produkt3" in einer einzigen Spalte. Stattdessen eine Zeile pro Wert.

2. Normalform (2NF): Alle Nicht-Schlüssel-Attribute sind vollständig vom Primärschlüssel abhängig — keine partiellen Abhängigkeiten. Relevant bei zusammengesetzten Primärschlüsseln: Ein Attribut darf nicht nur von einem Teil des PKs abhängen.

3. Normalform (3NF): Keine transitiven Abhängigkeiten. Nicht-Schlüssel-Attribute dürfen nur vom Primärschlüssel abhängen, nicht voneinander. Beispiel: city und country in einer Kundentabelle — wenn country von city abhängt (nicht vom Kunden-PK), verletzt das 3NF. Lösung: city in eine separate Tabelle auslagern.

-- Example: illustrate normalization levels

-- VIOLATES 2NF (partial dependency in composite PK: order_id + product_id):
-- order_id | product_id | product_name | qty_ordered | order_date
-- product_name depends only on product_id, NOT on the composite PK

-- 2NF FIX: separate products table
-- orders: order_id, customer_id, order_date
-- order_items: order_id, product_id, qty_ordered
-- products: product_id, product_name, price

-- VIOLATES 3NF (transitive dependency: city → country):
-- customer_id | email | city | country
-- country depends on city, not directly on customer_id

-- 3NF FIX: separate geographic table
-- customers: customer_id, email, city_id
-- cities: city_id, name, country_id
-- countries: country_id, name

Warum Normalisierung Reporting verlangsamt

Normalisierte Schemas sind optimal für transaktionale Workloads (OLTP): INSERT, UPDATE und DELETE sind schnell, weil Daten nur an einer Stelle stehen und keine Redundanzen aktualisiert werden müssen. Für analytische Workloads (OLAP) — Reports, Dashboards, Aggregationen — sind normalisierte Schemas dagegen oft ein Performance-Problem. Der typische Report-Query benötigt zahlreiche JOINs, um die verteilten Informationen wieder zusammenzuführen.

Ein Magento-Umsatz-Report, der Kunden nach Region gruppiert, benötigt mindestens folgende JOINs: sales_ordercustomer_entitycustomer_address_entitydirectory_region. Das sind vier JOINs für eine einfache Auswertung. Bei Millionen von Bestellungen verbraucht das erhebliche Ressourcen — besonders wenn diese Query täglich dutzende Male für das Admin-Dashboard ausgeführt wird. Das ist die Motivation für Denormalisierung im Reporting-Kontext.

-- Normalized schema: revenue report requires many JOINs
-- (costly for reporting on large Magento datasets)
SELECT
  r.default_name AS region,
  COUNT(so.entity_id) AS orders,
  ROUND(SUM(so.grand_total), 2) AS revenue
FROM sales_order so
INNER JOIN customer_entity ce ON so.customer_id = ce.entity_id
INNER JOIN customer_address_entity cae ON ce.default_billing = cae.entity_id
INNER JOIN directory_region r ON cae.region_id = r.region_id
WHERE so.status = 'complete'
  AND so.created_at >= '2025-01-01'
GROUP BY r.region_id
ORDER BY revenue DESC;
-- For 500k+ orders: can take 5–30 seconds without denormalized reporting table

-- Denormalized version: same result, zero JOINs (from pre-built report table)
SELECT region_name, order_count, revenue
FROM reporting_regional_revenue
WHERE period_date = '2025-01-01'
ORDER BY revenue DESC;
-- Execution time: < 50ms

Pre-Aggregationstabellen: berechnete Ergebnisse speichern

Der einfachste Weg zur Denormalisierung im Reporting-Kontext ist Pre-Aggregation: Statt den aufwändigen Report-Query auf Abruf auszuführen, berechnest du die Ergebnisse vorab und speicherst sie in einer dedizierten Zusammenfassungstabelle. Der Report-Query liest dann nur noch aus dieser kleinen, bereits aggregierten Tabelle — blitzschnell, da sie typischerweise nur einige hundert Zeilen statt Millionen enthält.

Denormalisierung via Pre-Aggregation ist für Metriken sinnvoll, die sich täglich oder stündlich berechnen lassen und für die keine Echtzeit-Aktualität erforderlich ist. Umsatz pro Tag, Bestellungen pro Region pro Woche, Top-Produkte des letzten Monats — all das eignet sich gut. Für Live-Dashboards, die die letzten Minuten zeigen müssen, ist dieser Ansatz weniger geeignet.

-- Create pre-aggregation (summary) table for daily revenue
CREATE TABLE reporting_daily_revenue (
  report_date DATE NOT NULL,
  store_id SMALLINT UNSIGNED NOT NULL,
  order_status VARCHAR(32) NOT NULL,
  order_count INT UNSIGNED DEFAULT 0,
  gross_revenue DECIMAL(20,4) DEFAULT 0,
  avg_order_value DECIMAL(12,4) DEFAULT 0,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (report_date, store_id, order_status)
);

-- Populate nightly via cron (upsert: safe to run multiple times)
INSERT INTO reporting_daily_revenue
  (report_date, store_id, order_status, order_count, gross_revenue, avg_order_value)
SELECT
  DATE(created_at) AS report_date,
  store_id,
  status,
  COUNT(*) AS order_count,
  ROUND(SUM(grand_total), 4) AS gross_revenue,
  ROUND(AVG(grand_total), 4) AS avg_order_value
FROM sales_order
WHERE DATE(created_at) = CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(created_at), store_id, status
ON DUPLICATE KEY UPDATE
  order_count = VALUES(order_count),
  gross_revenue = VALUES(gross_revenue),
  avg_order_value = VALUES(avg_order_value),
  updated_at = NOW();

-- Fast report query (microseconds, not seconds)
SELECT report_date, store_id, SUM(order_count) AS orders, SUM(gross_revenue) AS revenue
FROM reporting_daily_revenue
WHERE report_date BETWEEN '2025-01-01' AND '2025-01-31'
  AND order_status = 'complete'
GROUP BY report_date, store_id
ORDER BY report_date;

Materialized-View-Pattern in MySQL

In Oracle, PostgreSQL und anderen Datenbanken gibt es native Materialized Views — gespeicherte Views, die physische Kopien ihrer Abfrageergebnisse halten und periodisch aktualisiert werden können. MySQL hat keine nativen Materialized Views, aber das Muster lässt sich simulieren: Eine normale Tabelle speichert das "materialisierte" Ergebnis einer komplexen Abfrage, und ein Trigger oder Cron-Job aktualisiert sie, wenn die Quelldaten sich ändern.

Das Materialized-View-Pattern für Denormalisierung ist der professionelle Ansatz für Performance-kritische Reporting-Szenarien. Es entkoppelt die Komplexität der Berechnung vom Abrufzeitpunkt vollständig: Der Report-Query ist immer gleich schnell, unabhängig davon, wie aufwändig die zugrundeliegende Berechnung ist.

-- Materialized View Pattern: simulate with table + refresh procedure

-- Step 1: Create the materialized view table
CREATE TABLE mv_product_sales_summary (
  product_id INT UNSIGNED NOT NULL,
  sku VARCHAR(64),
  total_ordered INT UNSIGNED DEFAULT 0,
  total_revenue DECIMAL(20,4) DEFAULT 0,
  last_ordered_at DATETIME,
  refreshed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (product_id),
  INDEX idx_revenue (total_revenue DESC)
);

-- Step 2: Full refresh procedure (called from nightly cron)
CREATE PROCEDURE refresh_mv_product_sales()
BEGIN
  TRUNCATE TABLE mv_product_sales_summary;

  INSERT INTO mv_product_sales_summary
    (product_id, sku, total_ordered, total_revenue, last_ordered_at)
  SELECT
    soi.product_id,
    soi.sku,
    SUM(soi.qty_ordered) AS total_ordered,
    ROUND(SUM(soi.row_total), 4) AS total_revenue,
    MAX(so.created_at) AS last_ordered_at
  FROM sales_order_item soi
  INNER JOIN sales_order so ON soi.order_id = so.entity_id
  WHERE so.status IN ('complete', 'processing')
    AND soi.parent_item_id IS NULL    -- skip configurable parent rows
  GROUP BY soi.product_id, soi.sku;
END;

-- Step 3: Fast query against the materialized view
SELECT sku, total_ordered, total_revenue
FROM mv_product_sales_summary
ORDER BY total_revenue DESC
LIMIT 20;

Magento Aggregationstabellen

Magento implementiert das Denormalisierungs-Pattern für Reporting bereits eingebaut: Die sales_order_aggregated_*-Tabellen halten voraggregierte Verkaufsmetriken auf Tages-, Monats- und Jahresebene. Diese werden durch Cron-Jobs aktualisiert und von Magento's eingebautem Reports-System gelesen. Das ermöglicht schnelle Admin-Dashboard-Anzeigen ohne aufwändige Live-Aggregationen auf der sales_order-Tabelle mit ihren Millionen von Zeilen.

-- Magento's built-in aggregation tables
SHOW TABLES LIKE '%aggregated%';
-- sales_order_aggregated_created
-- sales_order_aggregated_updated
-- sales_invoice_aggregated
-- sales_refund_aggregated

-- Structure of sales_order_aggregated_created (denormalized daily data)
DESCRIBE sales_order_aggregated_created;
-- period (date), store_id, order_status, orders_count, total_qty_ordered,
-- total_revenue_amount, total_tax_amount, total_shipping_amount, ...

-- Query aggregated table for monthly report (very fast, no raw table joins)
SELECT
  YEAR(period) AS year,
  MONTH(period) AS month,
  store_id,
  SUM(orders_count) AS orders,
  ROUND(SUM(total_revenue_amount), 2) AS revenue
FROM sales_order_aggregated_created
WHERE period >= '2025-01-01'
  AND order_status = 'complete'
GROUP BY YEAR(period), MONTH(period), store_id
ORDER BY year, month;

-- Trigger Magento to refresh aggregation tables via CLI:
-- bin/magento reports:reset-status --reset-type=aggregated_created

Star Schema vs. Snowflake für Analytics

Im Data-Warehouse-Kontext sind Star Schema und Snowflake Schema die gängigen Denormalisierungsansätze für eigene Analytics-Datenbanken. Das Star Schema besteht aus einer zentralen Faktentabelle (z.B. Bestellpositionen mit allen Metriken) und direkt angehängten, bereits denormalisierten Dimensionstabellen (Zeit, Produkt, Kunde, Store). Das Snowflake Schema normalisiert die Dimensionstabellen weiter — weniger Redundanz, aber mehr JOINs im Reporting-Query.

Für Magento-Analytics-Projekte empfehle ich ein einfaches Star Schema in einem separaten Analytics-Schema. Die Faktentabelle enthält pro Bestellposition alle wichtigen Metriken sowie denormalisierte Dimensionen wie Produktkategorie, Kundensegment und Store-Name. Dadurch benötigen die häufigsten Reporting-Queries null bis einen JOIN — und sind damit blitzschnell.

-- Simple Star Schema for Magento analytics (separate analytics schema)
CREATE TABLE analytics.fact_order_items (
  fact_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  -- dimension keys
  order_id INT UNSIGNED NOT NULL,
  product_id INT UNSIGNED NOT NULL,
  customer_id INT UNSIGNED,
  store_id SMALLINT UNSIGNED NOT NULL,
  order_date DATE NOT NULL,
  -- denormalized dimension attributes (no JOIN needed in most reports)
  product_sku VARCHAR(64),
  product_category VARCHAR(255),       -- denormalized from category tree
  customer_segment VARCHAR(50),        -- new/returning/vip (pre-calculated)
  store_name VARCHAR(100),
  -- measures (facts)
  qty_ordered DECIMAL(12,4),
  unit_price DECIMAL(12,4),
  row_total DECIMAL(12,4),
  discount_amount DECIMAL(12,4),
  INDEX idx_date_store (order_date, store_id),
  INDEX idx_product (product_id, order_date),
  INDEX idx_category_date (product_category, order_date)
);

-- Reporting query: zero JOINs needed for common analytics
SELECT
  product_category,
  YEAR(order_date) AS year,
  MONTH(order_date) AS month,
  COUNT(DISTINCT order_id) AS orders,
  SUM(row_total) AS revenue,
  AVG(unit_price) AS avg_unit_price
FROM analytics.fact_order_items
WHERE store_id = 1
  AND order_date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY product_category, YEAR(order_date), MONTH(order_date)
ORDER BY revenue DESC;

Wann Denormalisierung sinnvoll ist

Denormalisierung ist nicht pauschal gut oder schlecht — sie ist ein gezielter Kompromiss. Die Faustregel: Denormalisiere erst, wenn du ein gemessenes Performance-Problem hast, das durch zu viele JOINs auf großen normalisierten Tabellen entsteht, und wenn du die erhöhte Komplexität der Datenpflege akzeptieren kannst. Denormalisierung auf Vorrat, ohne messbares Problem, ist premature optimization.

Sinnvolle Szenarien für Denormalisierung: Read-heavy Analytics mit bekannten, stabilen Query-Patterns. Pre-Aggregation für täglich berechnete Metriken ohne Echtzeit-Anforderung. Dimensionen, die sich selten ändern und hunderte Male pro Tag gejoined werden (z.B. Produktkategorien). Berechnete Felder, die teuer zu berechnen sind und sich nicht real-time ändern müssen (Lifetime-Value eines Kunden, einmal täglich berechnet).

-- Example: customer enrichment table (denormalized, updated nightly)
CREATE TABLE customer_analytics_snapshot (
  customer_id INT UNSIGNED NOT NULL PRIMARY KEY,
  email VARCHAR(255),
  first_order_date DATE,
  last_order_date DATE,
  total_orders INT UNSIGNED DEFAULT 0,
  lifetime_value DECIMAL(20,4) DEFAULT 0,
  avg_order_value DECIMAL(12,4) DEFAULT 0,
  customer_segment ENUM('new','active','at-risk','churned','vip') DEFAULT 'new',
  snapshot_date DATE DEFAULT (CURDATE()),
  INDEX idx_segment (customer_segment),
  INDEX idx_ltv (lifetime_value DESC)
);

-- Nightly rebuild (upsert for idempotency)
INSERT INTO customer_analytics_snapshot
  (customer_id, email, first_order_date, last_order_date,
   total_orders, lifetime_value, avg_order_value, customer_segment)
SELECT
  ce.entity_id,
  ce.email,
  MIN(DATE(so.created_at)),
  MAX(DATE(so.created_at)),
  COUNT(so.entity_id),
  ROUND(SUM(so.grand_total), 4),
  ROUND(AVG(so.grand_total), 4),
  CASE
    WHEN SUM(so.grand_total) > 5000 THEN 'vip'
    WHEN MAX(so.created_at) < DATE_SUB(NOW(), INTERVAL 6 MONTH) THEN 'churned'
    WHEN COUNT(so.entity_id) = 1 THEN 'new'
    ELSE 'active'
  END
FROM customer_entity ce
LEFT JOIN sales_order so ON ce.entity_id = so.customer_id AND so.status = 'complete'
GROUP BY ce.entity_id, ce.email
ON DUPLICATE KEY UPDATE
  last_order_date = VALUES(last_order_date),
  total_orders = VALUES(total_orders),
  lifetime_value = VALUES(lifetime_value),
  customer_segment = VALUES(customer_segment),
  snapshot_date = CURDATE();

JSON-Spalten als semi-strukturierte Denormalisierung

MySQL 5.7+ und MariaDB 10.2+ unterstützen native JSON-Spalten mit JSON-Path-Ausdrücken. JSON-Spalten sind eine Form von Denormalisierung: Statt mehrere zusammenhängende Attribute in separaten normalisierten Tabellen zu speichern, werden sie als JSON-Objekt in einer einzigen Spalte abgelegt. Das reduziert JOINs für bestimmte Query-Patterns, gibt aber strenge Typvalidierung auf und macht Indexierung komplexer.

In Magento gibt es bereits JSON-ähnliche Strukturen in serialisierten Konfigurationsfeldern. Mit echten JSON-Spalten lassen sich flexible Produktattribute speichern, die nicht im starren EAV-Schema stehen sollen. Für Indexierung auf JSON-Feldern sind Generated Columns der Schlüssel: Eine virtuelle generierte Spalte extrahiert einen JSON-Wert und kann indiziert werden wie eine normale Spalte.

-- JSON column for flexible product metadata (semi-structured denormalization)
ALTER TABLE catalog_product_entity
  ADD COLUMN extra_metadata JSON DEFAULT NULL
  COMMENT 'Flexible product metadata (material, certifications, etc.)';

-- Insert JSON data
UPDATE catalog_product_entity
SET extra_metadata = JSON_OBJECT(
  'material', 'Cotton',
  'care_instructions', 'Machine wash 30°C',
  'origin_country', 'DE',
  'certifications', JSON_ARRAY('GOTS', 'OEKO-TEX')
)
WHERE sku = 'SHIRT-001';

-- Query with JSON path expressions
SELECT entity_id, sku,
  JSON_UNQUOTE(extra_metadata->>'$.material') AS material,
  JSON_UNQUOTE(extra_metadata->>'$.origin_country') AS origin
FROM catalog_product_entity
WHERE JSON_EXTRACT(extra_metadata, '$.origin_country') = 'DE'
LIMIT 20;

-- Generated column for indexed JSON field (fast WHERE on JSON value)
ALTER TABLE catalog_product_entity
  ADD COLUMN material_gen VARCHAR(100)
    GENERATED ALWAYS AS (JSON_UNQUOTE(extra_metadata->>'$.material')) VIRTUAL,
  ADD INDEX idx_material (material_gen);

-- Now this query uses the index:
SELECT entity_id, sku FROM catalog_product_entity
WHERE material_gen = 'Cotton';

Analytics-Datenbank für dein Magento-Projekt aufbauen?

Wir entwerfen Analytics-Schemas mit Pre-Aggregation und Materialized Views — für schnelle Reports ohne Mehraufwand im Produktionssystem.

Jetzt Kontakt aufnehmen

Zusammenfassung

  • Normalisierung (1NF/2NF/3NF) vermeidet Redundanzen — optimal für OLTP, problematisch für OLAP-Reporting mit vielen JOINs.
  • Denormalisierung ist ein bewusster Kompromiss: Mehr Redundanz im Tausch für schnellere Lesezugriffe und einfachere Queries.
  • Pre-Aggregationstabellen speichern vorab berechnete Metriken (z.B. Tagesumsatz) — Zugriff in Millisekunden statt Sekunden.
  • MySQL hat keine nativen Materialized Views — simuliere sie mit normalen Tabellen und periodischen Refresh-Prozeduren.
  • Magento nutzt sales_order_aggregated_*-Tabellen als eingebauetes Denormalisierungs-Pattern für Reporting.
  • Star Schema für Analytics: Faktentabelle mit denormalisierten Dimensionen, minimiert JOINs für analytische Queries.
  • JSON-Spalten mit Generated Columns und Indizes als semi-strukturierte Denormalisierung für flexible Attribute.
  • Denormalisierung immer erst nach messbarem Problem und mit bewusstem Kompromiss bei der Datenpflege.

FAQ: Denormalisierung und Reporting-Datenbanken

Wann sollte ich normalisieren, wann denormalisieren?
Normalisiere für operative Daten (Bestellungen, Kunden, Produkte) — OLTP-Workloads profitieren von Normalisierung durch schnelle Writes und Datenintegrität. Denormalisiere für Reporting und Analytics — OLAP-Workloads profitieren von weniger JOINs und vorberechneten Aggregationen. Das Produktionssystem normalisiert halten, für Analytics ein separates denormalisiertes Schema pflegen.
Was ist der Nachteil von Denormalisierung?
Denormalisierung erzeugt Redundanz: Wenn sich ein Originalwert ändert (z.B. ein Produktname), müssen alle denormalisierten Kopien ebenfalls aktualisiert werden. Das erhöht die Komplexität der Datenpflege und das Risiko von Inkonsistenzen, wenn der Update-Prozess fehlschlägt. Für täglich aktualisierte Analytics-Snapshots ist das akzeptabel — für operationale Daten mit Echtzeit-Konsistenzanforderung nicht.
Wie aktualisiere ich Magento's Aggregationstabellen manuell?
In der Magento Admin unter Reports → Statistics → Refresh Statistics. Per CLI: bin/magento reports:reset-status gefolgt von bin/magento cron:run --group=reports. Stelle sicher, dass der Magento-Cron für die reports-Cron-Gruppe korrekt konfiguriert und regelmäßig aktiv ist.
Sind JSON-Spalten in MySQL performant für Analytics?
Direkte JSON-Path-Abfragen ohne Index sind langsam bei großen Tabellen — MySQL muss jeden Wert on-the-fly parsen. Mit Generated Columns und einem Index auf den generierten Wert wird es vergleichbar schnell wie normale Spalten. Für häufig gefilterte JSON-Felder: immer als Generated Column mit Index auslagern.
Was ist der Unterschied zwischen Star Schema und Snowflake Schema?
Im Star Schema sind Dimensionstabellen direkt an die Faktentabelle angehängt und selbst denormalisiert — weniger JOINs, mehr Redundanz. Im Snowflake Schema werden Dimensionen weiter normalisiert — mehr JOINs, weniger Redundanz. Star Schema ist für Reporting-Performance besser; Snowflake für Speichereffizienz und Konsistenz bei sich häufig ändernden Dimensionsdaten.
Wie oft sollte ich Pre-Aggregationstabellen aktualisieren?
Abhängig von der Aktualitätsanforderung: Tagesmetriken stündlich, Monatsmetriken nächtlich. Für Live-Dashboards mit 15-Minuten-Granularität: inkrementelle Updates alle 15 Minuten nur für den aktuellen Tag, historische Perioden täglich. Langzeithistorien (Monats- und Jahresberichte) reichen mit täglicher Vollaktualisierung aus.
Ist ein separates Analytics-Datenbankschema sinnvoll?
Ja. Ein separates Analytics-Schema verhindert, dass aufwändige Report-Queries das Produktionssystem belasten. Bei kleinen bis mittleren Shops genügt ein zweites Schema auf demselben Server. Für größere Systeme: Read-Replica oder dedizierter Analytics-Server. Das Produktionssystem bleibt für OLTP optimiert, die Analytics-Datenbank für OLAP.
Wie verhindere ich inkonsistente Daten in denormalisierten Tabellen?
Baue denormalisierte Tabellen immer als vollständige Rebuilds (TRUNCATE + INSERT), wo die Datenmenge es zulässt. Das ist einfacher zu implementieren und verhindert Inkonsistenz durch halb-abgeschlossene Updates. Für sehr große Tabellen: einen Snapshot-Timestamp nutzen, damit immer klar ist, welche Daten zum letzten vollständigen Refresh gehören.
Kann ich das Materialized-View-Pattern mit einem Trigger automatisieren?
Ja, aber mit Vorsicht. Ein Trigger auf sales_order, der bei jedem INSERT die Aggregationstabelle aktualisiert, hält die Aggregation aktuell, erhöht aber den Write-Overhead auf jede neue Bestellung. Für sehr häufige Writes und große Aggregationen ist ein Batch-Refresh per Cron effizienter als Row-by-Row-Trigger-Updates.
Wann lohnt sich ein dediziertes Data Warehouse statt MySQL-Denormalisierung?
Ab mehreren hundert Millionen Zeilen und dutzenden gleichzeitigen Analysten oder sehr komplexen mehrdimensionalen Analysen lohnt sich ein OLAP-System wie ClickHouse, BigQuery oder Redshift. Diese Systeme sind für spaltenorientierte Analytics optimiert. Für die meisten Magento-Shops bis mittlerer Größe reicht MySQL-Denormalisierung mit Pre-Aggregation vollständig aus.