LERNPFADE & KURSE

QVD-Optimierung – 100x schnellere Loads in Qlik Sense

Autor

Qlik Doktor

Oktober 5, 2025 · 39 Min. Lesezeit

📚 Qlik Sense Kurs – Artikel 8 von 28

← Vorheriger Artikel: Incremental Loading – Nur Changes laden
→ Nächster Artikel: Synthetic Keys & Circular References auflösen


Was lernen Sie über QVD-Optimierung für 100x schnellere Loads in Qlik Sense?

QVD-Dateien (QlikView Data) sind die geheime Performance-Waffe von Qlik – optimierte QVD-Loads erreichen 10-100x schnellere Datenladungen als direkte Datenbankverbindungen. Dieser Performance-Gewinn verwandelt mehrstündige Reloads in Minuten und ermöglicht Enterprise-Analytics in bisher unvorstellbarem Umfang.

Wie kann ich QVD-Optimierung für 100x schnellere Loads in Qlik Sense nutzen?

  1. QVD-Grundlagen meistern: Verstehen Sie die interne Architektur von QVD-Dateien und warum sie so performant sind
  2. Optimierte vs. nicht-optimierte Loads: Kennen Sie alle Regeln, die über 100x Geschwindigkeitsunterschiede entscheiden
  3. Enterprise-Architekturen aufbauen: Implementieren Sie 3- bis 5-Layer-Architekturen für skalierbare Datenmodelle
  4. Incremental Loading perfektionieren: Reduzieren Sie Datenbanklasten um 99% durch clevere Delta-Load-Patterns
  5. Massive Datasets partitionieren: Handhaben Sie Milliarden von Zeilen durch intelligente Segmentierung
  6. Performance troubleshooten: Diagnostizieren und beheben Sie die häufigsten QVD-Probleme
  7. Cloud-native Strategien: Nutzen Sie moderne Patterns für Qlik Cloud und hybride Architekturen
  8. Governance implementieren: Etablieren Sie Monitoring, Versionierung und Qualitätssicherung

Zeitinvestition: 90 Min Lesen + 8-16 Std praktische Übungen
Voraussetzungen: Qlik Sense Desktop/Cloud + grundlegende Datenlade-Kenntnisse
Schwierigkeitsgrad: Anfänger bis Fortgeschrittene (alle Level finden wertvolle Insights)


Was sind die Grundlagen und die Architektur von QVD?

Wie erzielen QVD-Dateien ihre Performance in Qlik Sense?

QVD-Dateien nutzen keine traditionellen Kompressionsalgorithmen wie ZIP oder LZH. Stattdessen erreichen sie dramatische Größenreduktion durch Symbol-Tabellen kombiniert mit Bit-stuffed Pointers – eine Technik, die jeden eindeutigen Feldwert genau einmal speichert und dann mit minimaler Bit-Anzahl referenziert.

Die drei Teile einer QVD-Datei:

┌─────────────────────────────────────┐
│  1. XML-Header (Metadata)           │
│  - Feldnamen und Datentypen         │
│  - Creation Timestamp               │
│  - Original SQL Statements          │
│  - Recordanzahl und Kompressionsinfo│
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│  2. Symbol-Tabellen (Column-Major)  │
│  - Jeder eindeutige Wert pro Feld   │
│  - Sortiert und indiziert           │
│  - Einmal gespeichert, oft genutzt  │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│  3. Index-Tabelle (Row-Major)       │
│  - Bit-stuffed Pointers zu Symbolen │
│  - Minimale Bit-Anzahl pro Zeile    │
│  - Hochkomprimiert                  │
└─────────────────────────────────────┘

Praktisches Beispiel: Wie Kompression funktioniert

Betrachten Sie eine Tabelle mit 1 Million Zeilen und einem Datumsfeld mit 1.460 eindeutigen Daten:

Ohne QVD (traditionelle Speicherung):

  • 1.000.000 Zeilen × 8 Bytes (Datum) = 8.000.000 Bytes
  • Gesamt: ~7,6 MB

Mit QVD (Symbol-Tabelle + Pointer):

  • Symbol-Tabelle: 1.460 Daten × 8 Bytes = 11.680 Bytes
  • Pointer: 1.000.000 × 11 Bits (2^11 = 2.048 kann 1.460 darstellen) = 1.375.000 Bytes
  • Gesamt: ~1,3 MB (83% Reduzierung!)

Warum optimierte Loads so schnell sind:

Die QVD-Architektur spiegelt exakt wider, wie Qlik Daten im RAM speichert. Ein optimierter Load überträgt Daten direkt von der Festplatte in den RAM in diesem komprimierten Format ohne jegliche Transformation:

Optimized Load:  Disk → RAM (direkte Block-Übertragung)
                 Geschwindigkeit: Pure I/O Speed

Non-Optimized:   Disk → Entpacken → Transformieren → 
                 Neu-Komprimieren → RAM
                 Geschwindigkeit: 10-100x langsamer

Wie starte ich in 10 Minuten mit der optimierten QVD in Qlik Sense?

Das Problem: Sie laden täglich 500.000 Zeilen aus einer SQL-Datenbank und jeder Load dauert 2 Minuten.

Die Lösung:

// ═══════════════════════════════════════════════════════════
// SCHRITT 1: QVD ERSTELLEN (Einmalig oder täglich geplant)
// ═══════════════════════════════════════════════════════════

// Daten aus der langsamen Datenbank laden
Raw_Customer_Data:
LOAD
    CustomerID,
    CustomerName,
    Region,
    Email,
    CreatedDate,
    LastModified
