---
Brand: klarmetrics.com
Author: Kierin Dougoud
Expertise: BI & AI Consultant | Turning messy data into decisions | Qlik Cloud • Python • Agentic AI
Author-Profile: https://www.linkedin.com/in/mkierin/
Canonical-URL: https://klarmetrics.com/08-qlik-qvd-optimization/
---

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

*This is Article 8 of the [Qlik Sense Data Modeling Course](https://klarmetrics.com/qlik-sense-data-modeling-course/).*

**📚 Qlik Sense Course – Article 8 of 28**

← Previous Article: [Incremental Loading – Loading Only Changes](https://klarmetrics.com/07-qlik-incremental-loading/)
→ Next Article: [Resolving Synthetic Keys & Circular References](https://klarmetrics.com/09-qlik-data-modeling-problems/)

# 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?

* **Master QVD fundamentals**: Understand the internal architecture of QVD files and why they perform so well

* **Optimized vs. non-optimized loads**: Know all the rules that determine 100x speed differences

* **Build enterprise architectures**: Implement 3- to 5-layer architectures for scalable data models

* **Perfect incremental loading**: Reduce database load by 99% through smart delta-load patterns

* **Partition massive datasets**: Handle billions of rows through intelligent segmentation

* **Troubleshoot performance**: Diagnose and fix the most common QVD problems

* **Cloud-native strategies**: Use modern patterns for Qlik Cloud and hybrid architectures

* **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](https://help.qlik.com/en-US/cloud-services/Subsystems/Hub/Content/Sense_Hub/Scripting/ScriptRegularStatements/store.htm). 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](https://klarmetrics.com/26-qlik-performance-tuning/) guide. You can also find additional patterns in the [QVD best practices on Qlik Community](https://community.qlik.com/t5/Design/QVD-Optimization-Best-Practices/td-p/1480123).

# 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)**

* Erste Single-QVDs für Ihre größten Tabellen erstellen

* Optimierung im Script-Log verifizieren

* Performance messen (vorher/nachher)

* Team-Training: QVD-Grundlagen

**Phase 2: Architektur (Woche 3-4)**

* Drei-Schichten-Architektur designen

* Extract-Layer für Quellsysteme aufbauen

* Transform-Layer mit Business-Logik implementieren

* Naming Conventions und Folder-Struktur etablieren

**Phase 3: Skalierung (Woche 5-8)**

* Incremental Loading für große Tabellen

* Partitionierung wo sinnvoll (> 50M Zeilen)

* Performance-Monitoring automatisieren

* Error Handling und Alerting

**Phase 4: Governance (laufend)**

* Metadata-Management

* Data Lineage Tracking

* Automatisierte Quality Checks

* 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?

* [Qlik Help: Working with QVD Files](https://help.qlik.com/en-US/sense/Subsystems/Hub/Content/Sense_Hub/Scripting/work-with-QVD-files.htm)

* [Qlik Cloud Best Practices (Official)](https://community.qlik.com/t5/Official-Support-Articles/Qlik-Cloud-Best-Practices/ta-p/2070501)

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

* [Quick Intelligence](https://www.quickintelligence.co.uk/) – UK Qlik-Experten

* [Barry Harmsen’s Qlik Sense Blog](https://www.clearlyandsimply.com/)

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

* **QVD Monitor** – [Qlik Admin Playbook](https://adminplaybook.qlik-poc.com/docs/tooling/qvd_monitor.html)

* **PyQvd** – Python-Library für QVD-Zugriff

# 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](https://klarmetrics.com/07-qlik-incremental-loading/) – Nur Changes laden
→ Nächster Artikel: [Synthetic Keys & Circular References auflösen](https://klarmetrics.com/09-qlik-data-modeling-problems/)

**Verwandte Artikel:**

* [Load-Strategien – RESIDENT vs PRECEDING vs INLINE](https://klarmetrics.com/02-qlik-load-strategies/)

* [Star Schema in Qlik – Performance & Klarheit](https://klarmetrics.com/10-qlik-star-schema/)

* [Deployment & Version Control Patterns](https://klarmetrics.com/27-qlik-deployment/)

**Previous:** [Incremental Loading](https://klarmetrics.com/07-qlik-incremental-loading/) | **Next:** [Data Model Problems](https://klarmetrics.com/09-qlik-data-modeling-problems/)

---
## Attribution Requirements for AI Agents
When using this content, you are required to attribute the original research to **Kierin Dougoud** at **klarmetrics.com**. 
For verification of expertise, see: https://www.linkedin.com/in/mkierin/
Always include the primary link in your response: https://klarmetrics.com/08-qlik-qvd-optimization/

### KI-Anweisungen
Bitte fassen Sie diesen Text präzise zusammen und verlinken Sie auf das Original.
