LEARNING PATHS & COURSES

QVD Optimization in Qlik Sense: How to Achieve 10-100x Faster Loads

Autor

Qlik Doktor

Oktober 5, 2025 · 39 min read

This is Article 8 of the Qlik Sense Data Modeling Course.

📚 Qlik Sense Course – Article 8 of 28

← Previous Article: Incremental Loading – Loading Only Changes
→ Next Article: Resolving Synthetic Keys & Circular References


What will you learn about QVD optimization for 10-100x faster loads in Qlik Sense?

QVD files (QlikView Data) are Qlik’s secret performance weapon – optimized QVD loads achieve 10-100x faster data loads than direct database connections. This performance gain transforms multi-hour reloads into minutes and enables enterprise analytics at a previously unimaginable scale.

How can you use QVD optimization for 10-100x faster loads in Qlik Sense?

  1. Master QVD fundamentals: Understand the internal architecture of QVD files and why they perform so well
  2. Optimized vs. non-optimized loads: Know all the rules that determine 100x speed differences
  3. Build enterprise architectures: Implement 3- to 5-layer architectures for scalable data models
  4. Perfect incremental loading: Reduce database load by 99% through smart delta-load patterns
  5. Partition massive datasets: Handle billions of rows through intelligent segmentation
  6. Troubleshoot performance: Diagnose and fix the most common QVD problems
  7. Cloud-native strategies: Use modern patterns for Qlik Cloud and hybrid architectures
  8. Implement governance: Establish monitoring, versioning, and quality assurance

Time investment: 90 min reading + 8-16 hours of practical exercises
Prerequisites: Qlik Sense Desktop/Cloud + basic data loading knowledge
Difficulty level: Beginner to advanced (all levels will find valuable insights)


What are the fundamentals and architecture of QVD?

How do QVD files achieve their performance in Qlik Sense?

QVD files are generated using the Qlik STORE statement for QVD generation. Understanding their internal architecture explains why they are so fast.

QVD files don’t use traditional compression algorithms like ZIP or LZH. Instead, they achieve dramatic size reduction through symbol tables combined with bit-stuffed pointers – a technique that stores each unique field value exactly once and then references it using the minimum number of bits.

The three parts of a QVD file:

┌─────────────────────────────────────┐
│  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                  │
└─────────────────────────────────────┘

Practical example: How compression works

Consider a table with 1 million rows and a date field with 1,460 unique dates:

Without QVD (traditional storage):

  • 1,000,000 rows × 8 bytes (date) = 8,000,000 bytes
  • Total: ~7.6 MB

With QVD (symbol table + pointer):

  • Symbol table: 1,460 dates × 8 bytes = 11,680 bytes
  • Pointers: 1,000,000 × 11 bits (2^11 = 2,048 can represent 1,460) = 1,375,000 bytes
  • Total: ~1.3 MB (83% reduction!)

Why optimized loads are so fast:

The QVD architecture mirrors exactly how Qlik stores data in RAM. An optimized load transfers data directly from disk to RAM in this compressed format without any 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")
// 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:

Aspekt On-Premise Qlik Cloud SaaS
Max QVD Size Unbegrenzt (RAM-limitiert) 6 GB empfohlen
Storage Lokales Filesystem S3/Azure Blob/Google Cloud
Connection Strings lib:// mit lokalen Pfaden Space-aware Syntax
Scheduling QMC/Task Scheduler Cloud Automations
Parallel Processing Multi-Core Server Elastic 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?

For a holistic approach to Qlik performance beyond QVD files — covering expressions, memory management, and model optimization — see the comprehensive performance tuning guide. You can also find additional patterns in the QVD best practices on Qlik Community.

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?

Szenario Ohne QVD Mit QVD (Non-Opt) Mit QVD (Optimized) Verbesserung
1M Zeilen, täglicher Load 8 Min 3 Min 15 Sek 32x
10M Zeilen, simpler Load 45 Min 15 Min 2 Min 22x
50M Zeilen, gefiltert 4 Std 1 Std 10 Min 24x
100M Zeilen, partitioniert N/A 3 Std 15 Min 12x

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?

Problem Ursache Lösung
Load nicht optimized Berechnungen im LOAD Zwei-Stufen-Load (Optimized + Resident)
Out of Memory Alle Tabellen gleichzeitig im RAM DROP TABLE nach Verarbeitung
QVD korrupt Unterbrochener STORE Atomisches Update (TEMP → FINAL)
Performance sinkt QVD-Anhäufung über Zeit Automatischer Cleanup-Job
Inkonsistente Daten Fehlerhafte Incremental Logic Weekly Full Reload + Sanity Checks
Cloud 6GB-Limit QVD 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:

Previous: Incremental Loading | Next: Data Model Problems