FROM [lib://DB_Connection] (SQL SELECT * FROM Customers);

// Als QVD-Datei speichern (unser "Download")
STORE Raw_Customer_Data INTO [lib://QVDs/Extract/Customers.qvd] (qvd);

// Original-Daten aus dem Speicher löschen (räumt auf)
DROP TABLE Raw_Customer_Data;

TRACE QVD erstellt: $(NoOfRows('Raw_Customer_Data')) Zeilen gespeichert;


// ═══════════════════════════════════════════════════════════
// SCHRITT 2: BLITZSCHNELL AUS DER QVD LADEN (Ihre tägliche App)
// ═══════════════════════════════════════════════════════════

Customers:
LOAD
    CustomerID,
    CustomerName,
    Region
FROM [lib://QVDs/Extract/Customers.qvd] (qvd);

// ✓ Dieser Load ist OPTIMIERT = blitzschnell!

Performance-Ergebnis:

  • Vorher (Database): 2 Minuten für 500k Zeilen
  • Nachher (QVD): 8 Sekunden für 500k Zeilen
  • Verbesserung: 15x schneller!

✓ Checkpoint: Öffnen Sie das Script-Log (Progress-Fenster). Sehen Sie «(qvd optimized)» neben dem Tabellennamen? Perfekt! Falls nicht, lesen Sie Abschnitt 2.2.


Was sind die Regeln für optimierte vs. nicht-optimierte Loads in Qlik Sense?

Was sind die kritischen Faktoren für die QVD-Optimierung in Qlik Sense?

Jede Transformation im LOAD-Statement erzwingt den nicht-optimierten Modus:

❌ Diese Operationen brechen die Optimierung:

// ══════════════════════════════════════════
// BERECHNETE FELDER
// ══════════════════════════════════════════
LOAD 
    OrderID,
    Year(OrderDate) as OrderYear,      // ❌ Datum-Funktion
    Amount * 1.19 as GrossAmount        // ❌ Berechnung
FROM [lib://QVDs/Orders.qvd] (qvd);


// ══════════════════════════════════════════
// WHERE-KLAUSELN MIT VERGLEICHEN
// ══════════════════════════════════════════
LOAD * 
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Country = 'Germany';              // ❌ Vergleichsoperator

LOAD * 
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Amount > 1000;                    // ❌ Numerischer Vergleich

LOAD * 
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Year(OrderDate) = 2024;           // ❌ Funktion in WHERE


// ══════════════════════════════════════════
// STRING-MANIPULATIONEN
// ══════════════════════════════════════════
LOAD 
    CustomerID,
    Upper(CustomerName) as CustomerName // ❌ String-Funktion
FROM [lib://QVDs/Customers.qvd] (qvd);


// ══════════════════════════════════════════
// JOINS UND CONCATENATES
// ══════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);

LEFT JOIN (Orders)                      // ❌ JOIN bricht Optimierung
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd);


// ══════════════════════════════════════════
// DUPLIZIERTE FELDER
// ══════════════════════════════════════════
LOAD 
    OrderID,
    OrderID as Order_Number             // ❌ Feld zweimal laden
FROM [lib://QVDs/Orders.qvd] (qvd);


// ══════════════════════════════════════════
// ORDER BY, GROUP BY, DISTINCT (vor Feb 2018)
// ══════════════════════════════════════════
LOAD DISTINCT CustomerID                // ❌ In älteren Versionen
FROM [lib://QVDs/Customers.qvd] (qvd)
ORDER BY CustomerID;                    // ❌ ORDER BY

✅ Diese Operationen bleiben optimiert:

// ══════════════════════════════════════════
// FELD-UMBENENNUNG
// ══════════════════════════════════════════
LOAD 
    OrderID as Order_Number,            // ✓ Nur Label-Änderung
    CustomerID as Customer_ID,
    OrderDate as Date
FROM [lib://QVDs/Orders.qvd] (qvd);


// ══════════════════════════════════════════
// FELD-SELEKTION
// ══════════════════════════════════════════
LOAD 
    CustomerID,                         // ✓ Nur spezifische Felder
    CustomerName,
    Region                              // Andere Felder weglassen
FROM [lib://QVDs/Customers.qvd] (qvd);


// ══════════════════════════════════════════
// WHERE EXISTS() - Der Optimierungs-Freund
// ══════════════════════════════════════════
// Schritt 1: Kleine Tabelle mit gewünschten Keys laden
ValidCustomers:
LOAD DISTINCT CustomerID
FROM [lib://Dims/Customers.qvd] (qvd)
WHERE Region = 'DACH';

// Schritt 2: Große Tabelle mit WHERE EXISTS filtern
Orders:
LOAD 
    OrderID,
    CustomerID,
    Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE EXISTS(CustomerID);               // ✓ Bleibt optimiert!


// ══════════════════════════════════════════
// WHERE NOT EXISTS()
// ══════════════════════════════════════════
NewOrders:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE NOT EXISTS(OrderID);              // ✓ Bleibt optimiert!


// ══════════════════════════════════════════
// LOAD DISTINCT (ab Feb 2018)
// ══════════════════════════════════════════
UniqueCustomers:
LOAD DISTINCT 
    CustomerID,
    CustomerName
FROM [lib://QVDs/Customers.qvd] (qvd);  // ✓ In modernen Versionen optimiert


// ══════════════════════════════════════════
// FIRST/LAST (Zeilenbegrenzung)
// ══════════════════════════════════════════
TestData:
FIRST 1000                              // ✓ Optimiert in neueren Versionen
LOAD * 
FROM [lib://QVDs/Orders.qvd] (qvd);

Was ist die Zweistufige Lade-Strategie in Qlik Sense?

Für maximale Performance: Erst optimiert laden, dann transformieren!

// ═══════════════════════════════════════════════════════════
// PATTERN: OPTIMIZED LOAD + RESIDENT TRANSFORMATION
// ═══════════════════════════════════════════════════════════

// STUFE 1: Optimierter Load (super schnell!)
TempOrders:
LOAD 
    OrderID,
    CustomerID,
    OrderDate,
    Amount,
    Currency
FROM [lib://QVDs/Orders.qvd] (qvd);
// ← Hier steht "(qvd optimized)" im Log ✓


// STUFE 2: Transformationen via RESIDENT (im RAM, schnell!)
Orders:
LOAD
    OrderID,
    CustomerID,
    Date(OrderDate, 'DD.MM.YYYY') as OrderDate,
    Year(OrderDate) as OrderYear,
    Month(OrderDate) as OrderMonth,
    Quarter(OrderDate) as OrderQuarter,
    Amount,
    Amount * 1.19 as GrossAmount,
    If(Amount > 1000, 'High Value', 'Standard') as OrderSegment,
    Currency
RESIDENT TempOrders;

// STUFE 3: Aufräumen
DROP TABLE TempOrders;


// ═══════════════════════════════════════════════════════════
// WARUM DAS FUNKTIONIERT
// ═══════════════════════════════════════════════════════════
// 
// 1. TempOrders lädt mit QVD-Geschwindigkeit (10-100x schnell)
// 2. Transformationen passieren im RAM (auch schnell!)
// 3. Statt 100% langsam → 5% langsam + 95% schnell
//
// Beispiel 10M Zeilen:
// - Alles non-optimized: 15 Minuten
// - Optimized + Resident: 2 Min (QVD) + 1 Min (Transform) = 3 Min
// - Faktor 5x schneller!

Wie funktioniert WHERE EXISTS() für große Datenmengen in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// PROBLEM: 
// Sie haben 100M Zeilen Orders, brauchen aber nur 5M für bestimmte Kunden
// ═══════════════════════════════════════════════════════════

// SCHLECHTE LÖSUNG (Non-Optimized, sehr langsam):
Orders:
LOAD * 
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Country = 'Germany';  
// ❌ Muss alle 100M Zeilen prüfen!


// ═══════════════════════════════════════════════════════════
// GUTE LÖSUNG (Optimized, 4-8x schneller):
// ═══════════════════════════════════════════════════════════

// Schritt 1: Kleine Key-Tabelle laden
GermanCustomers:
LOAD DISTINCT CustomerID
FROM [lib://QVDs/Customers.qvd] (qvd)
WHERE Country = 'Germany';
// Nur wenige tausend Zeilen, schnell!


// Schritt 2: Große Tabelle mit EXISTS filtern
Orders:
LOAD 
    OrderID,
    CustomerID,
    OrderDate,
    Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE EXISTS(CustomerID);
// ✓ Optimized Load! Nur Key-Matching, keine Berechnungen


// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH (Real-World-Daten):
// ═══════════════════════════════════════════════════════════
// 94M Zeilen, Filter auf 5M Zeilen:
// - WHERE Country = 'Germany': 4 Minuten
// - WHERE EXISTS(CustomerID): 30 Sekunden
// - Verbesserung: 8x schneller!

Wie prüfe ich die Optimierung im Script-Log in Qlik Sense?

// So sieht ein optimierter Load im Progress-Log aus:
Orders << Orders.qvd (qvd optimized)
500,000 Zeilen abgerufen
Execution time: 00:00:08

// Non-optimized sieht so aus:
Orders << Orders.qvd
500,000 Zeilen abgerufen
Execution time: 00:02:15

// 🎯 MERKE: Kein "(qvd optimized)" = Problem!

Log-Dateien finden:

  • Qlik Sense Desktop: C:Users<Username>DocumentsQlikSenseApps<AppName>ScriptLog.txt
  • Qlik Sense Server: C:ProgramDataQlikSenseLogScript<AppID>
  • QlikView: Gleicher Ordner wie die QVW-Datei (wenn Logging aktiviert)

Wie funktionieren Enterprise-Architekturen mit QVD-Layern in Qlik Sense?

3.1 Warum Layer-Architekturen?

In kleinen Projekten laden Sie vielleicht direkt aus der Datenbank in Ihre App. Aber in Enterprise-Umgebungen führt das zu:

Problemen ohne Layer-Architektur:

  • Jede App lädt redundant aus der Datenbank (DB-Overload!)
  • Geschäftslogik wird in 20 Apps dupliziert (Wartungsalptraum)
  • Inkonsistente Berechnungen über Apps hinweg (Datenqualitätsprobleme)
  • Änderungen an Quelldaten brechen alle Apps gleichzeitig

Vorteile mit Layer-Architektur:

  • Einmal extrahieren, x-fach nutzen (DB-Last um 90% reduziert)
  • Zentrale Geschäftslogik (eine Änderung, überall wirksam)
  • Wiederverwendbare Daten-Assets (Zeit sparen bei neuen Apps)
  • Entkopplung (Source-Änderungen betreffen nur Extract-Layer)

Was ist die Drei-Schichten-Architektur (Industry Standard) in Qlik Sense?

┌─────────────────────────────────────────────────────────────┐
│                    PRESENTATION LAYER (Layer 3)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │  Sales   │  │  Finance │  │   HR     │  │   Ops    │    │
│  │Dashboard │  │Dashboard │  │Dashboard │  │Dashboard │    │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘    │
│       │             │              │             │           │
│       └─────────────┴──────────────┴─────────────┘           │
│                          │                                   │
└──────────────────────────┼───────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    TRANSFORM LAYER (Layer 2)                 │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Business-Ready QVDs mit Transformationen:             │ │
│  │  • Fact_Sales.qvd                                      │ │
│  │  • Dim_Customer.qvd                                    │ │
│  │  • Dim_Product.qvd                                     │ │
│  │  • Master_Calendar.qvd                                 │ │
│  │                                                         │ │
│  │  Hier passiert:                                        │ │
│  │  ✓ Datenbereinigung                                    │ │
│  │  ✓ Geschäftslogik                                      │ │
│  │  ✓ Typ-Konvertierungen                                 │ │
│  │  ✓ Star-Schema-Aufbau                                  │ │
│  └────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    EXTRACT LAYER (Layer 1)                   │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Raw QVDs - unveränderte Kopien der Quellen:          │ │
│  │  • Extract_Orders.qvd                                  │ │
│  │  • Extract_Customers.qvd                               │ │
│  │  • Extract_Products.qvd                                │ │
│  │                                                         │ │
│  │  Hier passiert:                                        │ │
│  │  ✓ Pure Extraktion (keine Transformationen!)          │ │
│  │  ✓ Minimale DB-Connection-Zeit                        │ │
│  │  ✓ 1:1 Abbild der Quelle                              │ │
│  └────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
                           ▼
                    ┌─────────────┐
                    │  DATA       │
                    │  SOURCES    │
                    │             │
                    │ • SAP       │
                    │ • SQL       │
                    │ • Oracle    │
                    │ • Files     │
                    └─────────────┘

Wie extrahiere ich Raw Data QVDs in Layer 1?

Ziel: Minimale Connection-Zeit zu Quellsystemen, keine Transformationen!

// ═══════════════════════════════════════════════════════════
// EXTRACT LAYER - SCHRITT 1: RAW DATA EXTRACTION
// ═══════════════════════════════════════════════════════════
// App: 01_Extract_ERP_Orders.qvf
// Schedule: Täglich 02:00 Uhr
// Connection Time: < 5 Minuten
// ═══════════════════════════════════════════════════════════

SET ErrorMode = 0;  // Fehler abfangen, nicht abbrechen

// Verbindung zur Quelldatenbank
LET vStartTime = Now();

// ═══════════════════════════════════════════════════════════
// Rohdaten 1:1 extrahieren
// ═══════════════════════════════════════════════════════════
Extract_Orders:
LOAD
    OrderID,
    CustomerID,
    ProductID,
    OrderDate,
    ShipDate,
    Amount,
    Currency,
    Status,
    CreatedBy,
    CreatedDate,
    ModifiedBy,
    ModifiedDate
FROM [lib://ERP_Connection] (SQL SELECT * FROM Orders 
    WHERE ModifiedDate >= '$(vLastExtract)');

// ═══════════════════════════════════════════════════════════
// Speichern mit Timestamp im Dateinamen
// ═══════════════════════════════════════════════════════════
LET vTimestamp = Date(Today(), 'YYYY-MM-DD');

STORE Extract_Orders INTO 
    [lib://QVDs/01_Extract/Orders_$(vTimestamp).qvd] (qvd);

// Aktuellste Version auch als "current" speichern
STORE Extract_Orders INTO 
    [lib://QVDs/01_Extract/Orders_CURRENT.qvd] (qvd);

// ═══════════════════════════════════════════════════════════
// Metadaten loggen
// ═══════════════════════════════════════════════════════════
LET vRowCount = NoOfRows('Extract_Orders');
LET vEndTime = Now();
LET vDuration = Interval(vEndTime - vStartTime, 'mm:ss');

TRACE ═══════════════════════════════════════════════;
TRACE EXTRACT ABGESCHLOSSEN;
TRACE Tabelle: Orders;
TRACE Zeilen: $(vRowCount);
TRACE Dauer: $(vDuration);
TRACE Zeitstempel: $(vTimestamp);
TRACE ═══════════════════════════════════════════════;

// Aufräumen
DROP TABLE Extract_Orders;

// Fehlerprüfung
IF ScriptErrorCount > 0 THEN
    TRACE FEHLER: $(ScriptErrorCount) Fehler aufgetreten!;
    // Alert an Monitoring-System senden
    EXECUTE cmd /c echo "Extract Orders failed" | mail -s "QVD Alert" admin@company.com;
END IF

Naming Convention für Extract Layer:

  • Format: Extract_<SourceSystem>_<TableName>_<Timestamp>.qvd
  • Beispiele:
    • Extract_SAP_BSEG_2024-01-15.qvd
    • Extract_SQL_Orders_CURRENT.qvd
    • Extract_Oracle_Customers_2024-01-15.qvd

Wie transformiere ich Layer 2 zu Business-Ready QVDs?

Ziel: Alle Geschäftslogik, Berechnungen und Datenqualität zentral verwalten!

// ═══════════════════════════════════════════════════════════
// TRANSFORM LAYER - SCHRITT 2: BUSINESS LOGIC
// ═══════════════════════════════════════════════════════════
// App: 02_Transform_Orders.qvf
// Schedule: Täglich 02:10 Uhr (nach Extract!)
// Processing Time: 5-15 Minuten
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// MAPPING-TABELLEN VORBEREITEN
// ═══════════════════════════════════════════════════════════

// Währungs-Mapping
CurrencyMapping:
MAPPING LOAD
    CurrencyCode,
    ExchangeRate
FROM [lib://QVDs/01_Extract/ExchangeRates_CURRENT.qvd] (qvd);

// Status-Mapping (Friendly Names)
StatusMapping:
MAPPING LOAD * INLINE [
    Status_Code, Status_Text
    10, Neu
    20, In Bearbeitung
    30, Versandt
    40, Geliefert
    50, Storniert
];

// ═══════════════════════════════════════════════════════════
// HAUPTDATEN TRANSFORMIEREN
// ═══════════════════════════════════════════════════════════

TempOrders:
LOAD
    OrderID,
    CustomerID,
    ProductID,
    OrderDate,
    ShipDate,
    Amount,
    Currency,
    Status,
    CreatedDate,
    ModifiedDate
FROM [lib://QVDs/01_Extract/Orders_CURRENT.qvd] (qvd);
// ✓ Optimized Load!


// Jetzt alle Transformationen:
Transform_Orders:
LOAD
    // ═══════════ IDs ═══════════
    OrderID,
    CustomerID,
    ProductID,
    
    // ═══════════ DATUMS-DIMENSIONEN ═══════════
    Date(Floor(OrderDate), 'DD.MM.YYYY') as OrderDate,
    Year(OrderDate) as OrderYear,
    Month(OrderDate) as OrderMonth,
    Quarter(OrderDate) as OrderQuarter,
    Week(OrderDate) as OrderWeek,
    Weekday(OrderDate) as OrderWeekday,
    Day(OrderDate) as OrderDay,
    
    // Geschäftsjahr (beginnt in Q2)
    If(Month(OrderDate) >= 4, 
        Year(OrderDate), 
        Year(OrderDate) - 1) as FiscalYear,
    
    // ═══════════ BETRÄGE & WÄHRUNG ═══════════
    Amount as Amount_Original,
    Currency,
    
    // Umrechnung in EUR
    Amount * ApplyMap('CurrencyMapping', Currency, 1) as Amount_EUR,
    
    // Kategorisierung
    If(Amount > 10000, 'Großauftrag',
       If(Amount > 1000, 'Mittel',
          'Klein')) as OrderSizeCategory,
    
    // ═══════════ STATUS ═══════════
    Status as StatusCode,
    ApplyMap('StatusMapping', Text(Status), 'Unbekannt') as StatusText,
    
    // Business-Flags
    If(Status >= 40, 1, 0) as IsCompleted,
    If(Status = 50, 1, 0) as IsCancelled,
    If(ShipDate - OrderDate > 7, 1, 0) as IsDelayed,
    
    // ═══════════ AUDIT-FELDER ═══════════
    CreatedDate,
    ModifiedDate,
    
    // Data Quality Flags
    If(IsNull(CustomerID), 1, 0) as DQ_MissingCustomer,
    If(Amount <= 0, 1, 0) as DQ_InvalidAmount,
    If(ShipDate < OrderDate, 1, 0) as DQ_InvalidDates
    
RESIDENT TempOrders;

DROP TABLE TempOrders;


// ═══════════════════════════════════════════════════════════
// DATA QUALITY CHECKS
// ═══════════════════════════════════════════════════════════

QualityCheck:
LOAD
    Sum(DQ_MissingCustomer) as Missing_Customers,
    Sum(DQ_InvalidAmount) as Invalid_Amounts,
    Sum(DQ_InvalidDates) as Invalid_Dates,
    Count(*) as Total_Records
RESIDENT Transform_Orders;

LET vMissingCustomers = Peek('Missing_Customers', 0, 'QualityCheck');

IF $(vMissingCustomers) > 0 THEN
    TRACE WARNUNG: $(vMissingCustomers) Bestellungen ohne CustomerID!;
END IF

DROP TABLE QualityCheck;


// ═══════════════════════════════════════════════════════════
// SPEICHERN
// ═══════════════════════════════════════════════════════════

STORE Transform_Orders INTO 
    [lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd);

DROP TABLE Transform_Orders;

TRACE Transform Layer abgeschlossen: Fact_Orders.qvd erstellt;

Naming Convention für Transform Layer:

  • Format: <Type>_<EntityName>.qvd
  • Typen:
    • Fact_* für Faktentabellen (Orders, Sales, Transactions)
    • Dim_* für Dimensionstabellen (Customer, Product, Date)
    • Bridge_* für Brückentabellen (Many-to-Many)
    • Master_* für Referenzdaten (Calendar, ExchangeRates)

Wie optimiert man QVD für 100x schnellere Loads in Qlik Sense?

Ziel: Nur laden, was diese App braucht. Optimiert und gefiltert!

// ═══════════════════════════════════════════════════════════
// PRESENTATION LAYER - APP: Sales Dashboard
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// CALENDAR FILTER VORBEREITEN
// ═══════════════════════════════════════════════════════════
// Diese App zeigt nur letzten 24 Monate
TempCalendar:
LOAD DISTINCT
    OrderDate
RESIDENT 
    (SELECT OrderDate FROM [lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd))
WHERE OrderDate >= AddMonths(Today(), -24);


// ═══════════════════════════════════════════════════════════
// OPTIMIZED LOAD MIT WHERE EXISTS
// ═══════════════════════════════════════════════════════════
Orders:
LOAD
    OrderID,
    CustomerID,
    ProductID,
    OrderDate,
    OrderYear,
    OrderMonth,
    OrderQuarter,
    Amount_EUR,
    OrderSizeCategory,
    StatusText,
    IsCompleted,
    IsCancelled
FROM [lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd)
WHERE EXISTS(OrderDate)         // ✓ Optimized!
  AND IsCompleted = 1;          // Nur abgeschlossene Aufträge

DROP TABLE TempCalendar;


// ═══════════════════════════════════════════════════════════
// DIMENSIONEN LADEN
// ═══════════════════════════════════════════════════════════
Customers:
LOAD
    CustomerID,
    CustomerName,
    Country,
    Region,
    Segment
FROM [lib://QVDs/02_Transform/Dim_Customer.qvd] (qvd)
WHERE EXISTS(CustomerID);       // ✓ Nur verwendete Kunden!

Products:
LOAD
    ProductID,
    ProductName,
    Category,
    SubCategory
FROM [lib://QVDs/02_Transform/Dim_Product.qvd] (qvd)
WHERE EXISTS(ProductID);        // ✓ Nur verwendete Produkte!


// ═══════════════════════════════════════════════════════════
// APP-SPEZIFISCHE BERECHNUNGEN (wenn nötig)
// ═══════════════════════════════════════════════════════════
// Hier können Sie app-spezifische Aggregationen machen
// ohne die Transform-Layer-QVDs zu ändern

Was sind Vier- und Fünf-Schichten-Architekturen für große Enterprises?

Vier-Schichten mit Data Model Layer:

Layer 1: Extract    (Raw QVDs)
Layer 2: Transform  (Business QVDs)
Layer 3: Model      (Wiederverwendbare QVF mit Datenmodell, ohne UI)
Layer 4: Presentation (Apps mit Binary Load + UI)

Vorteil: 10 Apps können ein Modell via Binary Load teilen!

// ═══════════════════════════════════════════════════════════
// LAYER 3: MODEL APP (z.B. "Sales_DataModel.qvf")
// ═══════════════════════════════════════════════════════════
// Dieses QVF wird NIE von Endbenutzern geöffnet!
// Es dient nur als Datenmodell-Quelle für andere Apps

// ... Hier laden Sie alle Transform-QVDs ...
// ... Hier erstellen Sie die Tabellenverknüpfungen ...
// ... KEINE Visualisierungen! ...

// Save und Schedule!


// ═══════════════════════════════════════════════════════════
// LAYER 4: PRESENTATION APP
// ═══════════════════════════════════════════════════════════

// Komplettes Datenmodell in 1 Sekunde laden!
Binary [lib://DataModels/Sales_DataModel.qvf];

// Jetzt nur noch UI-spezifische Dinge:
// - Section Access (User-basierte Filter)
// - App-spezifische Variablen
// - Visualisierungen erstellen

Fünf-Schichten (für Konzerne mit verteilten Quellen):

Layer 1: Extract Regional  (Pro Region/System separate QVDs)
Layer 2: Extract Consolidated (Alle Regionen zusammengeführt)
Layer 3: Transform (Business Logic)
Layer 4: Model (Wiederverwendbare Modelle)
Layer 5: Presentation (Apps)

Wann nutzen?

  • Globale Konzerne mit regionalen Datensilos
  • Unterschiedliche Refresh-Frequenzen pro Region
  • Compliance-Anforderungen (Daten dürfen Region nicht verlassen)

Wie funktioniert Incremental Loading mit QVDs in Qlik Sense?

Warum ist Incremental Loading kritisch für QVD-Optimierung in Qlik Sense?

Szenario ohne Incremental Loading:

  • Tabelle: 100 Millionen Zeilen
  • Tägliches Wachstum: 1 Million neue Zeilen (1%)
  • Full Reload jeden Tag: Lädt alle 100M Zeilen neu!
    • Dauer: 2 Stunden
    • DB-Last: Extrem hoch
    • Netzwerk: Gigabytes übertragen

Mit Incremental Loading:

  • Lädt nur die 1M neuen Zeilen!
    • Dauer: 5 Minuten (24x schneller!)
    • DB-Last: 99% reduziert
    • Netzwerk: Minimal

Was ist Pattern 1: Insert-Only (Append-Only-Tabellen) in Qlik Sense?

Für Tabellen, wo Daten nur hinzugefügt werden (nie Updates/Deletes):

// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT-ONLY PATTERN
// ═══════════════════════════════════════════════════════════
// Beispiel: Log-Tabellen, Transaktions-Tabellen
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// SCHRITT 1: Maximum ID/Timestamp aus letztem Load ermitteln
// ═══════════════════════════════════════════════════════════

// Prüfen ob QVD existiert
IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
    
    // QVD existiert: Lade MaxID
    MaxID:
    LOAD
        Max(OrderID) as MaxOrderID
    FROM [lib://QVDs/Orders.qvd] (qvd);
    
    LET vMaxOrderID = Peek('MaxOrderID', 0, 'MaxID');
    DROP TABLE MaxID;
    
    TRACE Letzter geladener OrderID: $(vMaxOrderID);
    
ELSE
    // QVD existiert nicht: Full Load
    LET vMaxOrderID = 0;
    TRACE QVD nicht gefunden - führe Full Load durch;
END IF


// ═══════════════════════════════════════════════════════════
// SCHRITT 2: Neue Daten aus Source laden
// ═══════════════════════════════════════════════════════════

NewOrders:
LOAD
    OrderID,
    CustomerID,
    OrderDate,
    Amount,
    Status
FROM [lib://DB_Connection] 
    (SQL SELECT * FROM Orders WHERE OrderID > $(vMaxOrderID));

LET vNewRecords = NoOfRows('NewOrders');
TRACE Neue Datensätze gefunden: $(vNewRecords);


// ═══════════════════════════════════════════════════════════
// SCHRITT 3: Alte Daten aus QVD laden (wenn vorhanden)
// ═══════════════════════════════════════════════════════════

IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
    
    CONCATENATE (NewOrders)
    LOAD *
    FROM [lib://QVDs/Orders.qvd] (qvd);
    // ✓ Optimized Load!
    
    TRACE Alte Datensätze hinzugefügt;
END IF


// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Kombinierte Daten speichern
// ═══════════════════════════════════════════════════════════

// Erst in temp speichern (atomisches Update!)
STORE NewOrders INTO [lib://QVDs/Orders_temp.qvd] (qvd);

// Falls erfolgreich: temp → final umbenennen
IF ScriptErrorCount = 0 THEN
    // Backup erstellen
    IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
        EXECUTE cmd /c copy "$(vQVDPath)Orders.qvd" "$(vQVDPath)Orders_backup.qvd";
    END IF
    
    // Temp → Final
    EXECUTE cmd /c move /Y "$(vQVDPath)Orders_temp.qvd" "$(vQVDPath)Orders.qvd";
    
    TRACE ═══════════════════════════════════════;
    TRACE Incremental Load erfolgreich!;
    TRACE Total Datensätze: $(NoOfRows('NewOrders'));
    TRACE Neue Datensätze: $(vNewRecords);
    TRACE ═══════════════════════════════════════;
END IF

DROP TABLE NewOrders;

Wie funktioniert Pattern 2: Insert + Update (Slowly Changing Dimensions)?

Für Tabellen mit Änderungen an bestehenden Datensätzen:

// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT + UPDATE PATTERN
// ═══════════════════════════════════════════════════════════
// Beispiel: Kunden-Stammdaten, Produkt-Katalog
// Voraussetzung: ModifiedDate-Feld in Source-Tabelle!
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// SCHRITT 1: Letzten Änderungszeitpunkt ermitteln
// ═══════════════════════════════════════════════════════════

IF FileSize('lib://QVDs/Customers.qvd') > 0 THEN
    
    MaxModified:
    LOAD
        Max(ModifiedDate) as MaxModifiedDate
    FROM [lib://QVDs/Customers.qvd] (qvd);
    
    LET vMaxModified = Peek('MaxModifiedDate', 0, 'MaxModified');
    DROP TABLE MaxModified;
    
    // Sicherheitspuffer: 1 Tag zurück (falls Clock-Skew)
    LET vMaxModified = Date(vMaxModified - 1, 'YYYY-MM-DD HH:mm:ss');
    
ELSE
    // Full Load
    LET vMaxModified = '1900-01-01 00:00:00';
END IF

TRACE Lade Änderungen seit: $(vMaxModified);


// ═══════════════════════════════════════════════════════════
// SCHRITT 2: Geänderte Datensätze laden
// ═══════════════════════════════════════════════════════════

ChangedCustomers:
LOAD
    CustomerID,
    CustomerName,
    Country,
    Region,
    Email,
    Status,
    CreatedDate,
    ModifiedDate
FROM [lib://DB_Connection]
    (SQL SELECT * FROM Customers 
     WHERE ModifiedDate > '$(vMaxModified)');

LET vChangedRecords = NoOfRows('ChangedCustomers');
TRACE Geänderte Datensätze: $(vChangedRecords);


// ═══════════════════════════════════════════════════════════
// SCHRITT 3: Alte Daten laden (OHNE die geänderten!)
// ═══════════════════════════════════════════════════════════

IF FileSize('lib://QVDs/Customers.qvd') > 0 THEN
    
    CONCATENATE (ChangedCustomers)
    LOAD *
    FROM [lib://QVDs/Customers.qvd] (qvd)
    WHERE NOT EXISTS(CustomerID);
    // ✓ Lädt nur Kunden, die NICHT in ChangedCustomers sind
    // ⚠️ Achtung: NOT EXISTS bricht Optimierung!
    //    Aber: Wir laden nur aus QVD, nicht aus DB = immer noch schnell!
    
    TRACE Alte Datensätze (ohne Updates) hinzugefügt;
END IF


// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Speichern (atomisch!)
// ═══════════════════════════════════════════════════════════

STORE ChangedCustomers INTO [lib://QVDs/Customers_temp.qvd] (qvd);

IF ScriptErrorCount = 0 THEN
    // Backup + Replace (wie in Pattern 1)
    EXECUTE cmd /c copy "$(vQVDPath)Customers.qvd" "$(vQVDPath)Customers_backup.qvd";
    EXECUTE cmd /c move /Y "$(vQVDPath)Customers_temp.qvd" "$(vQVDPath)Customers.qvd";
    
    LET vTotalRecords = NoOfRows('ChangedCustomers');
    TRACE ═══════════════════════════════════════;
    TRACE Incremental Load erfolgreich!;
    TRACE Total Datensätze: $(vTotalRecords);
    TRACE Geänderte Datensätze: $(vChangedRecords);
    TRACE Update-Rate: $(Num(vChangedRecords/vTotalRecords, '#,##0.0%'));
    TRACE ═══════════════════════════════════════;
END IF

DROP TABLE ChangedCustomers;

Wie funktioniert Pattern 3: Insert + Update + Delete (Full Synchronization)?

Für Tabellen, wo auch Deletes synchronisiert werden müssen:

// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT + UPDATE + DELETE PATTERN
// ═══════════════════════════════════════════════════════════
// Komplexestes Pattern - vollständige Synchronisation
// ═══════════════════════════════════════════════════════════

// SCHRITTE 1-3: Wie in Pattern 2
// ... (ChangedCustomers Tabelle wird erstellt) ...


// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Alle aktuellen IDs aus Source laden
// ═══════════════════════════════════════════════════════════

CurrentCustomerIDs:
LOAD DISTINCT
    CustomerID
FROM [lib://DB_Connection]
    (SQL SELECT CustomerID FROM Customers);

TRACE Aktuelle Customer IDs in Source-System geladen;


// ═══════════════════════════════════════════════════════════
// SCHRITT 5: NUR Datensätze behalten, die in Source existieren
// ═══════════════════════════════════════════════════════════

INNER JOIN (ChangedCustomers)
LOAD *
RESIDENT CurrentCustomerIDs;

DROP TABLE CurrentCustomerIDs;

// Resultat: ChangedCustomers enthält nur Kunden, die:
// - In der Source-DB existieren (via INNER JOIN)
// - Entweder neu sind oder geändert wurden


// ═══════════════════════════════════════════════════════════
// SCHRITT 6: Speichern
// ═══════════════════════════════════════════════════════════

STORE ChangedCustomers INTO [lib://QVDs/Customers_temp.qvd] (qvd);

// ... Rest wie in Pattern 2 ...

TRACE Deletes wurden durch INNER JOIN entfernt;

Wie kann ich das MERGE Statement in Qlik Sense Modern verwenden?

Seit neueren Qlik-Versionen gibt es den MERGE-Befehl:

// ═══════════════════════════════════════════════════════════
// MODERN APPROACH: MERGE STATEMENT
// ═══════════════════════════════════════════════════════════
// Klarerer, wartbarerer Code als manuelle CONCATENATE-Logik
// ═══════════════════════════════════════════════════════════

// Geänderte Datensätze aus Source
ChangedOrders:
LOAD
    'I' as Operation,           // I=Insert, U=Update, D=Delete
    SequenceNumber,             // Für Reihenfolge bei mehrfachen Änderungen
    OrderID,
    CustomerID,
    Amount,
    Status,
    ModifiedDate
FROM [lib://DB_Connection]
    (SQL SELECT * FROM Orders WHERE ModifiedDate > '$(vLastSync)');


// Bestehende Daten
ExistingOrders:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);


// MERGE durchführen
MergedOrders:
MERGE (SequenceNumber)      // Sortiere nach SequenceNumber
LOAD
    Operation,
    SequenceNumber,
    OrderID,
    CustomerID,
    Amount,
    Status,
    ModifiedDate
RESIDENT ChangedOrders
ON OrderID;                 // Primary Key

CONCATENATE (MergedOrders)
LOAD *
RESIDENT ExistingOrders
WHERE NOT EXISTS(OrderID);  // Nur alte Datensätze ohne Änderung

DROP TABLES ChangedOrders, ExistingOrders;

STORE MergedOrders INTO [lib://QVDs/Orders.qvd] (qvd);

Was ist der Buffer-Prefix und wie kann er in Qlik Sense genutzt werden?

Der einfachste Weg für Insert-Only-Tabellen:

// ═══════════════════════════════════════════════════════════
// BUFFER PREFIX: Automatisches Incremental Loading
// ═══════════════════════════════════════════════════════════
// Qlik trackt automatisch, wie viele Zeilen geladen wurden
// und lädt beim nächsten Mal nur neue Zeilen!
// ═══════════════════════════════════════════════════════════

Logs:
LOAD
    LogID,
    Timestamp,
    Message,
    Severity
FROM [lib://DB_Connection]
    (SQL SELECT * FROM ApplicationLogs ORDER BY LogID);
PREFIX BUFFER (incremental);

// Das war's! Beim nächsten Reload lädt Qlik automatisch
// nur neue Zeilen (basierend auf Zeilenanzahl).
//
// ⚠️ WICHTIG: Funktioniert nur wenn:
// 1. Tabelle ist append-only (keine Updates/Deletes)
// 2. Keine WHERE-Klausel im SQL (außer Datum-Filter)
// 3. ORDER BY ist essentiell (meist Primary Key)

Wie erfolgt die Partitionierung für massive Datenmengen in Qlik Sense?

5.1 Wann partitionieren?

Partitionierung wird relevant ab:

  • 50+ Millionen Zeilen in einer Tabelle
  • QVD-Dateien > 5 GB (Cloud: max 6 GB!)
  • Load-Zeiten > 10 Minuten trotz Optimierung
  • Klare Filter-Patterns (Zeit, Region, etc.)

Vorteile:

  • 🚀 80% weniger Daten laden (nur relevante Partitionen)
  • 💾 Dramatisch weniger RAM-Verbrauch
  • ⚡ Parallele Verarbeitung möglich
  • 🔧 Einfachere Updates (nur aktuelle Partition ändern)
  • 📦 Überschaubare Dateigrößen

Was ist Pattern 1: Zeitbasierte Partitionierung in Qlik Sense?

Monatliche Partitionierung

// ═══════════════════════════════════════════════════════════
// PARTITIONIERUNG ERSTELLEN: MONATLICH
// ═══════════════════════════════════════════════════════════
// App: Partition_Generator.qvf
// Schedule: Täglich (updated nur aktuelle Partition!)
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Orders';

// ═══════════════════════════════════════════════════════════
// Schleife durch letzten 24 Monate
// ═══════════════════════════════════════════════════════════

FOR i = 0 to 23

    // Monatsdatum berechnen
    LET vPartitionDate = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
    LET vMonthStart = Date(MonthStart(Today(), -$(i)), 'YYYY-MM-DD');
    LET vMonthEnd = Date(MonthEnd(Today(), -$(i)), 'YYYY-MM-DD');
    
    TRACE ═══════════════════════════════════════;
    TRACE Verarbeite Monat: $(vPartitionDate);
    TRACE Von: $(vMonthStart) Bis: $(vMonthEnd);
    TRACE ═══════════════════════════════════════;
    
    
    // ═══════════════════════════════════════════════════════════
    // Daten für diesen Monat laden
    // ═══════════════════════════════════════════════════════════
    
    Orders_Partition:
    LOAD
        OrderID,
        CustomerID,
        ProductID,
        OrderDate,
        Amount,
        Status
    FROM [lib://DB_Connection]
        (SQL SELECT * FROM Orders 
         WHERE OrderDate >= '$(vMonthStart)' 
           AND OrderDate <= '$(vMonthEnd)');
    
    LET vRecordCount = NoOfRows('Orders_Partition');
    
    
    // ═══════════════════════════════════════════════════════════
    // Nur speichern wenn Daten vorhanden
    // ═══════════════════════════════════════════════════════════
    
    IF vRecordCount > 0 THEN
        
        STORE Orders_Partition INTO 
            [$(vPartitionPath)/Orders_$(vPartitionDate).qvd] (qvd);
        
        TRACE ✓ Partition gespeichert: $(vRecordCount) Zeilen;
        
    ELSE
        TRACE ⚠ Keine Daten für Monat $(vPartitionDate);
    END IF
    
    DROP TABLE Orders_Partition;

NEXT i

TRACE ═══════════════════════════════════════;
TRACE Partitionierung abgeschlossen!;
TRACE ═══════════════════════════════════════;


// ═══════════════════════════════════════════════════════════
// ERGEBNIS: 24 separate QVD-Dateien
// ═══════════════════════════════════════════════════════════
// Orders_2023-01.qvd (450 MB)
// Orders_2023-02.qvd (520 MB)
// Orders_2023-03.qvd (610 MB)
// ...
// Orders_2024-12.qvd (890 MB)

Partitionen laden (Consumer-App)

// ═══════════════════════════════════════════════════════════
// PARTITIONEN LADEN: Nur die benötigten Monate!
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Orders';

// ═══════════════════════════════════════════════════════════
// VARIANTE 1: Letzten 12 Monate laden
// ═══════════════════════════════════════════════════════════

FOR i = 0 to 11

    LET vPartitionDate = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
    LET vPartitionFile = '$(vPartitionPath)/Orders_$(vPartitionDate).qvd';
    
    // Prüfen ob Datei existiert
    IF FileSize('$(vPartitionFile)') > 0 THEN
        
        CONCATENATE (Orders)
        LOAD *
        FROM [$(vPartitionFile)] (qvd);
        // ✓ Optimized Load!
        
        TRACE Partition geladen: $(vPartitionDate);
        
    ELSE
        TRACE ⚠ Partition nicht gefunden: $(vPartitionDate);
    END IF

NEXT i


// ═══════════════════════════════════════════════════════════
// VARIANTE 2: Wildcard-Load (alle Partitionen eines Jahres)
// ═══════════════════════════════════════════════════════════

Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_2024-*.qvd] (qvd);
// Lädt alle Monate von 2024


// ═══════════════════════════════════════════════════════════
// VARIANTE 3: Spezifischer Zeitraum (z.B. Q1 2024)
// ═══════════════════════════════════════════════════════════

FOR EACH vMonth in '2024-01', '2024-02', '2024-03'
    
    CONCATENATE (Orders)
    LOAD *
    FROM [$(vPartitionPath)/Orders_$(vMonth).qvd] (qvd);
    
NEXT vMonth


// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH
// ═══════════════════════════════════════════════════════════
// 
// Szenario: 5 Jahre Daten, nur letzten 12 Monate brauchen
//
// Ohne Partitionierung:
// - 60 Monate à 500 MB = 30 GB laden
// - Load-Zeit: 45 Minuten
// - RAM benötigt: 30 GB
//
// Mit Partitionierung:
// - 12 Monate à 500 MB = 6 GB laden
// - Load-Zeit: 5 Minuten (9x schneller!)
// - RAM benötigt: 6 GB (80% weniger!)

Wie funktioniert die Intraday-Partitionierung (für High-Frequency Data)?

Für Szenarien mit Millionen Transaktionen pro Tag:

// ═══════════════════════════════════════════════════════════
// INTRADAY PARTITIONIERUNG: Stündlich
// ═══════════════════════════════════════════════════════════
// Tagsüber: Stündliche QVDs
// Nachts: Konsolidierung zu Tages-QVDs
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Transactions';

// ═══════════════════════════════════════════════════════════
// TAGSÜBER (Stündlich, 06:00 - 22:00): Hourly QVDs
// ═══════════════════════════════════════════════════════════
// Schedule: Jede Stunde

LET vCurrentDate = Date(Today(), 'YYYY-MM-DD');
LET vCurrentHour = Text(Hour(Now()), '00');
LET vLastHour = Text(Hour(Now()) - 1, '00');

// Letzte Stunde laden
HourlyTransactions:
LOAD *
FROM [lib://DB_Connection]
    (SQL SELECT * FROM Transactions 
     WHERE TransactionTime >= '$(vCurrentDate) $(vLastHour):00:00'
       AND TransactionTime < '$(vCurrentDate) $(vCurrentHour):00:00');

// Als stündliche Partition speichern
STORE HourlyTransactions INTO 
    [$(vPartitionPath)/Trans_$(vCurrentDate)_H$(vLastHour).qvd] (qvd);

DROP TABLE HourlyTransactions;


// ═══════════════════════════════════════════════════════════
// NACHTS (01:00 Uhr): Konsolidierung zu Daily QVDs
// ═══════════════════════════════════════════════════════════
// Schedule: Täglich 01:00

LET vYesterday = Date(Today() - 1, 'YYYY-MM-DD');

// Alle stündlichen Partitionen des Vortags laden
DailyTransactions:
LOAD *
FROM [$(vPartitionPath)/Trans_$(vYesterday)_H*.qvd] (qvd);
// Wildcard lädt alle Stunden: H00, H01, H02, ... H23

// Als Tages-QVD speichern
STORE DailyTransactions INTO 
    [$(vPartitionPath)/Trans_$(vYesterday).qvd] (qvd);

DROP TABLE DailyTransactions;

// Stündliche QVDs löschen (Cleanup)
FOR vHour = 0 to 23
    LET vHourFormatted = Text(vHour, '00');
    LET vFileToDelete = '$(vPartitionPath)/Trans_$(vYesterday)_H$(vHourFormatted).qvd';
    
    IF FileSize('$(vFileToDelete)') > 0 THEN
        EXECUTE cmd /c del "$(vFileToDelete)";
        TRACE Deleted: Trans_$(vYesterday)_H$(vHourFormatted).qvd;
    END IF
NEXT vHour


// ═══════════════════════════════════════════════════════════
// CONSUMER-APP: Intelligentes Laden
// ═══════════════════════════════════════════════════════════

// Historische Daten (Daily QVDs)
Transactions:
LOAD *
FROM [$(vPartitionPath)/Trans_2024-*.qvd] (qvd)
WHERE Date# <> Today();  // Nicht der heutige Tag!

// Heutiger Tag (Hourly QVDs für near-real-time)
LET vToday = Date(Today(), 'YYYY-MM-DD');

CONCATENATE (Transactions)
LOAD *
FROM [$(vPartitionPath)/Trans_$(vToday)_H*.qvd] (qvd);

TRACE Near-Real-Time: Daten bis $(Hour(Now())) Uhr geladen;

Was ist Pattern 3: Multi-Dimensionale Partitionierung in Qlik Sense?

Kombination mehrerer Dimensionen für extreme Skalierung:

// ═══════════════════════════════════════════════════════════
// MULTI-DIMENSIONAL: Jahr × Region
// ═══════════════════════════════════════════════════════════
// Struktur:
// Orders_2024_EMEA.qvd
// Orders_2024_AMER.qvd
// Orders_2024_APAC.qvd
// Orders_2023_EMEA.qvd
// ...
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Orders';

// Regionen definieren
RegionList:
LOAD * INLINE [
    Region
    EMEA
    AMER
    APAC
];

// Jahre definieren (letzten 3 Jahre)
FOR vYear = Year(Today()) to Year(Today()) - 2 STEP -1

    // Durch Regionen iterieren
    FOR i = 0 to NoOfRows('RegionList') - 1
        
        LET vRegion = Peek('Region', i, 'RegionList');
        
        TRACE Verarbeite: $(vYear) - $(vRegion);
        
        // Daten für Jahr × Region laden
        Orders_Partition:
        LOAD *
        FROM [lib://DB_Connection]
            (SQL SELECT * FROM Orders 
             WHERE Year(OrderDate) = $(vYear)
               AND Region = '$(vRegion)');
        
        IF NoOfRows('Orders_Partition') > 0 THEN
            STORE Orders_Partition INTO 
                [$(vPartitionPath)/Orders_$(vYear)_$(vRegion).qvd] (qvd);
            TRACE ✓ Gespeichert: $(NoOfRows('Orders_Partition')) Zeilen;
        END IF
        
        DROP TABLE Orders_Partition;
        
    NEXT i

NEXT vYear

DROP TABLE RegionList;


// ═══════════════════════════════════════════════════════════
// CONSUMER: Nur relevante Partitionen laden
// ═══════════════════════════════════════════════════════════

// Beispiel: EMEA Sales Manager braucht nur EMEA-Daten

Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_*_EMEA.qvd] (qvd);
// Wildcard: Alle Jahre, nur EMEA!

// Oder spezifisch:
Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_2024_EMEA.qvd] (qvd);

CONCATENATE (Orders)
LOAD *
FROM [$(vPartitionPath)/Orders_2023_EMEA.qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// PERFORMANCE-IMPACT
// ═══════════════════════════════════════════════════════════
//
// Global Dataset: 300M Zeilen über 3 Jahre, 3 Regionen
//
// Ohne Partitionierung:
// - Jeder Regional Manager lädt 300M Zeilen
// - 30 GB RAM, 45 Min Load-Zeit
//
// Mit Jahr × Region:
// - EMEA Manager lädt ~33M Zeilen (nur EMEA, letzten 2 Jahre)
// - 3 GB RAM, 5 Min Load-Zeit
// - 90% weniger Ressourcen!

Wie funktioniert die Hash-basierte Partitionierung für parallele Verarbeitung?

// ═══════════════════════════════════════════════════════════
// HASH-PARTITIONING: Gleichmäßige Verteilung
// ═══════════════════════════════════════════════════════════
// Nutzen Sie dies für parallele Verarbeitung auf Multi-Core
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Customers';
LET vNumPartitions = 10;  // 10 Partitionen

FOR i = 0 to vNumPartitions - 1

    Customer_Partition:
    LOAD *
    FROM [lib://DB_Connection]
        (SQL SELECT * FROM Customers)
    WHERE Mod(Hash128(CustomerID), $(vNumPartitions)) = $(i);
    // Hash128 verteilt IDs gleichmäßig auf Partitionen
    
    STORE Customer_Partition INTO 
        [$(vPartitionPath)/Customers_P$(i).qvd] (qvd);
    
    DROP TABLE Customer_Partition;

NEXT i


// ═══════════════════════════════════════════════════════════
// PARALLELE VERARBEITUNG MIT SEPARATEN APPS
// ═══════════════════════════════════════════════════════════
// Erstellen Sie 10 separate Apps, jede lädt eine Partition
// Schedule sie gleichzeitig auf verschiedenen Nodes/Cores!
//
// App_Process_P0.qvf: Lädt Customers_P0.qvd
// App_Process_P1.qvf: Lädt Customers_P1.qvd
// ...
//
// Ergebnis: 10x schnellere Verarbeitung bei 10 Cores!

Wie funktioniert die Partition-Wartung und welche Best Practices gibt es?

// ═══════════════════════════════════════════════════════════
// PARTITION MAINTENANCE
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// 1. ALTE PARTITIONEN ARCHIVIEREN
// ═══════════════════════════════════════════════════════════

SET vPartitionPath = 'lib://QVDs/Partitions/Orders';
SET vArchivePath = 'lib://Archive/Orders';
LET vRetentionMonths = 24;  // Behalte 24 Monate online

// Alle QVD-Dateien auflisten
FileList:
LOAD
    @1 as FileName,
    @2 as FileSize,
    @3 as FileDate
FROM [$(vPartitionPath)/*.qvd]
(txt, delimiter is 't');

// Alte Dateien identifizieren
FOR i = 0 to NoOfRows('FileList') - 1
    
    LET vFile = Peek('FileName', i, 'FileList');
    LET vFileDate = Peek('FileDate', i, 'FileList');
    
    IF MonthName(vFileDate) < MonthName(AddMonths(Today(), -vRetentionMonths)) THEN
        
        // Zu Archive verschieben
        EXECUTE cmd /c move "$(vPartitionPath)$(vFile)" "$(vArchivePath)$(vFile)";
        TRACE Archived: $(vFile);
        
    END IF

NEXT i


// ═══════════════════════════════════════════════════════════
// 2. KORRUPTE PARTITIONEN REPARIEREN
// ═══════════════════════════════════════════════════════════

FOR i = 0 to NoOfRows('FileList') - 1
    
    LET vFile = Peek('FileName', i, 'FileList');
    LET vFileSize = Peek('FileSize', i, 'FileList');
    
    // Prüfen auf 0-Byte-Dateien (korrupt!)
    IF vFileSize = 0 THEN
        
        TRACE ⚠️ WARNUNG: Korrupte Partition gefunden: $(vFile);
        
        // Aus Backup restaurieren
        EXECUTE cmd /c copy "$(vArchivePath)$(vFile)" "$(vPartitionPath)$(vFile)";
        
        // Oder neu generieren
        // ... (Partition-Generator-Code hier) ...
        
    END IF

NEXT i


// ═══════════════════════════════════════════════════════════
// 3. PARTITION-KONSOLIDIERUNG
// ═══════════════════════════════════════════════════════════
// Tägliche → Monatliche Partitionen konsolidieren

LET vLastMonth = Date(MonthStart(Today() - 1), 'YYYY-MM');

// Alle Tages-Partitionen des letzten Monats laden
MonthlyData:
LOAD *
FROM [$(vPartitionPath)/Orders_$(vLastMonth)-*.qvd] (qvd);

// Als Monats-Partition speichern
STORE MonthlyData INTO 
    [$(vPartitionPath)/Orders_$(vLastMonth).qvd] (qvd);

// Tages-Partitionen löschen
EXECUTE cmd /c del "$(vPartitionPath)Orders_$(vLastMonth)-*.qvd";

TRACE Konsolidiert: $(vLastMonth) (Tages → Monats-Partition);

Wie kann ich QVD-Optimierung für 100x schnellere Loads in Qlik Sense durchführen?

6.1 Diagnostizieren: Warum ist mein Load nicht optimiert?

// ═══════════════════════════════════════════════════════════
// DEBUGGING-SCRIPT: Optimization-Status prüfen
// ═══════════════════════════════════════════════════════════

SET Verbatim = 1;  // Zeigt Qlik's Script-Interpretation

// Test 1: Komplett simpler Load
TestTable1:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);
// Im Log sollte stehen: "TestTable1 << Orders.qvd (qvd optimized)"

DROP TABLE TestTable1;


// Test 2: Mit Feld-Auswahl
TestTable2:
LOAD 
    OrderID,
    CustomerID,
    Amount
FROM [lib://QVDs/Orders.qvd] (qvd);
// Sollte auch optimized sein!

DROP TABLE TestTable2;


// Test 3: Mit Umbenennung
TestTable3:
LOAD 
    OrderID as Order_Number,
    CustomerID
FROM [lib://QVDs/Orders.qvd] (qvd);
// Sollte auch optimized sein!

DROP TABLE TestTable3;


// Test 4: Mit Berechnung (❌ bricht Optimierung)
TestTable4:
LOAD 
    OrderID,
    Year(OrderDate) as OrderYear  // ❌ Berechnung!
FROM [lib://QVDs/Orders.qvd] (qvd);
// Im Log: KEIN "(qvd optimized)" - das ist das Problem!

DROP TABLE TestTable4;

Im Script-Log suchen nach:

✓ OPTIMIZED:    Orders << Orders.qvd (qvd optimized)
❌ NOT OPTIMIZED: Orders << Orders.qvd

Was sind die häufigsten Performance-Probleme bei QVD-Optimierung in Qlik Sense?

Problem 1: Non-Optimized trotz einfachem Statement

// ═══════════════════════════════════════════════════════════
// PROBLEM
// ═══════════════════════════════════════════════════════════
Orders:
LOAD 
    OrderID,
    CustomerID,
    Date(OrderDate) as OrderDate  // ❌ Funktion!
FROM [lib://QVDs/Orders.qvd] (qvd);
// Non-Optimized! Dauert 5 Minuten für 10M Zeilen


// ═══════════════════════════════════════════════════════════
// LÖSUNG: Zwei-Stufen-Load
// ═══════════════════════════════════════════════════════════
TempOrders:
LOAD 
    OrderID,
    CustomerID,
    OrderDate  // Keine Transformation!
FROM [lib://QVDs/Orders.qvd] (qvd);
// ✓ Optimized! Dauert 30 Sekunden

Orders:
LOAD
    OrderID,
    CustomerID,
    Date(OrderDate) as OrderDate  // Jetzt transformieren
RESIDENT TempOrders;

DROP TABLE TempOrders;

// Total: 30s + 30s = 1 Minute (5x schneller!)

Problem 2: Memory Overflow

// ═══════════════════════════════════════════════════════════
// PROBLEM: Alle Tabellen gleichzeitig im RAM
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);     // 5 GB RAM
Customers:
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd);  // +2 GB RAM
Products:
LOAD * FROM [lib://QVDs/Products.qvd] (qvd);   // +1 GB RAM
// Peak RAM: 8 GB! System kann crashen


// ═══════════════════════════════════════════════════════════
// LÖSUNG: Sequentielles Laden mit DROP
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);

// Orders verarbeiten und als finale QVD speichern
STORE Orders INTO [lib://QVDs/Final/Orders.qvd] (qvd);
DROP TABLE Orders;  // ✓ 5 GB RAM freigegeben!

Customers:
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd);
STORE Customers INTO [lib://QVDs/Final/Customers.qvd] (qvd);
DROP TABLE Customers;  // ✓ Wieder RAM frei!

Products:
LOAD * FROM [lib://QVDs/Products.qvd] (qvd);
STORE Products INTO [lib://QVDs/Final/Products.qvd] (qvd);
DROP TABLE Products;

// Peak RAM: Nur 5 GB (maximal für größte Tabelle)


// ═══════════════════════════════════════════════════════════
// ZUSÄTZLICHE OPTIMIERUNG: High-Cardinality-Felder
// ═══════════════════════════════════════════════════════════
// IDs mit AutoNumber() optimieren
Orders:
LOAD
    AutoNumber(OrderID) as OrderID,      // Spart RAM!
    AutoNumber(CustomerID) as CustomerID,
    Amount
FROM [lib://QVDs/Orders.qvd] (qvd);

// Warum? AutoNumber() ersetzt lange String-IDs (z.B. GUIDs)
// mit kompakten Nummern → bis zu 70% RAM-Ersparnis!

Problem 3: QVD-Datei korrupt

// ═══════════════════════════════════════════════════════════
// DEFENSIVE QVD-ERSTELLUNG: Atomische Updates
// ═══════════════════════════════════════════════════════════

SET ErrorMode = 0;  // Fehler abfangen

// ═══════════════════════════════════════════════════════════
// Schritt 1: Daten laden und verarbeiten
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://DB_Connection] (SQL SELECT * FROM Orders);

LET vRecordCount = NoOfRows('Orders');
LET vExpectedRecords = 1000000;  // Erwartete Mindestanzahl


// ═══════════════════════════════════════════════════════════
// Schritt 2: Sanity Checks
// ═══════════════════════════════════════════════════════════
IF vRecordCount < vExpectedRecords * 0.9 THEN
    
    TRACE ⚠️ FEHLER: Nur $(vRecordCount) Zeilen geladen!;
    TRACE ⚠️ Erwartet: mindestens $(vExpectedRecords);
    
    // Alert senden
    EXECUTE cmd /c echo "QVD Generation failed: Low record count" | mail -s "ALERT" admin@company.com;
    
    // Script abbrechen (ohne QVD zu überschreiben!)
    EXIT SCRIPT;
    
END IF


// ═══════════════════════════════════════════════════════════
// Schritt 3: In TEMP-Datei speichern
// ═══════════════════════════════════════════════════════════
LET vTimestamp = Timestamp(Now(), 'YYYYMMDD_hhmmss');

STORE Orders INTO 
    [lib://QVDs/Orders_TEMP_$(vTimestamp).qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// Schritt 4: Prüfen ob STORE erfolgreich
// ═══════════════════════════════════════════════════════════
IF ScriptErrorCount = 0 AND 
   FileSize('lib://QVDs/Orders_TEMP_$(vTimestamp).qvd') > 0 THEN
    
    // ═══════════════════════════════════════════════════════════
    // Schritt 5: Backup der alten QVD erstellen
    // ═══════════════════════════════════════════════════════════
    IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
        EXECUTE cmd /c copy "$(vQVDPath)Orders.qvd" "$(vQVDPath)Orders_BACKUP.qvd";
        TRACE ✓ Backup erstellt;
    END IF
    
    
    // ═══════════════════════════════════════════════════════════
    // Schritt 6: Atomisches Replace (TEMP → FINAL)
    // ═══════════════════════════════════════════════════════════
    EXECUTE cmd /c move /Y "$(vQVDPath)Orders_TEMP_$(vTimestamp).qvd" "$(vQVDPath)Orders.qvd";
    
    TRACE ═══════════════════════════════════════;
    TRACE ✓ QVD erfolgreich aktualisiert!;
    TRACE   Zeilen: $(vRecordCount);
    TRACE   Zeitstempel: $(vTimestamp);
    TRACE ═══════════════════════════════════════;
    
ELSE
    
    TRACE ⚠️ FEHLER beim STORE - QVD NICHT überschrieben!;
    TRACE ⚠️ Temp-Datei verbleibt zur Analyse;
    
END IF

DROP TABLE Orders;

Problem 4: Performance degradiert mit der Zeit

// ═══════════════════════════════════════════════════════════
// CLEANUP-SCRIPT: Alte/Temporäre QVDs aufräumen
// ═══════════════════════════════════════════════════════════
// Schedule: Wöchentlich

SET vQVDPath = 'lib://QVDs';
LET vRetentionDays = 30;
LET vCutoffDate = Today() - vRetentionDays;

// Liste aller QVD-Dateien
FileList:
LOAD
    FileBaseName() as FileName,
    FileTime() as FileDate,
    FileSize() as FileSizeMB
FROM [$(vQVDPath)/*.qvd]
(fix, codepage is 1252);

// Alte Dateien identifizieren
FOR i = 0 to NoOfRows('FileList') - 1
    
    LET vFile = Peek('FileName', i, 'FileList');
    LET vFileDate = Peek('FileDate', i, 'FileList');
    LET vFileSize = Peek('FileSizeMB', i, 'FileList');
    
    // Löschen wenn:
    // - Älter als Retention
    // - ODER Name enthält "TEMP" oder "OLD"
    // - ODER Größe = 0 (korrupt)
    
    IF vFileDate < vCutoffDate OR
       WildMatch('$(vFile)', '*TEMP*', '*OLD*', '*_test*') > 0 OR
       vFileSize = 0 THEN
        
        EXECUTE cmd /c del "$(vQVDPath)$(vFile).qvd";
        TRACE ✓ Gelöscht: $(vFile) ($(vFileDate), $(vFileSize) MB);
        
    END IF

NEXT i

DROP TABLE FileList;


// ═══════════════════════════════════════════════════════════
// WEEKLY FULL RELOAD (gegen Data Drift)
// ═══════════════════════════════════════════════════════════
// Auch bei Incremental Loads: Einmal pro Woche Full Reload!

IF WeekDay(Today()) = 0 THEN  // Sonntag
    
    TRACE ═══════════════════════════════════════;
    TRACE WEEKLY FULL RELOAD gestartet;
    TRACE ═══════════════════════════════════════;
    
    // Incremental-Load-Tracking zurücksetzen
    LET vLastLoadDate = '1900-01-01';
    
    // Jetzt lädt alles als Full Reload...
    
END IF

Wie implementiert man Performance-Monitoring in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// QVD PERFORMANCE MONITORING
// ═══════════════════════════════════════════════════════════
// Diese Metrics in separate Monitoring-Tabelle loggen
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// Performance-Metriken sammeln
// ═══════════════════════════════════════════════════════════

PerformanceLog:
LOAD * INLINE [
    Metric, Value, Unit, Timestamp
];

// Startzeit merken
LET vStartTime = Now();

// Daten laden
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);

// Endzeit und Dauer
LET vEndTime = Now();
LET vDuration = Interval(vEndTime - vStartTime, 's');
LET vRecordCount = NoOfRows('Orders');
LET vThroughput = vRecordCount / vDuration;  // Rows per second

// Metriken in Tabelle schreiben
CONCATENATE (PerformanceLog)
LOAD * INLINE [
    Metric, Value, Unit, Timestamp
    Orders_LoadTime, $(vDuration), Seconds, $(vEndTime)
    Orders_RecordCount, $(vRecordCount), Rows, $(vEndTime)
    Orders_Throughput, $(vThroughput), Rows/Sec, $(vEndTime)
    Orders_FileSize, $(FileSize('lib://QVDs/Orders.qvd')), Bytes, $(vEndTime)
];

// Logs persistent speichern
STORE PerformanceLog INTO [lib://Monitoring/Performance_Log.qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// ALERTING: Performance-Schwellenwerte prüfen
// ═══════════════════════════════════════════════════════════

IF vDuration > 300 THEN  // Mehr als 5 Minuten
    
    TRACE ⚠️ PERFORMANCE ALERT: Load dauerte $(vDuration)s!;
    
    // Email-Alert senden
    EXECUTE powershell -Command "Send-MailMessage -To 'admin@company.com' -From 'qlik@company.com' -Subject 'QVD Performance Alert' -Body 'Orders.qvd Load time: $(vDuration)s' -SmtpServer 'smtp.company.com'";
    
END IF

IF vRecordCount < 1000000 * 0.9 THEN  // 10% weniger als erwartet
    
    TRACE ⚠️ DATA QUALITY ALERT: Nur $(vRecordCount) Zeilen!;
    
END IF

Was sind die Cloud-Native QVD-Strategien für 2024-2025?

Was sind die Besonderheiten und Limits von Qlik Cloud?

Kritische Unterschiede zu On-Premise:

AspektOn-PremiseQlik Cloud SaaS
Max QVD SizeUnbegrenzt (RAM-limitiert)6 GB empfohlen
StorageLokales FilesystemS3/Azure Blob/Google Cloud
Connection Stringslib:// mit lokalen PfadenSpace-aware Syntax
SchedulingQMC/Task SchedulerCloud Automations
Parallel ProcessingMulti-Core ServerElastic Cloud Capacity

Was ist die Cloud-QVD-Architektur in Bezug auf QVD-Optimierung?

// ═══════════════════════════════════════════════════════════
// QLIK CLOUD: Space-Aware QVD Access
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// SZENARIO 1: QVDs im gleichen Space
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://DataFiles/Orders.qvd] (qvd);
// Standard Syntax - funktioniert!


// ═══════════════════════════════════════════════════════════
// SZENARIO 2: QVDs in anderem Space (Cross-Space Access)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://ETL_Space:DataFiles/Orders.qvd] (qvd);
//          ^^^^^^^^^^ Space-Name!
// Zugriff über Space-Grenzen hinweg


// ═══════════════════════════════════════════════════════════
// SZENARIO 3: Hybrid (On-Premise QVDs für Cloud-App)
// ═══════════════════════════════════════════════════════════
// Via Data Gateway verbunden:
Orders:
LOAD *
FROM [lib://OnPrem_Gateway:QVDs/Orders.qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// SPEICHERN in verschiedene Spaces
// ═══════════════════════════════════════════════════════════
STORE Orders INTO 
    [lib://Production_Space:DataFiles/Orders.qvd] (qvd);

Was sind die Cloud Storage Connectors in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// AMAZON S3
// ═══════════════════════════════════════════════════════════
// Connection in Qlik Cloud erstellt als "S3_Bucket"

// Lesen
Orders:
LOAD *
FROM [lib://S3_Bucket/qlik/data/Orders.qvd] (qvd);

// Schreiben
STORE Orders INTO 
    [lib://S3_Bucket/qlik/data/Orders_$(vDate).qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// AZURE BLOB STORAGE
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://Azure_Storage/qlik-container/Orders.qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// GOOGLE DRIVE (nur lesen!)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://GoogleDrive/Qlik Data/Orders.qvd] (qvd);
// ⚠️ STORE funktioniert NICHT - nur Read-Only!


// ═══════════════════════════════════════════════════════════
// GOOGLE CLOUD STORAGE
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://GCS_Bucket/qlik/Orders.qvd] (qvd);

STORE Orders INTO 
    [lib://GCS_Bucket/qlik/Orders_$(vDate).qvd] (qvd);

Was ist Cloud-Optimierte Partitionierung und wie funktioniert das 6GB-Limit?

// ═══════════════════════════════════════════════════════════
// CLOUD-STRATEGIE: Kleinere Partitionen für 6GB-Limit
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// On-Premise (alte Strategie):
// - Jahres-Partitionen: 12 Monate à 10 GB = 120 GB/Jahr
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// Cloud (neue Strategie):
// - Monats-Partitionen: 1 Monat à 5 GB = safe für Cloud!
// ═══════════════════════════════════════════════════════════

SET vS3Bucket = 'lib://S3_Bucket/qlik/partitions';

FOR i = 0 to 35  // Letzten 36 Monate

    LET vMonth = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
    LET vMonthStart = Date(MonthStart(Today(), -$(i)), 'YYYY-MM-DD');
    LET vMonthEnd = Date(MonthEnd(Today(), -$(i)), 'YYYY-MM-DD');
    
    Orders_Partition:
    LOAD *
    FROM [lib://SourceDB]
        (SQL SELECT * FROM Orders 
         WHERE OrderDate >= '$(vMonthStart)' 
           AND OrderDate <= '$(vMonthEnd)');
    
    // Partition Size prüfen
    LET vPartitionSize = NoOfRows('Orders_Partition') * 100 / 1024 / 1024;  // Grobe Schätzung in GB
    
    IF vPartitionSize > 5 THEN
        
        TRACE ⚠️ WARNUNG: Partition $(vMonth) ist $(vPartitionSize) GB!;
        TRACE ⚠️ Überlegen Sie weitere Aufteilung (z.B. nach Region);
        
        // Option: Nach Region weiter aufteilen
        FOR EACH vRegion in 'EMEA', 'AMER', 'APAC'
            
            RegionalOrders:
            LOAD *
            RESIDENT Orders_Partition
            WHERE Region = '$(vRegion)';
            
            STORE RegionalOrders INTO 
                [$(vS3Bucket)/Orders_$(vMonth)_$(vRegion).qvd] (qvd);
            
            DROP TABLE RegionalOrders;
        NEXT vRegion
        
    ELSE
        // Partition OK, normal speichern
        STORE Orders_Partition INTO 
            [$(vS3Bucket)/Orders_$(vMonth).qvd] (qvd);
    END IF
    
    DROP TABLE Orders_Partition;

NEXT i


// ═══════════════════════════════════════════════════════════
// CONSUMER: Intelligentes Laden mit dynamischer Erkennung
// ═══════════════════════════════════════════════════════════

Orders:
LOAD *
FROM [$(vS3Bucket)/Orders_2024-*.qvd] (qvd);
// Lädt alle Monate UND alle Regionen via Wildcard!

Wie funktioniert CI/CD und Automation in der Cloud für QVD-Optimierung in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// QLIK CLOUD AUTOMATIONS: QVD-Generation automatisieren
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// AUTOMATION TRIGGER: Täglich 02:00 Uhr
// ═══════════════════════════════════════════════════════════
// 1. Start App Reload: "01_Extract_Orders"
// 2. Warten auf Success
// 3. Start App Reload: "02_Transform_Orders"
// 4. Warten auf Success
// 5. Start App Reload: "03_Sales_Dashboard"
// 6. Bei Fehler: Send Email Alert


// ═══════════════════════════════════════════════════════════
// ODER: Via REST API (für externe Orchestrierung)
// ═══════════════════════════════════════════════════════════

// PowerShell Script (läuft extern):
$tenant = "your-tenant.region.qlikcloud.com"
$appId = "abc123..."
$apiKey = "Bearer your-api-key"

$headers = @{
    "Authorization" = $apiKey
    "Content-Type" = "application/json"
}

$uri = "https://$tenant/api/v1/reloads"
$body = @{
    "appId" = $appId
    "partial" = $false
} | ConvertTo-Json

$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body

Write-Host "Reload started: $($response.id)"


// ═══════════════════════════════════════════════════════════
// GIT INTEGRATION: Version Control für QVD-Generator-Apps
// ═══════════════════════════════════════════════════════════

// In Qlik Cloud: App mit Git Repository verbinden
// 1. Settings > Version Control
// 2. Connect to Git (GitHub/GitLab/Azure DevOps)
// 3. Branch: main / develop / feature/*

// Workflow:
// - Developer: Ändert Script in "develop" Branch
// - Commit: "Added region-based partitioning"
// - Pull Request → Review
// - Merge to "main"
// - Qlik Cloud: Auto-Deploy zu Production Space

// Script-Beispiel für Git-freundliches Logging:
// vVersion = '1.5.2';
// vLastModified = '2024-01-15';
// vAuthor = 'data.team@company.com';

Ist Parquet eine Alternative oder Ergänzung zu QVD in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// PARQUET: Open-Source Alternative mit Cloud-Vorteilen
// ═══════════════════════════════════════════════════════════
// Seit Qlik Sense Mai 2024: Native Parquet Support!
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// Laden aus Parquet (neue Syntax!)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://DataLake/orders.parquet]
(parquet);


// ═══════════════════════════════════════════════════════════
// VERGLEICH: QVD vs PARQUET
// ═══════════════════════════════════════════════════════════

// QVD Vorteile:
// ✓ 10-30% schneller als Parquet in Qlik
// ✓ Qlik-spezifische Metadaten (Comments, Tags)
// ✓ Perfekt für Qlik-only Pipelines

// Parquet Vorteile:
// ✓ 50-100x kleinere Dateien (bessere Kompression!)
// ✓ Plattform-unabhängig (Python, Spark, ML)
// ✓ Cloud-native (S3, ADLS, GCS standard)
// ✓ Kostenersparnis (weniger Storage)


// ═══════════════════════════════════════════════════════════
// HYBRID-STRATEGIE: Best of Both Worlds
// ═══════════════════════════════════════════════════════════

// ┌──────────────────────────────────────────┐
// │  SOURCE SYSTEMS                          │
// │  (SQL, SAP, APIs)                        │
// └─────────────┬────────────────────────────┘
//               ▼
// ┌──────────────────────────────────────────┐
// │  EXTRACT LAYER (QVD)                     │
// │  - Optimiert für Qlik-Reloads            │
// │  - Schnell, Qlik-intern                  │
// └─────────────┬────────────────────────────┘
//               ▼
// ┌──────────────────────────────────────────┐
// │  TRANSFORM LAYER                         │
// │  ├─ Business QVDs (für Qlik Apps)        │
// │  └─ Parquet Files (für Data Science)     │
// └──────────────┬───────────────────────────┘
//                │
//      ┌─────────┴──────────┐
//      ▼                    ▼
// ┌─────────┐         ┌──────────┐
// │  Qlik   │         │ Python   │
// │  Apps   │         │ ML/AI    │
// └─────────┘         └──────────┘


// Praktische Implementierung:
Transform_Orders:
LOAD * FROM [...];  // Daten transformieren

// Für Qlik: QVD
STORE Transform_Orders INTO 
    [lib://QVDs/Transform/Orders.qvd] (qvd);

// Für Data Science: Parquet
STORE Transform_Orders INTO 
    [lib://DataLake/transform/orders.parquet] (parquet);

// Nun können beide Teams optimal arbeiten!

Wie kann ich Governance, Monitoring und Best Practices für QVD-Optimierung umsetzen?

Was ist das QVD-Governance-Framework für schnellere Loads in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// QVD METADATA MANAGEMENT
// ═══════════════════════════════════════════════════════════
// Zentrale Metadata-Tabelle für alle QVDs
// ═══════════════════════════════════════════════════════════

QVD_Catalog:
LOAD * INLINE [
    QVD_Name, Layer, Source_System, Owner, Refresh_Frequency, Description, Created_Date, Last_Modified
    Orders.qvd, Extract, ERP_SQL, data.team@company.com, Daily, Raw order data from ERP system, 2023-01-15, 2024-01-15
    Fact_Orders.qvd, Transform, Orders.qvd, data.team@company.com, Daily, Business-ready orders with transformations, 2023-01-15, 2024-01-15
    Dim_Customer.qvd, Transform, CRM_SQL, data.team@company.com, Weekly, Customer master data, 2023-01-15, 2024-01-10
];

STORE QVD_Catalog INTO [lib://Metadata/QVD_Catalog.qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// AUTOMATISCHES METADATA-SCANNING
// ═══════════════════════════════════════════════════════════
// Scannt QVD-Verzeichnis und extrahiert Metadata
// ═══════════════════════════════════════════════════════════

QVD_Inventory:
LOAD
    QvdTableName(FileName) as TableName,
    QvdNoOfRecords(FileName) as RecordCount,
    QvdNoOfFields(FileName) as FieldCount,
    FileSize(FileName) / 1024 / 1024 as FileSizeMB,
    FileTime(FileName) as LastModified,
    FileName
FROM [lib://QVDs/Transform/*.qvd]
(QVD);

// Feldliste für jedes QVD
FOR i = 0 to NoOfRows('QVD_Inventory') - 1
    
    LET vFile = Peek('FileName', i, 'QVD_Inventory');
    LET vTable = Peek('TableName', i, 'QVD_Inventory');
    LET vFieldCount = Peek('FieldCount', i, 'QVD_Inventory');
    
    FOR j = 0 to vFieldCount - 1
        
        LET vFieldName = QvdFieldName('$(vFile)', j);
        
        CONCATENATE (QVD_Fields)
        LOAD * INLINE [
            QVD_Name, Table_Name, Field_Name
            $(vFile), $(vTable), $(vFieldName)
        ];
        
    NEXT j

NEXT i

STORE QVD_Inventory INTO [lib://Metadata/QVD_Inventory.qvd] (qvd);
STORE QVD_Fields INTO [lib://Metadata/QVD_Fields.qvd] (qvd);

Wie funktioniert das Data Lineage Tracking in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// LINEAGE TRACKING: Wer nutzt welche QVDs?
// ═══════════════════════════════════════════════════════════

DataLineage:
LOAD * INLINE [
    Source_QVD, Target_App, App_Owner, Load_Frequency
    Extract_Orders.qvd, 01_Transform_Orders.qvf, ETL_Team, Daily
    Fact_Orders.qvd, Sales_Dashboard.qvf, Sales_Team, Hourly
    Fact_Orders.qvd, Finance_Report.qvf, Finance_Team, Daily
    Fact_Orders.qvd, Executive_KPIs.qvf, Management, Daily
    Dim_Customer.qvd, Sales_Dashboard.qvf, Sales_Team, Hourly
    Dim_Customer.qvd, Marketing_Analytics.qvf, Marketing_Team, Weekly
];

// Impact Analysis: Wenn Fact_Orders.qvd sich ändert?
ImpactAnalysis:
LOAD
    Source_QVD,
    Count(DISTINCT Target_App) as Affected_Apps,
    Concat(DISTINCT Target_App, ', ') as App_List
RESIDENT DataLineage
WHERE Source_QVD = 'Fact_Orders.qvd'
GROUP BY Source_QVD;

// Resultat: "3 Apps betroffen: Sales_Dashboard, Finance_Report, Executive_KPIs"

Wie wird Qualitätssicherung und Testing in QVD-Optimierung durchgeführt?

// ═══════════════════════════════════════════════════════════
// DATA QUALITY CHECKS für QVDs
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// Check 1: Record Count im erwarteten Bereich?
// ═══════════════════════════════════════════════════════════

QualityCheck_RecordCount:
LOAD
    'Orders' as TableName,
    QvdNoOfRecords('lib://QVDs/Orders.qvd') as ActualCount,
    1000000 as ExpectedMin,
    5000000 as ExpectedMax
AUTOGENERATE 1;

// Validation
IF Peek('ActualCount', 0) < Peek('ExpectedMin', 0) OR 
   Peek('ActualCount', 0) > Peek('ExpectedMax', 0) THEN
    
    TRACE ⚠️ DATA QUALITY ALERT: Orders.qvd außerhalb erwarteten Bereichs!;
    // Alert senden
    
END IF


// ═══════════════════════════════════════════════════════════
// Check 2: Duplikate in Primary Key?
// ═══════════════════════════════════════════════════════════

Orders_Test:
LOAD 
    OrderID,
    Count(OrderID) as DuplicateCount
FROM [lib://QVDs/Orders.qvd] (qvd)
GROUP BY OrderID
HAVING Count(OrderID) > 1;

IF NoOfRows('Orders_Test') > 0 THEN
    
    TRACE ⚠️ DATA QUALITY ALERT: $(NoOfRows('Orders_Test')) Duplikate in OrderID!;
    
    // Duplikate loggen
    STORE Orders_Test INTO [lib://Logs/Duplicates_$(Date(Today(),'YYYYMMDD')).qvd] (qvd);
    
END IF

DROP TABLE Orders_Test;


// ═══════════════════════════════════════════════════════════
// Check 3: NULL-Werte in kritischen Feldern?
// ═══════════════════════════════════════════════════════════

Orders_Nulls:
LOAD
    'CustomerID' as FieldName,
    Sum(If(IsNull(CustomerID), 1, 0)) as NullCount
FROM [lib://QVDs/Orders.qvd] (qvd)
UNION
LOAD
    'Amount' as FieldName,
    Sum(If(IsNull(Amount), 1, 0)) as NullCount
FROM [lib://QVDs/Orders.qvd] (qvd);

// Alle Checks zusammenfassen
QualityReport:
LOAD
    Today() as CheckDate,
    FieldName,
    NullCount,
    If(NullCount > 0, 'FAIL', 'PASS') as Status
RESIDENT Orders_Nulls;

STORE QualityReport INTO 
    [lib://Quality/DailyChecks_$(Date(Today(),'YYYYMMDD')).qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// Check 4: Datums-Konsistenz
// ═══════════════════════════════════════════════════════════

DateConsistency:
LOAD
    OrderID,
    OrderDate,
    ShipDate
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE ShipDate < OrderDate;  // Shipdate vor Orderdate = unmöglich!

IF NoOfRows('DateConsistency') > 0 THEN
    TRACE ⚠️ DATA QUALITY ALERT: $(NoOfRows('DateConsistency')) Orders mit ShipDate < OrderDate!;
END IF

Was sind die Benennungsrichtlinien und die Ordnerstruktur in Qlik Sense?

📁 QVD Root Directory
│
├── 📁 01_Extract/              ← Layer 1: Raw Data
│   ├── Extract_SAP_BSEG_2024-01-15.qvd
│   ├── Extract_SAP_BSEG_CURRENT.qvd
│   ├── Extract_SQL_Orders_2024-01-15.qvd
│   ├── Extract_SQL_Orders_CURRENT.qvd
│   └── 📁 Archive/
│       └── Extract_SQL_Orders_2024-01-01.qvd
│
├── 📁 02_Transform/            ← Layer 2: Business Logic
│   ├── Fact_Sales.qvd
│   ├── Fact_Orders.qvd
│   ├── Dim_Customer.qvd
│   ├── Dim_Product.qvd
│   ├── Bridge_Order_Product.qvd
│   └── Master_Calendar.qvd
│
├── 📁 03_Partitions/           ← Partitionierte QVDs
│   ├── 📁 Orders/
│   │   ├── Orders_2024-01.qvd
│   │   ├── Orders_2024-02.qvd
│   │   └── Orders_2024-03.qvd
│   └── 📁 Transactions/
│       ├── Trans_2024-01-01.qvd
│       └── Trans_2024-01-02.qvd
│
├── 📁 04_Metadata/             ← Governance & Tracking
│   ├── QVD_Catalog.qvd
│   ├── QVD_Inventory.qvd
│   ├── DataLineage.qvd
│   └── QualityChecks.qvd
│
├── 📁 05_Temp/                 ← Temporäre QVDs
│   └── (werden täglich bereinigt)
│
└── 📁 06_Archive/              ← Alte Versionen (Backup)
    └── (Retention: 90 Tage)


═══════════════════════════════════════════════════════════
NAMING CONVENTIONS
═══════════════════════════════════════════════════════════

Extract Layer:
    Format: Extract_<SourceSystem>_<TableName>_<Timestamp>.qvd
    Beispiel: Extract_SAP_BSEG_2024-01-15.qvd
    
Transform Layer:
    Format: <Type>_<EntityName>.qvd
    Typen:
        - Fact_*     (Faktentabellen)
        - Dim_*      (Dimensionen)
        - Bridge_*   (Many-to-Many)
        - Master_*   (Referenzdaten)
    Beispiele:
        - Fact_Sales.qvd
        - Dim_Customer.qvd
        - Bridge_Order_Product.qvd
        - Master_ExchangeRates.qvd

Partitions:
    Format: <EntityName>_<PartitionKey>.qvd
    Beispiele:
        - Orders_2024-01.qvd          (Zeit)
        - Orders_2024_EMEA.qvd        (Jahr × Region)
        - Trans_2024-01-15_H14.qvd    (Tag × Stunde)

Temporäre Files:
    Format: <Name>_TEMP_<Timestamp>.qvd
    Beispiel: Orders_TEMP_20240115_143022.qvd

Wie funktioniert Section Access mit QVDs in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// SECTION ACCESS: Row-Level Security
// ═══════════════════════════════════════════════════════════
// ⚠️ WICHTIG: Section Access BRICHT QVD-Optimierung!
//    Aber das ist OK - Security geht vor Performance
// ═══════════════════════════════════════════════════════════

Section Access;

// Benutzer-Tabelle (normalerweise aus AD/LDAP oder QVD)
ACCESS:
LOAD * INLINE [
    ACCESS, USERID, REGION
    USER, JOHN.DOE, EMEA
    USER, JANE.SMITH, AMER
    USER, MIKE.JONES, APAC
    ADMIN, ADMIN, *
];

Section Application;

// Daten laden (wird per User gefiltert!)
Orders:
LOAD
    OrderID,
    CustomerID,
    Region,        // ← Muss in Section Access sein!
    Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE 1=1;        // Erzwingt Non-Optimized (für Section Access nötig)

// User sieht nur seine Region:
// - JOHN.DOE sieht nur EMEA
// - JANE.SMITH sieht nur AMER
// - ADMIN sieht alles (*)


// ═══════════════════════════════════════════════════════════
// ALTERNATIVE: Separate QVDs pro Region (optimiert!)
// ═══════════════════════════════════════════════════════════

// Besser für Performance:
// 1. Erstelle Orders_EMEA.qvd, Orders_AMER.qvd, Orders_APAC.qvd
// 2. User lädt nur sein QVD (Region via Variable)
// 3. Bleibt optimiert!

LET vUserRegion = OSUser();  // Oder aus Datenbank laden

IF vUserRegion = 'EMEA' THEN
    Orders:
    LOAD * FROM [lib://QVDs/Orders_EMEA.qvd] (qvd);
ELSEIF vUserRegion = 'AMER' THEN
    Orders:
    LOAD * FROM [lib://QVDs/Orders_AMER.qvd] (qvd);
END IF

// Resultat: Optimized Load + Security!

Was sind Advanced Patterns und Techniken zur QVD-Optimierung in Qlik Sense?

Wie funktioniert das dynamische QVD-Loading in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// PATTERN: User wählt Zeitraum → App lädt nur benötigte QVDs
// ═══════════════════════════════════════════════════════════

// Variable: User-Selection (aus UI oder Parameter)
LET vStartDate = '2024-01-01';
LET vEndDate = '2024-06-30';

// Berechne benötigte Monate
LET vStartMonth = Date(MonthStart(vStartDate), 'YYYY-MM');
LET vEndMonth = Date(MonthStart(vEndDate), 'YYYY-MM');

// Loop durch Monate zwischen Start und End
LET vCurrentMonth = vStartMonth;

DO WHILE Date#(vCurrentMonth, 'YYYY-MM') <= Date#(vEndMonth, 'YYYY-MM')
    
    LET vQVDFile = 'lib://QVDs/Partitions/Orders_' & vCurrentMonth & '.qvd';
    
    IF FileSize('$(vQVDFile)') > 0 THEN
        
        CONCATENATE (Orders)
        LOAD *
        FROM [$(vQVDFile)] (qvd);
        
        TRACE ✓ Geladen: Orders_$(vCurrentMonth);
        
    ELSE
        TRACE ⚠ Nicht gefunden: Orders_$(vCurrentMonth);
    END IF
    
    // Zum nächsten Monat
    LET vCurrentMonth = Date(AddMonths(Date#(vCurrentMonth, 'YYYY-MM'), 1), 'YYYY-MM');

LOOP


// ═══════════════════════════════════════════════════════════
// RESULTAT
// ═══════════════════════════════════════════════════════════
// User wählt Jan-Jun 2024:
// → App lädt 6 Partitionen (6 × 500 MB = 3 GB)
//
// User wählt komplettes Jahr:
// → App lädt 12 Partitionen (12 × 500 MB = 6 GB)
//
// Flexibel und performant!

Wie funktioniert die Multi-Threaded QVD Generation in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// PARALLEL QVD GENERATION via separate Apps
// ═══════════════════════════════════════════════════════════
// Ideal für Multi-Core-Server!
// ═══════════════════════════════════════════════════════════

// ═══════════════════════════════════════════════════════════
// ORCHESTRATOR APP: Startet parallel Apps
// ═══════════════════════════════════════════════════════════

// Liste aller zu generierenden Partitionen
PartitionList:
LOAD * INLINE [
    Partition
    2024-01
    2024-02
    2024-03
    2024-04
    2024-05
    2024-06
];

// Via QMC Tasks parallel starten (oder API Calls):
FOR i = 0 to NoOfRows('PartitionList') - 1
    
    LET vPartition = Peek('Partition', i, 'PartitionList');
    
    // REST API Call um Task zu starten
    EXECUTE powershell -Command "Invoke-RestMethod -Uri 'https://qlik-server/qrs/task/start/full?name=Generate_$(vPartition)' -Method Post -Headers @{...}";
    
    TRACE Gestartet: Generate_$(vPartition);

NEXT i

// ═══════════════════════════════════════════════════════════
// WORKER APP: Generate_2024-01.qvf (eine pro Partition)
// ═══════════════════════════════════════════════════════════

LET vPartition = '2024-01';  // Über Variable übergeben

Orders_Partition:
LOAD *
FROM [lib://DB]
(SQL SELECT * FROM Orders WHERE ... $(vPartition) ...);

STORE Orders_Partition INTO 
    [lib://QVDs/Orders_$(vPartition).qvd] (qvd);


// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH
// ═══════════════════════════════════════════════════════════
// 12 Partitionen à 5 Min = 60 Min sequential
// 12 Partitionen parallel auf 8-Core = 15 Min!
// → 4x schneller!

Was sind Self-Healing QVD Pipelines in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// SELF-HEALING: Automatische Retry und Fallback
// ═══════════════════════════════════════════════════════════

SET ErrorMode = 0;  // Fehler nicht abbrechen
LET vMaxRetries = 3;
LET vRetryCount = 0;
LET vSuccess = 0;

// ═══════════════════════════════════════════════════════════
// RETRY LOOP
// ═══════════════════════════════════════════════════════════

DO WHILE vRetryCount < vMaxRetries AND vSuccess = 0

    TRACE ═══════════════════════════════════════;
    TRACE QVD Generation Versuch $(vRetryCount + 1)/$(vMaxRetries);
    TRACE ═══════════════════════════════════════;
    
    // Daten laden
    Orders:
    LOAD *
    FROM [lib://DB_Connection]
        (SQL SELECT * FROM Orders);
    
    // Erfolgreich?
    IF ScriptErrorCount = 0 AND NoOfRows('Orders') > 0 THEN
        
        // QVD speichern
        STORE Orders INTO [lib://QVDs/Orders_TEMP.qvd] (qvd);
        
        IF ScriptErrorCount = 0 THEN
            // Atomisches Replace
            EXECUTE cmd /c move /Y "$(vQVDPath)Orders_TEMP.qvd" "$(vQVDPath)Orders.qvd";
            
            LET vSuccess = 1;
            TRACE ✓ Erfolgreich im Versuch $(vRetryCount + 1);
        END IF
        
    ELSE
        // Fehler aufgetreten
        LET vRetryCount = vRetryCount + 1;
        TRACE ⚠ Fehler - warte 60 Sekunden vor Retry...;
        
        // Warten vor Retry
        EXECUTE timeout /t 60 /nobreak > nul;
    END IF

LOOP

// ═══════════════════════════════════════════════════════════
// FALLBACK: Wenn alle Retries fehlschlagen
// ═══════════════════════════════════════════════════════════

IF vSuccess = 0 THEN
    
    TRACE ⚠️⚠️⚠️ KRITISCHER FEHLER ⚠️⚠️⚠️;
    TRACE Alle $(vMaxRetries) Versuche fehlgeschlagen;
    
    // Fallback: Verwende gestrige QVD
    IF FileSize('lib://QVDs/Orders_BACKUP.qvd') > 0 THEN
        
        TRACE → Fallback: Verwende Backup-QVD;
        
        EXECUTE cmd /c copy "$(vQVDPath)Orders_BACKUP.qvd" "$(vQVDPath)Orders.qvd";
        
        // Alert: Admins benachrichtigen
        EXECUTE powershell -Command "Send-MailMessage -To 'admin@company.com' -Subject 'QVD FALLBACK ACTIVATED' -Body 'Orders.qvd verwendet Backup!' ...";
        
    ELSE
        TRACE ⚠️ Kein Backup verfügbar - kritischer Fehler!;
        EXIT SCRIPT;
    END IF
    
END IF

Wie funktioniert die QVD-Compression-Analyse in Qlik Sense?

// ═══════════════════════════════════════════════════════════
// COMPRESSION ANALYSIS: Wie gut komprimiert Ihre QVD?
// ═══════════════════════════════════════════════════════════

Orders:
LOAD * FROM [lib://DB] (SQL SELECT * FROM Orders);

// Memory-Größe im RAM
LET vMemorySizeMB = Num(DocumentSize() / 1024 / 1024, '#,##0.00');

// QVD speichern
STORE Orders INTO [lib://QVDs/Orders_Test.qvd] (qvd);

// QVD-Dateigröße
LET vFileSizeMB = Num(FileSize('lib://QVDs/Orders_Test.qvd') / 1024 / 1024, '#,##0.00');

// Compression Ratio
LET vCompressionRatio = Num(vMemorySizeMB / vFileSizeMB, '#,##0.0');

TRACE ═══════════════════════════════════════;
TRACE COMPRESSION ANALYSIS:
TRACE Memory Size: $(vMemorySizeMB) MB;
TRACE File Size: $(vFileSizeMB) MB;
TRACE Compression: $(vCompressionRatio):1;
TRACE ═══════════════════════════════════════;

// Interpretation:
// - Compression < 2:1  → Schlecht (viele unique values)
// - Compression 2-5:1  → Normal
// - Compression > 5:1  → Gut (viele repetitive values)


// ═══════════════════════════════════════════════════════════
// FIELD-LEVEL COMPRESSION ANALYSIS
// ═══════════════════════════════════════════════════════════

FOR i = 0 to NoOfFields('Orders') - 1
    
    LET vField = FieldName(i, 'Orders');
    LET vDistinctValues = FieldValueCount('$(vField)');
    LET vTotalRows = NoOfRows('Orders');
    LET vCardinality = vDistinctValues / vTotalRows;
    
    TRACE Field: $(vField);
    TRACE   Distinct: $(vDistinctValues);
    TRACE   Cardinality: $(Num(vCardinality, '#,##0.0%'));
    TRACE   Compression Potential: $(If(vCardinality < 0.1, 'Hoch', If(vCardinality < 0.5, 'Mittel', 'Niedrig')));
    TRACE;

NEXT i

// Hohe Cardinality (> 50%) → Kandidat für:
// - AutoNumber() (bei IDs)
// - Separate Dimension Table (bei Strings)
// - Splitting (bei Timestamps)

Wie sieht die Zusammenfassung und die Checklisten zur QVD-Optimierung aus?

Was sind die Schritte in der QVD-Optimierungs-Checkliste?

Grundlagen (Jedes Projekt!)

  • [ ] QVDs nutzen statt direkte DB-Connections für wiederholte Loads
  • [ ] Optimierte Loads prüfen im Script-Log («qvd optimized»)
  • [ ] Keine Berechnungen im QVD-LOAD – erst laden, dann transformieren
  • [ ] WHERE EXISTS() statt WHERE-Vergleiche für Filter
  • [ ] DROP TABLE nach STORE um RAM freizugeben
  • [ ] Atomische QVD-Updates (via TEMP → FINAL Rename)

Performance (für Apps > 1M Zeilen)

  • [ ] Zwei-Stufen-Load implementiert (Optimized + Resident Transform)
  • [ ] Felder-Auswahl – nur benötigte Felder laden
  • [ ] AutoNumber() für hochkardinalische ID-Felder
  • [ ] Timestamp-Splitting (Date + Time statt Timestamp)
  • [ ] Incremental Loading für große Tabellen
  • [ ] Performance-Monitoring mit Benchmarks

Enterprise (für Production-Umgebungen)

  • [ ] Drei-Schichten-Architektur (Extract-Transform-Presentation)
  • [ ] Naming Conventions etabliert und dokumentiert
  • [ ] Folder-Struktur klar gegliedert (01_Extract, 02_Transform, etc.)
  • [ ] Error Handling mit Retries und Alerting
  • [ ] Backup-Strategie (wöchentliche Kopien, Archivierung)
  • [ ] Metadata-Management (Katalog, Lineage, Inventory)

Skalierung (für > 50M Zeilen)

  • [ ] Partitionierung nach Zeit/Region/Hash
  • [ ] Cloud-Limits beachtet (6 GB max pro QVD)
  • [ ] Parallele Verarbeitung wo möglich
  • [ ] Monitoring-Dashboard für QVD-Health
  • [ ] Automatisierte Cleanup-Prozesse

Governance

  • [ ] Data Lineage dokumentiert (wer nutzt was?)
  • [ ] Quality Checks automatisiert
  • [ ] Version Control (Git-Integration)
  • [ ] Security (Section Access wo nötig)
  • [ ] Documentation (Inline-Comments + Wiki)

Was sind die Performance-Benchmarks zum Vergleich in Qlik Sense?

SzenarioOhne QVDMit QVD (Non-Opt)Mit QVD (Optimized)Verbesserung
1M Zeilen, täglicher Load8 Min3 Min15 Sek32x
10M Zeilen, simpler Load45 Min15 Min2 Min22x
50M Zeilen, gefiltert4 Std1 Std10 Min24x
100M Zeilen, partitioniertN/A3 Std15 Min12x

Typische Performance-Faktoren:

  • Database → QVD (Non-Opt): 3-5x schneller
  • Non-Optimized → Optimized: 10-20x schneller
  • Kombination: 10-100x Gesamtverbesserung

Wie kann ich die QVD-Optimierung für 100x schnellere Loads in Qlik Sense nutzen?

ProblemUrsacheLösung
Load nicht optimizedBerechnungen im LOADZwei-Stufen-Load (Optimized + Resident)
Out of MemoryAlle Tabellen gleichzeitig im RAMDROP TABLE nach Verarbeitung
QVD korruptUnterbrochener STOREAtomisches Update (TEMP → FINAL)
Performance sinktQVD-Anhäufung über ZeitAutomatischer Cleanup-Job
Inkonsistente DatenFehlerhafte Incremental LogicWeekly Full Reload + Sanity Checks
Cloud 6GB-LimitQVD zu großWeitere Partitionierung (Monat → Tag)

Was sind die nächsten Schritte für meine Implementation in Qlik Sense?

Phase 1: Fundament (Woche 1-2)

  1. Erste Single-QVDs für Ihre größten Tabellen erstellen
  2. Optimierung im Script-Log verifizieren
  3. Performance messen (vorher/nachher)
  4. Team-Training: QVD-Grundlagen

Phase 2: Architektur (Woche 3-4)

  1. Drei-Schichten-Architektur designen
  2. Extract-Layer für Quellsysteme aufbauen
  3. Transform-Layer mit Business-Logik implementieren
  4. Naming Conventions und Folder-Struktur etablieren

Phase 3: Skalierung (Woche 5-8)

  1. Incremental Loading für große Tabellen
  2. Partitionierung wo sinnvoll (> 50M Zeilen)
  3. Performance-Monitoring automatisieren
  4. Error Handling und Alerting

Phase 4: Governance (laufend)

  1. Metadata-Management
  2. Data Lineage Tracking
  3. Automatisierte Quality Checks
  4. Documentation und Knowledge Transfer

Welche weiterführenden Ressourcen gibt es zur QVD-Optimierung in Qlik Sense?

Wie kann ich QVD-Optimierung für 100x schnellere Loads in Qlik Sense erreichen?

Was sind Blogs und Tutorials zur QVD-Optimierung in Qlik Sense?

Welche Tools und Utilities helfen bei der QVD-Optimierung in Qlik Sense?


Was ist das Schlusswort zur QVD-Optimierung in Qlik Sense?

QVD-Optimierung ist keine «Nice-to-Have»-Technik – es ist der Schlüssel zu erfolgreicher Qlik-Implementation auf Enterprise-Level. Die Investition in eine solide QVD-Architektur zahlt sich täglich aus durch:

  • Dramatisch schnellere Reloads (10-100x)
  • Reduzierte Kosten (DB-Lizenz-Einsparungen, Cloud Storage)
  • Bessere User Experience (kürzere Wartezeiten)
  • Skalierbarkeit (Milliarden von Zeilen handhabbar)
  • Governance (Datenqualität, Nachvollziehbarkeit)

Beginnen Sie klein – erstellen Sie Ihre erste optimierte QVD heute, messen Sie die Performance-Verbesserung, und bauen Sie schrittweise aus. Ihre zukünftigen Entwickler (und Ihr zukünftiges Ich) werden es Ihnen danken!

Viel Erfolg bei Ihrer QVD-Implementation! 🚀


📚 Qlik Sense Kurs – Artikel 8 von 28

← Vorheriger Artikel: Incremental Loading – Nur Changes laden
→ Nächster Artikel: Synthetic Keys & Circular References auflösen

Verwandte Artikel: