This is Article 8 of the Qlik Sense Data Modeling Course.
📚 Qlik Sense Course – Article 8 of 28
← Previous Article: Incremental Loading – Loading Only Changes
→ Next Article: Resolving Synthetic Keys & Circular References
What will you learn about QVD optimization for 10-100x faster loads in Qlik Sense?
QVD files (QlikView Data) are Qlik’s secret performance weapon – optimized QVD loads achieve 10-100x faster data loads than direct database connections. This performance gain transforms multi-hour reloads into minutes and enables enterprise analytics at a previously unimaginable scale.
How can you use QVD optimization for 10-100x faster loads in Qlik Sense?
- 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. 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.qvdExtract_SQL_Orders_CURRENT.qvdExtract_Oracle_Customers_2024-01-15.qvd
Wie transformiere ich Layer 2 zu Business-Ready QVDs?
Ziel: Alle Geschäftslogik, Berechnungen und Datenqualität zentral verwalten!
// ═══════════════════════════════════════════════════════════
// TRANSFORM LAYER - SCHRITT 2: BUSINESS LOGIC
// ═══════════════════════════════════════════════════════════
// App: 02_Transform_Orders.qvf
// Schedule: Täglich 02:10 Uhr (nach Extract!)
// Processing Time: 5-15 Minuten
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// MAPPING-TABELLEN VORBEREITEN
// ═══════════════════════════════════════════════════════════
// Währungs-Mapping
CurrencyMapping:
MAPPING LOAD
CurrencyCode,
ExchangeRate
FROM [lib://QVDs/01_Extract/ExchangeRates_CURRENT.qvd] (qvd);
// Status-Mapping (Friendly Names)
StatusMapping:
MAPPING LOAD * INLINE [
Status_Code, Status_Text
10, Neu
20, In Bearbeitung
30, Versandt
40, Geliefert
50, Storniert
];
// ═══════════════════════════════════════════════════════════
// HAUPTDATEN TRANSFORMIEREN
// ═══════════════════════════════════════════════════════════
TempOrders:
LOAD
OrderID,
CustomerID,
ProductID,
OrderDate,
ShipDate,
Amount,
Currency,
Status,
CreatedDate,
ModifiedDate
FROM [lib://QVDs/01_Extract/Orders_CURRENT.qvd] (qvd);
// ✓ Optimized Load!
// Jetzt alle Transformationen:
Transform_Orders:
LOAD
// ═══════════ IDs ═══════════
OrderID,
CustomerID,
ProductID,
// ═══════════ DATUMS-DIMENSIONEN ═══════════
Date(Floor(OrderDate), 'DD.MM.YYYY') as OrderDate,
Year(OrderDate) as OrderYear,
Month(OrderDate) as OrderMonth,
Quarter(OrderDate) as OrderQuarter,
Week(OrderDate) as OrderWeek,
Weekday(OrderDate) as OrderWeekday,
Day(OrderDate) as OrderDay,
// Geschäftsjahr (beginnt in Q2)
If(Month(OrderDate) >= 4,
Year(OrderDate),
Year(OrderDate) - 1) as FiscalYear,
// ═══════════ BETRÄGE & WÄHRUNG ═══════════
Amount as Amount_Original,
Currency,
// Umrechnung in EUR
Amount * ApplyMap('CurrencyMapping', Currency, 1) as Amount_EUR,
// Kategorisierung
If(Amount > 10000, 'Großauftrag',
If(Amount > 1000, 'Mittel',
'Klein')) as OrderSizeCategory,
// ═══════════ STATUS ═══════════
Status as StatusCode,
ApplyMap('StatusMapping', Text(Status), 'Unbekannt') as StatusText,
// Business-Flags
If(Status >= 40, 1, 0) as IsCompleted,
If(Status = 50, 1, 0) as IsCancelled,
If(ShipDate - OrderDate > 7, 1, 0) as IsDelayed,
// ═══════════ AUDIT-FELDER ═══════════
CreatedDate,
ModifiedDate,
// Data Quality Flags
If(IsNull(CustomerID), 1, 0) as DQ_MissingCustomer,
If(Amount <= 0, 1, 0) as DQ_InvalidAmount,
If(ShipDate < OrderDate, 1, 0) as DQ_InvalidDates
RESIDENT TempOrders;
DROP TABLE TempOrders;
// ═══════════════════════════════════════════════════════════
// DATA QUALITY CHECKS
// ═══════════════════════════════════════════════════════════
QualityCheck:
LOAD
Sum(DQ_MissingCustomer) as Missing_Customers,
Sum(DQ_InvalidAmount) as Invalid_Amounts,
Sum(DQ_InvalidDates) as Invalid_Dates,
Count(*) as Total_Records
RESIDENT Transform_Orders;
LET vMissingCustomers = Peek('Missing_Customers', 0, 'QualityCheck');
IF $(vMissingCustomers) > 0 THEN
TRACE WARNUNG: $(vMissingCustomers) Bestellungen ohne CustomerID!;
END IF
DROP TABLE QualityCheck;
// ═══════════════════════════════════════════════════════════
// SPEICHERN
// ═══════════════════════════════════════════════════════════
STORE Transform_Orders INTO
[lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd);
DROP TABLE Transform_Orders;
TRACE Transform Layer abgeschlossen: Fact_Orders.qvd erstellt;
Naming Convention für Transform Layer:
- Format:
<Type>_<EntityName>.qvd - Typen:
Fact_*für Faktentabellen (Orders, Sales, Transactions)Dim_*für Dimensionstabellen (Customer, Product, Date)Bridge_*für Brückentabellen (Many-to-Many)Master_*für Referenzdaten (Calendar, ExchangeRates)
Wie optimiert man QVD für 100x schnellere Loads in Qlik Sense?
Ziel: Nur laden, was diese App braucht. Optimiert und gefiltert!
// ═══════════════════════════════════════════════════════════
// PRESENTATION LAYER - APP: Sales Dashboard
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// CALENDAR FILTER VORBEREITEN
// ═══════════════════════════════════════════════════════════
// Diese App zeigt nur letzten 24 Monate
TempCalendar:
LOAD DISTINCT
OrderDate
RESIDENT
(SELECT OrderDate FROM [lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd))
WHERE OrderDate >= AddMonths(Today(), -24);
// ═══════════════════════════════════════════════════════════
// OPTIMIZED LOAD MIT WHERE EXISTS
// ═══════════════════════════════════════════════════════════
Orders:
LOAD
OrderID,
CustomerID,
ProductID,
OrderDate,
OrderYear,
OrderMonth,
OrderQuarter,
Amount_EUR,
OrderSizeCategory,
StatusText,
IsCompleted,
IsCancelled
FROM [lib://QVDs/02_Transform/Fact_Orders.qvd] (qvd)
WHERE EXISTS(OrderDate) // ✓ Optimized!
AND IsCompleted = 1; // Nur abgeschlossene Aufträge
DROP TABLE TempCalendar;
// ═══════════════════════════════════════════════════════════
// DIMENSIONEN LADEN
// ═══════════════════════════════════════════════════════════
Customers:
LOAD
CustomerID,
CustomerName,
Country,
Region,
Segment
FROM [lib://QVDs/02_Transform/Dim_Customer.qvd] (qvd)
WHERE EXISTS(CustomerID); // ✓ Nur verwendete Kunden!
Products:
LOAD
ProductID,
ProductName,
Category,
SubCategory
FROM [lib://QVDs/02_Transform/Dim_Product.qvd] (qvd)
WHERE EXISTS(ProductID); // ✓ Nur verwendete Produkte!
// ═══════════════════════════════════════════════════════════
// APP-SPEZIFISCHE BERECHNUNGEN (wenn nötig)
// ═══════════════════════════════════════════════════════════
// Hier können Sie app-spezifische Aggregationen machen
// ohne die Transform-Layer-QVDs zu ändern
Was sind Vier- und Fünf-Schichten-Architekturen für große Enterprises?
Vier-Schichten mit Data Model Layer:
Layer 1: Extract (Raw QVDs)
Layer 2: Transform (Business QVDs)
Layer 3: Model (Wiederverwendbare QVF mit Datenmodell, ohne UI)
Layer 4: Presentation (Apps mit Binary Load + UI)
Vorteil: 10 Apps können ein Modell via Binary Load teilen!
// ═══════════════════════════════════════════════════════════
// LAYER 3: MODEL APP (z.B. "Sales_DataModel.qvf")
// ═══════════════════════════════════════════════════════════
// Dieses QVF wird NIE von Endbenutzern geöffnet!
// Es dient nur als Datenmodell-Quelle für andere Apps
// ... Hier laden Sie alle Transform-QVDs ...
// ... Hier erstellen Sie die Tabellenverknüpfungen ...
// ... KEINE Visualisierungen! ...
// Save und Schedule!
// ═══════════════════════════════════════════════════════════
// LAYER 4: PRESENTATION APP
// ═══════════════════════════════════════════════════════════
// Komplettes Datenmodell in 1 Sekunde laden!
Binary [lib://DataModels/Sales_DataModel.qvf];
// Jetzt nur noch UI-spezifische Dinge:
// - Section Access (User-basierte Filter)
// - App-spezifische Variablen
// - Visualisierungen erstellen
Fünf-Schichten (für Konzerne mit verteilten Quellen):
Layer 1: Extract Regional (Pro Region/System separate QVDs)
Layer 2: Extract Consolidated (Alle Regionen zusammengeführt)
Layer 3: Transform (Business Logic)
Layer 4: Model (Wiederverwendbare Modelle)
Layer 5: Presentation (Apps)
Wann nutzen?
- Globale Konzerne mit regionalen Datensilos
- Unterschiedliche Refresh-Frequenzen pro Region
- Compliance-Anforderungen (Daten dürfen Region nicht verlassen)
Wie funktioniert Incremental Loading mit QVDs in Qlik Sense?
Warum ist Incremental Loading kritisch für QVD-Optimierung in Qlik Sense?
Szenario ohne Incremental Loading:
- Tabelle: 100 Millionen Zeilen
- Tägliches Wachstum: 1 Million neue Zeilen (1%)
- Full Reload jeden Tag: Lädt alle 100M Zeilen neu!
- Dauer: 2 Stunden
- DB-Last: Extrem hoch
- Netzwerk: Gigabytes übertragen
Mit Incremental Loading:
- Lädt nur die 1M neuen Zeilen!
- Dauer: 5 Minuten (24x schneller!)
- DB-Last: 99% reduziert
- Netzwerk: Minimal
Was ist Pattern 1: Insert-Only (Append-Only-Tabellen) in Qlik Sense?
Für Tabellen, wo Daten nur hinzugefügt werden (nie Updates/Deletes):
// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT-ONLY PATTERN
// ═══════════════════════════════════════════════════════════
// Beispiel: Log-Tabellen, Transaktions-Tabellen
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// SCHRITT 1: Maximum ID/Timestamp aus letztem Load ermitteln
// ═══════════════════════════════════════════════════════════
// Prüfen ob QVD existiert
IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
// QVD existiert: Lade MaxID
MaxID:
LOAD
Max(OrderID) as MaxOrderID
FROM [lib://QVDs/Orders.qvd] (qvd);
LET vMaxOrderID = Peek('MaxOrderID', 0, 'MaxID');
DROP TABLE MaxID;
TRACE Letzter geladener OrderID: $(vMaxOrderID);
ELSE
// QVD existiert nicht: Full Load
LET vMaxOrderID = 0;
TRACE QVD nicht gefunden - führe Full Load durch;
END IF
// ═══════════════════════════════════════════════════════════
// SCHRITT 2: Neue Daten aus Source laden
// ═══════════════════════════════════════════════════════════
NewOrders:
LOAD
OrderID,
CustomerID,
OrderDate,
Amount,
Status
FROM [lib://DB_Connection]
(SQL SELECT * FROM Orders WHERE OrderID > $(vMaxOrderID));
LET vNewRecords = NoOfRows('NewOrders');
TRACE Neue Datensätze gefunden: $(vNewRecords);
// ═══════════════════════════════════════════════════════════
// SCHRITT 3: Alte Daten aus QVD laden (wenn vorhanden)
// ═══════════════════════════════════════════════════════════
IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
CONCATENATE (NewOrders)
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);
// ✓ Optimized Load!
TRACE Alte Datensätze hinzugefügt;
END IF
// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Kombinierte Daten speichern
// ═══════════════════════════════════════════════════════════
// Erst in temp speichern (atomisches Update!)
STORE NewOrders INTO [lib://QVDs/Orders_temp.qvd] (qvd);
// Falls erfolgreich: temp → final umbenennen
IF ScriptErrorCount = 0 THEN
// Backup erstellen
IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
EXECUTE cmd /c copy "$(vQVDPath)Orders.qvd" "$(vQVDPath)Orders_backup.qvd";
END IF
// Temp → Final
EXECUTE cmd /c move /Y "$(vQVDPath)Orders_temp.qvd" "$(vQVDPath)Orders.qvd";
TRACE ═══════════════════════════════════════;
TRACE Incremental Load erfolgreich!;
TRACE Total Datensätze: $(NoOfRows('NewOrders'));
TRACE Neue Datensätze: $(vNewRecords);
TRACE ═══════════════════════════════════════;
END IF
DROP TABLE NewOrders;
Wie funktioniert Pattern 2: Insert + Update (Slowly Changing Dimensions)?
Für Tabellen mit Änderungen an bestehenden Datensätzen:
// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT + UPDATE PATTERN
// ═══════════════════════════════════════════════════════════
// Beispiel: Kunden-Stammdaten, Produkt-Katalog
// Voraussetzung: ModifiedDate-Feld in Source-Tabelle!
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// SCHRITT 1: Letzten Änderungszeitpunkt ermitteln
// ═══════════════════════════════════════════════════════════
IF FileSize('lib://QVDs/Customers.qvd') > 0 THEN
MaxModified:
LOAD
Max(ModifiedDate) as MaxModifiedDate
FROM [lib://QVDs/Customers.qvd] (qvd);
LET vMaxModified = Peek('MaxModifiedDate', 0, 'MaxModified');
DROP TABLE MaxModified;
// Sicherheitspuffer: 1 Tag zurück (falls Clock-Skew)
LET vMaxModified = Date(vMaxModified - 1, 'YYYY-MM-DD HH:mm:ss');
ELSE
// Full Load
LET vMaxModified = '1900-01-01 00:00:00';
END IF
TRACE Lade Änderungen seit: $(vMaxModified);
// ═══════════════════════════════════════════════════════════
// SCHRITT 2: Geänderte Datensätze laden
// ═══════════════════════════════════════════════════════════
ChangedCustomers:
LOAD
CustomerID,
CustomerName,
Country,
Region,
Email,
Status,
CreatedDate,
ModifiedDate
FROM [lib://DB_Connection]
(SQL SELECT * FROM Customers
WHERE ModifiedDate > '$(vMaxModified)');
LET vChangedRecords = NoOfRows('ChangedCustomers');
TRACE Geänderte Datensätze: $(vChangedRecords);
// ═══════════════════════════════════════════════════════════
// SCHRITT 3: Alte Daten laden (OHNE die geänderten!)
// ═══════════════════════════════════════════════════════════
IF FileSize('lib://QVDs/Customers.qvd') > 0 THEN
CONCATENATE (ChangedCustomers)
LOAD *
FROM [lib://QVDs/Customers.qvd] (qvd)
WHERE NOT EXISTS(CustomerID);
// ✓ Lädt nur Kunden, die NICHT in ChangedCustomers sind
// ⚠️ Achtung: NOT EXISTS bricht Optimierung!
// Aber: Wir laden nur aus QVD, nicht aus DB = immer noch schnell!
TRACE Alte Datensätze (ohne Updates) hinzugefügt;
END IF
// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Speichern (atomisch!)
// ═══════════════════════════════════════════════════════════
STORE ChangedCustomers INTO [lib://QVDs/Customers_temp.qvd] (qvd);
IF ScriptErrorCount = 0 THEN
// Backup + Replace (wie in Pattern 1)
EXECUTE cmd /c copy "$(vQVDPath)Customers.qvd" "$(vQVDPath)Customers_backup.qvd";
EXECUTE cmd /c move /Y "$(vQVDPath)Customers_temp.qvd" "$(vQVDPath)Customers.qvd";
LET vTotalRecords = NoOfRows('ChangedCustomers');
TRACE ═══════════════════════════════════════;
TRACE Incremental Load erfolgreich!;
TRACE Total Datensätze: $(vTotalRecords);
TRACE Geänderte Datensätze: $(vChangedRecords);
TRACE Update-Rate: $(Num(vChangedRecords/vTotalRecords, '#,##0.0%'));
TRACE ═══════════════════════════════════════;
END IF
DROP TABLE ChangedCustomers;
Wie funktioniert Pattern 3: Insert + Update + Delete (Full Synchronization)?
Für Tabellen, wo auch Deletes synchronisiert werden müssen:
// ═══════════════════════════════════════════════════════════
// INCREMENTAL LOAD: INSERT + UPDATE + DELETE PATTERN
// ═══════════════════════════════════════════════════════════
// Komplexestes Pattern - vollständige Synchronisation
// ═══════════════════════════════════════════════════════════
// SCHRITTE 1-3: Wie in Pattern 2
// ... (ChangedCustomers Tabelle wird erstellt) ...
// ═══════════════════════════════════════════════════════════
// SCHRITT 4: Alle aktuellen IDs aus Source laden
// ═══════════════════════════════════════════════════════════
CurrentCustomerIDs:
LOAD DISTINCT
CustomerID
FROM [lib://DB_Connection]
(SQL SELECT CustomerID FROM Customers);
TRACE Aktuelle Customer IDs in Source-System geladen;
// ═══════════════════════════════════════════════════════════
// SCHRITT 5: NUR Datensätze behalten, die in Source existieren
// ═══════════════════════════════════════════════════════════
INNER JOIN (ChangedCustomers)
LOAD *
RESIDENT CurrentCustomerIDs;
DROP TABLE CurrentCustomerIDs;
// Resultat: ChangedCustomers enthält nur Kunden, die:
// - In der Source-DB existieren (via INNER JOIN)
// - Entweder neu sind oder geändert wurden
// ═══════════════════════════════════════════════════════════
// SCHRITT 6: Speichern
// ═══════════════════════════════════════════════════════════
STORE ChangedCustomers INTO [lib://QVDs/Customers_temp.qvd] (qvd);
// ... Rest wie in Pattern 2 ...
TRACE Deletes wurden durch INNER JOIN entfernt;
Wie kann ich das MERGE Statement in Qlik Sense Modern verwenden?
Seit neueren Qlik-Versionen gibt es den MERGE-Befehl:
// ═══════════════════════════════════════════════════════════
// MODERN APPROACH: MERGE STATEMENT
// ═══════════════════════════════════════════════════════════
// Klarerer, wartbarerer Code als manuelle CONCATENATE-Logik
// ═══════════════════════════════════════════════════════════
// Geänderte Datensätze aus Source
ChangedOrders:
LOAD
'I' as Operation, // I=Insert, U=Update, D=Delete
SequenceNumber, // Für Reihenfolge bei mehrfachen Änderungen
OrderID,
CustomerID,
Amount,
Status,
ModifiedDate
FROM [lib://DB_Connection]
(SQL SELECT * FROM Orders WHERE ModifiedDate > '$(vLastSync)');
// Bestehende Daten
ExistingOrders:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);
// MERGE durchführen
MergedOrders:
MERGE (SequenceNumber) // Sortiere nach SequenceNumber
LOAD
Operation,
SequenceNumber,
OrderID,
CustomerID,
Amount,
Status,
ModifiedDate
RESIDENT ChangedOrders
ON OrderID; // Primary Key
CONCATENATE (MergedOrders)
LOAD *
RESIDENT ExistingOrders
WHERE NOT EXISTS(OrderID); // Nur alte Datensätze ohne Änderung
DROP TABLES ChangedOrders, ExistingOrders;
STORE MergedOrders INTO [lib://QVDs/Orders.qvd] (qvd);
Was ist der Buffer-Prefix und wie kann er in Qlik Sense genutzt werden?
Der einfachste Weg für Insert-Only-Tabellen:
// ═══════════════════════════════════════════════════════════
// BUFFER PREFIX: Automatisches Incremental Loading
// ═══════════════════════════════════════════════════════════
// Qlik trackt automatisch, wie viele Zeilen geladen wurden
// und lädt beim nächsten Mal nur neue Zeilen!
// ═══════════════════════════════════════════════════════════
Logs:
LOAD
LogID,
Timestamp,
Message,
Severity
FROM [lib://DB_Connection]
(SQL SELECT * FROM ApplicationLogs ORDER BY LogID);
PREFIX BUFFER (incremental);
// Das war's! Beim nächsten Reload lädt Qlik automatisch
// nur neue Zeilen (basierend auf Zeilenanzahl).
//
// ⚠️ WICHTIG: Funktioniert nur wenn:
// 1. Tabelle ist append-only (keine Updates/Deletes)
// 2. Keine WHERE-Klausel im SQL (außer Datum-Filter)
// 3. ORDER BY ist essentiell (meist Primary Key)
Wie erfolgt die Partitionierung für massive Datenmengen in Qlik Sense?
5.1 Wann partitionieren?
Partitionierung wird relevant ab:
- 50+ Millionen Zeilen in einer Tabelle
- QVD-Dateien > 5 GB (Cloud: max 6 GB!)
- Load-Zeiten > 10 Minuten trotz Optimierung
- Klare Filter-Patterns (Zeit, Region, etc.)
Vorteile:
- 🚀 80% weniger Daten laden (nur relevante Partitionen)
- 💾 Dramatisch weniger RAM-Verbrauch
- ⚡ Parallele Verarbeitung möglich
- 🔧 Einfachere Updates (nur aktuelle Partition ändern)
- 📦 Überschaubare Dateigrößen
Was ist Pattern 1: Zeitbasierte Partitionierung in Qlik Sense?
Monatliche Partitionierung
// ═══════════════════════════════════════════════════════════
// PARTITIONIERUNG ERSTELLEN: MONATLICH
// ═══════════════════════════════════════════════════════════
// App: Partition_Generator.qvf
// Schedule: Täglich (updated nur aktuelle Partition!)
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Orders';
// ═══════════════════════════════════════════════════════════
// Schleife durch letzten 24 Monate
// ═══════════════════════════════════════════════════════════
FOR i = 0 to 23
// Monatsdatum berechnen
LET vPartitionDate = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
LET vMonthStart = Date(MonthStart(Today(), -$(i)), 'YYYY-MM-DD');
LET vMonthEnd = Date(MonthEnd(Today(), -$(i)), 'YYYY-MM-DD');
TRACE ═══════════════════════════════════════;
TRACE Verarbeite Monat: $(vPartitionDate);
TRACE Von: $(vMonthStart) Bis: $(vMonthEnd);
TRACE ═══════════════════════════════════════;
// ═══════════════════════════════════════════════════════════
// Daten für diesen Monat laden
// ═══════════════════════════════════════════════════════════
Orders_Partition:
LOAD
OrderID,
CustomerID,
ProductID,
OrderDate,
Amount,
Status
FROM [lib://DB_Connection]
(SQL SELECT * FROM Orders
WHERE OrderDate >= '$(vMonthStart)'
AND OrderDate <= '$(vMonthEnd)');
LET vRecordCount = NoOfRows('Orders_Partition');
// ═══════════════════════════════════════════════════════════
// Nur speichern wenn Daten vorhanden
// ═══════════════════════════════════════════════════════════
IF vRecordCount > 0 THEN
STORE Orders_Partition INTO
[$(vPartitionPath)/Orders_$(vPartitionDate).qvd] (qvd);
TRACE ✓ Partition gespeichert: $(vRecordCount) Zeilen;
ELSE
TRACE ⚠ Keine Daten für Monat $(vPartitionDate);
END IF
DROP TABLE Orders_Partition;
NEXT i
TRACE ═══════════════════════════════════════;
TRACE Partitionierung abgeschlossen!;
TRACE ═══════════════════════════════════════;
// ═══════════════════════════════════════════════════════════
// ERGEBNIS: 24 separate QVD-Dateien
// ═══════════════════════════════════════════════════════════
// Orders_2023-01.qvd (450 MB)
// Orders_2023-02.qvd (520 MB)
// Orders_2023-03.qvd (610 MB)
// ...
// Orders_2024-12.qvd (890 MB)
Partitionen laden (Consumer-App)
// ═══════════════════════════════════════════════════════════
// PARTITIONEN LADEN: Nur die benötigten Monate!
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Orders';
// ═══════════════════════════════════════════════════════════
// VARIANTE 1: Letzten 12 Monate laden
// ═══════════════════════════════════════════════════════════
FOR i = 0 to 11
LET vPartitionDate = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
LET vPartitionFile = '$(vPartitionPath)/Orders_$(vPartitionDate).qvd';
// Prüfen ob Datei existiert
IF FileSize('$(vPartitionFile)') > 0 THEN
CONCATENATE (Orders)
LOAD *
FROM [$(vPartitionFile)] (qvd);
// ✓ Optimized Load!
TRACE Partition geladen: $(vPartitionDate);
ELSE
TRACE ⚠ Partition nicht gefunden: $(vPartitionDate);
END IF
NEXT i
// ═══════════════════════════════════════════════════════════
// VARIANTE 2: Wildcard-Load (alle Partitionen eines Jahres)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_2024-*.qvd] (qvd);
// Lädt alle Monate von 2024
// ═══════════════════════════════════════════════════════════
// VARIANTE 3: Spezifischer Zeitraum (z.B. Q1 2024)
// ═══════════════════════════════════════════════════════════
FOR EACH vMonth in '2024-01', '2024-02', '2024-03'
CONCATENATE (Orders)
LOAD *
FROM [$(vPartitionPath)/Orders_$(vMonth).qvd] (qvd);
NEXT vMonth
// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH
// ═══════════════════════════════════════════════════════════
//
// Szenario: 5 Jahre Daten, nur letzten 12 Monate brauchen
//
// Ohne Partitionierung:
// - 60 Monate à 500 MB = 30 GB laden
// - Load-Zeit: 45 Minuten
// - RAM benötigt: 30 GB
//
// Mit Partitionierung:
// - 12 Monate à 500 MB = 6 GB laden
// - Load-Zeit: 5 Minuten (9x schneller!)
// - RAM benötigt: 6 GB (80% weniger!)
Wie funktioniert die Intraday-Partitionierung (für High-Frequency Data)?
Für Szenarien mit Millionen Transaktionen pro Tag:
// ═══════════════════════════════════════════════════════════
// INTRADAY PARTITIONIERUNG: Stündlich
// ═══════════════════════════════════════════════════════════
// Tagsüber: Stündliche QVDs
// Nachts: Konsolidierung zu Tages-QVDs
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Transactions';
// ═══════════════════════════════════════════════════════════
// TAGSÜBER (Stündlich, 06:00 - 22:00): Hourly QVDs
// ═══════════════════════════════════════════════════════════
// Schedule: Jede Stunde
LET vCurrentDate = Date(Today(), 'YYYY-MM-DD');
LET vCurrentHour = Text(Hour(Now()), '00');
LET vLastHour = Text(Hour(Now()) - 1, '00');
// Letzte Stunde laden
HourlyTransactions:
LOAD *
FROM [lib://DB_Connection]
(SQL SELECT * FROM Transactions
WHERE TransactionTime >= '$(vCurrentDate) $(vLastHour):00:00'
AND TransactionTime < '$(vCurrentDate) $(vCurrentHour):00:00');
// Als stündliche Partition speichern
STORE HourlyTransactions INTO
[$(vPartitionPath)/Trans_$(vCurrentDate)_H$(vLastHour).qvd] (qvd);
DROP TABLE HourlyTransactions;
// ═══════════════════════════════════════════════════════════
// NACHTS (01:00 Uhr): Konsolidierung zu Daily QVDs
// ═══════════════════════════════════════════════════════════
// Schedule: Täglich 01:00
LET vYesterday = Date(Today() - 1, 'YYYY-MM-DD');
// Alle stündlichen Partitionen des Vortags laden
DailyTransactions:
LOAD *
FROM [$(vPartitionPath)/Trans_$(vYesterday)_H*.qvd] (qvd);
// Wildcard lädt alle Stunden: H00, H01, H02, ... H23
// Als Tages-QVD speichern
STORE DailyTransactions INTO
[$(vPartitionPath)/Trans_$(vYesterday).qvd] (qvd);
DROP TABLE DailyTransactions;
// Stündliche QVDs löschen (Cleanup)
FOR vHour = 0 to 23
LET vHourFormatted = Text(vHour, '00');
LET vFileToDelete = '$(vPartitionPath)/Trans_$(vYesterday)_H$(vHourFormatted).qvd';
IF FileSize('$(vFileToDelete)') > 0 THEN
EXECUTE cmd /c del "$(vFileToDelete)";
TRACE Deleted: Trans_$(vYesterday)_H$(vHourFormatted).qvd;
END IF
NEXT vHour
// ═══════════════════════════════════════════════════════════
// CONSUMER-APP: Intelligentes Laden
// ═══════════════════════════════════════════════════════════
// Historische Daten (Daily QVDs)
Transactions:
LOAD *
FROM [$(vPartitionPath)/Trans_2024-*.qvd] (qvd)
WHERE Date# <> Today(); // Nicht der heutige Tag!
// Heutiger Tag (Hourly QVDs für near-real-time)
LET vToday = Date(Today(), 'YYYY-MM-DD');
CONCATENATE (Transactions)
LOAD *
FROM [$(vPartitionPath)/Trans_$(vToday)_H*.qvd] (qvd);
TRACE Near-Real-Time: Daten bis $(Hour(Now())) Uhr geladen;
Was ist Pattern 3: Multi-Dimensionale Partitionierung in Qlik Sense?
Kombination mehrerer Dimensionen für extreme Skalierung:
// ═══════════════════════════════════════════════════════════
// MULTI-DIMENSIONAL: Jahr × Region
// ═══════════════════════════════════════════════════════════
// Struktur:
// Orders_2024_EMEA.qvd
// Orders_2024_AMER.qvd
// Orders_2024_APAC.qvd
// Orders_2023_EMEA.qvd
// ...
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Orders';
// Regionen definieren
RegionList:
LOAD * INLINE [
Region
EMEA
AMER
APAC
];
// Jahre definieren (letzten 3 Jahre)
FOR vYear = Year(Today()) to Year(Today()) - 2 STEP -1
// Durch Regionen iterieren
FOR i = 0 to NoOfRows('RegionList') - 1
LET vRegion = Peek('Region', i, 'RegionList');
TRACE Verarbeite: $(vYear) - $(vRegion);
// Daten für Jahr × Region laden
Orders_Partition:
LOAD *
FROM [lib://DB_Connection]
(SQL SELECT * FROM Orders
WHERE Year(OrderDate) = $(vYear)
AND Region = '$(vRegion)');
IF NoOfRows('Orders_Partition') > 0 THEN
STORE Orders_Partition INTO
[$(vPartitionPath)/Orders_$(vYear)_$(vRegion).qvd] (qvd);
TRACE ✓ Gespeichert: $(NoOfRows('Orders_Partition')) Zeilen;
END IF
DROP TABLE Orders_Partition;
NEXT i
NEXT vYear
DROP TABLE RegionList;
// ═══════════════════════════════════════════════════════════
// CONSUMER: Nur relevante Partitionen laden
// ═══════════════════════════════════════════════════════════
// Beispiel: EMEA Sales Manager braucht nur EMEA-Daten
Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_*_EMEA.qvd] (qvd);
// Wildcard: Alle Jahre, nur EMEA!
// Oder spezifisch:
Orders:
LOAD *
FROM [$(vPartitionPath)/Orders_2024_EMEA.qvd] (qvd);
CONCATENATE (Orders)
LOAD *
FROM [$(vPartitionPath)/Orders_2023_EMEA.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// PERFORMANCE-IMPACT
// ═══════════════════════════════════════════════════════════
//
// Global Dataset: 300M Zeilen über 3 Jahre, 3 Regionen
//
// Ohne Partitionierung:
// - Jeder Regional Manager lädt 300M Zeilen
// - 30 GB RAM, 45 Min Load-Zeit
//
// Mit Jahr × Region:
// - EMEA Manager lädt ~33M Zeilen (nur EMEA, letzten 2 Jahre)
// - 3 GB RAM, 5 Min Load-Zeit
// - 90% weniger Ressourcen!
Wie funktioniert die Hash-basierte Partitionierung für parallele Verarbeitung?
// ═══════════════════════════════════════════════════════════
// HASH-PARTITIONING: Gleichmäßige Verteilung
// ═══════════════════════════════════════════════════════════
// Nutzen Sie dies für parallele Verarbeitung auf Multi-Core
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Customers';
LET vNumPartitions = 10; // 10 Partitionen
FOR i = 0 to vNumPartitions - 1
Customer_Partition:
LOAD *
FROM [lib://DB_Connection]
(SQL SELECT * FROM Customers)
WHERE Mod(Hash128(CustomerID), $(vNumPartitions)) = $(i);
// Hash128 verteilt IDs gleichmäßig auf Partitionen
STORE Customer_Partition INTO
[$(vPartitionPath)/Customers_P$(i).qvd] (qvd);
DROP TABLE Customer_Partition;
NEXT i
// ═══════════════════════════════════════════════════════════
// PARALLELE VERARBEITUNG MIT SEPARATEN APPS
// ═══════════════════════════════════════════════════════════
// Erstellen Sie 10 separate Apps, jede lädt eine Partition
// Schedule sie gleichzeitig auf verschiedenen Nodes/Cores!
//
// App_Process_P0.qvf: Lädt Customers_P0.qvd
// App_Process_P1.qvf: Lädt Customers_P1.qvd
// ...
//
// Ergebnis: 10x schnellere Verarbeitung bei 10 Cores!
Wie funktioniert die Partition-Wartung und welche Best Practices gibt es?
// ═══════════════════════════════════════════════════════════
// PARTITION MAINTENANCE
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// 1. ALTE PARTITIONEN ARCHIVIEREN
// ═══════════════════════════════════════════════════════════
SET vPartitionPath = 'lib://QVDs/Partitions/Orders';
SET vArchivePath = 'lib://Archive/Orders';
LET vRetentionMonths = 24; // Behalte 24 Monate online
// Alle QVD-Dateien auflisten
FileList:
LOAD
@1 as FileName,
@2 as FileSize,
@3 as FileDate
FROM [$(vPartitionPath)/*.qvd]
(txt, delimiter is 't');
// Alte Dateien identifizieren
FOR i = 0 to NoOfRows('FileList') - 1
LET vFile = Peek('FileName', i, 'FileList');
LET vFileDate = Peek('FileDate', i, 'FileList');
IF MonthName(vFileDate) < MonthName(AddMonths(Today(), -vRetentionMonths)) THEN
// Zu Archive verschieben
EXECUTE cmd /c move "$(vPartitionPath)$(vFile)" "$(vArchivePath)$(vFile)";
TRACE Archived: $(vFile);
END IF
NEXT i
// ═══════════════════════════════════════════════════════════
// 2. KORRUPTE PARTITIONEN REPARIEREN
// ═══════════════════════════════════════════════════════════
FOR i = 0 to NoOfRows('FileList') - 1
LET vFile = Peek('FileName', i, 'FileList');
LET vFileSize = Peek('FileSize', i, 'FileList');
// Prüfen auf 0-Byte-Dateien (korrupt!)
IF vFileSize = 0 THEN
TRACE ⚠️ WARNUNG: Korrupte Partition gefunden: $(vFile);
// Aus Backup restaurieren
EXECUTE cmd /c copy "$(vArchivePath)$(vFile)" "$(vPartitionPath)$(vFile)";
// Oder neu generieren
// ... (Partition-Generator-Code hier) ...
END IF
NEXT i
// ═══════════════════════════════════════════════════════════
// 3. PARTITION-KONSOLIDIERUNG
// ═══════════════════════════════════════════════════════════
// Tägliche → Monatliche Partitionen konsolidieren
LET vLastMonth = Date(MonthStart(Today() - 1), 'YYYY-MM');
// Alle Tages-Partitionen des letzten Monats laden
MonthlyData:
LOAD *
FROM [$(vPartitionPath)/Orders_$(vLastMonth)-*.qvd] (qvd);
// Als Monats-Partition speichern
STORE MonthlyData INTO
[$(vPartitionPath)/Orders_$(vLastMonth).qvd] (qvd);
// Tages-Partitionen löschen
EXECUTE cmd /c del "$(vPartitionPath)Orders_$(vLastMonth)-*.qvd";
TRACE Konsolidiert: $(vLastMonth) (Tages → Monats-Partition);
Wie kann ich QVD-Optimierung für 100x schnellere Loads in Qlik Sense durchführen?
6.1 Diagnostizieren: Warum ist mein Load nicht optimiert?
// ═══════════════════════════════════════════════════════════
// DEBUGGING-SCRIPT: Optimization-Status prüfen
// ═══════════════════════════════════════════════════════════
SET Verbatim = 1; // Zeigt Qlik's Script-Interpretation
// Test 1: Komplett simpler Load
TestTable1:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);
// Im Log sollte stehen: "TestTable1 << Orders.qvd (qvd optimized)"
DROP TABLE TestTable1;
// Test 2: Mit Feld-Auswahl
TestTable2:
LOAD
OrderID,
CustomerID,
Amount
FROM [lib://QVDs/Orders.qvd] (qvd);
// Sollte auch optimized sein!
DROP TABLE TestTable2;
// Test 3: Mit Umbenennung
TestTable3:
LOAD
OrderID as Order_Number,
CustomerID
FROM [lib://QVDs/Orders.qvd] (qvd);
// Sollte auch optimized sein!
DROP TABLE TestTable3;
// Test 4: Mit Berechnung (❌ bricht Optimierung)
TestTable4:
LOAD
OrderID,
Year(OrderDate) as OrderYear // ❌ Berechnung!
FROM [lib://QVDs/Orders.qvd] (qvd);
// Im Log: KEIN "(qvd optimized)" - das ist das Problem!
DROP TABLE TestTable4;
Im Script-Log suchen nach:
✓ OPTIMIZED: Orders << Orders.qvd (qvd optimized)
❌ NOT OPTIMIZED: Orders << Orders.qvd
Was sind die häufigsten Performance-Probleme bei QVD-Optimierung in Qlik Sense?
Problem 1: Non-Optimized trotz einfachem Statement
// ═══════════════════════════════════════════════════════════
// PROBLEM
// ═══════════════════════════════════════════════════════════
Orders:
LOAD
OrderID,
CustomerID,
Date(OrderDate) as OrderDate // ❌ Funktion!
FROM [lib://QVDs/Orders.qvd] (qvd);
// Non-Optimized! Dauert 5 Minuten für 10M Zeilen
// ═══════════════════════════════════════════════════════════
// LÖSUNG: Zwei-Stufen-Load
// ═══════════════════════════════════════════════════════════
TempOrders:
LOAD
OrderID,
CustomerID,
OrderDate // Keine Transformation!
FROM [lib://QVDs/Orders.qvd] (qvd);
// ✓ Optimized! Dauert 30 Sekunden
Orders:
LOAD
OrderID,
CustomerID,
Date(OrderDate) as OrderDate // Jetzt transformieren
RESIDENT TempOrders;
DROP TABLE TempOrders;
// Total: 30s + 30s = 1 Minute (5x schneller!)
Problem 2: Memory Overflow
// ═══════════════════════════════════════════════════════════
// PROBLEM: Alle Tabellen gleichzeitig im RAM
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd); // 5 GB RAM
Customers:
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd); // +2 GB RAM
Products:
LOAD * FROM [lib://QVDs/Products.qvd] (qvd); // +1 GB RAM
// Peak RAM: 8 GB! System kann crashen
// ═══════════════════════════════════════════════════════════
// LÖSUNG: Sequentielles Laden mit DROP
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);
// Orders verarbeiten und als finale QVD speichern
STORE Orders INTO [lib://QVDs/Final/Orders.qvd] (qvd);
DROP TABLE Orders; // ✓ 5 GB RAM freigegeben!
Customers:
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd);
STORE Customers INTO [lib://QVDs/Final/Customers.qvd] (qvd);
DROP TABLE Customers; // ✓ Wieder RAM frei!
Products:
LOAD * FROM [lib://QVDs/Products.qvd] (qvd);
STORE Products INTO [lib://QVDs/Final/Products.qvd] (qvd);
DROP TABLE Products;
// Peak RAM: Nur 5 GB (maximal für größte Tabelle)
// ═══════════════════════════════════════════════════════════
// ZUSÄTZLICHE OPTIMIERUNG: High-Cardinality-Felder
// ═══════════════════════════════════════════════════════════
// IDs mit AutoNumber() optimieren
Orders:
LOAD
AutoNumber(OrderID) as OrderID, // Spart RAM!
AutoNumber(CustomerID) as CustomerID,
Amount
FROM [lib://QVDs/Orders.qvd] (qvd);
// Warum? AutoNumber() ersetzt lange String-IDs (z.B. GUIDs)
// mit kompakten Nummern → bis zu 70% RAM-Ersparnis!
Problem 3: QVD-Datei korrupt
// ═══════════════════════════════════════════════════════════
// DEFENSIVE QVD-ERSTELLUNG: Atomische Updates
// ═══════════════════════════════════════════════════════════
SET ErrorMode = 0; // Fehler abfangen
// ═══════════════════════════════════════════════════════════
// Schritt 1: Daten laden und verarbeiten
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://DB_Connection] (SQL SELECT * FROM Orders);
LET vRecordCount = NoOfRows('Orders');
LET vExpectedRecords = 1000000; // Erwartete Mindestanzahl
// ═══════════════════════════════════════════════════════════
// Schritt 2: Sanity Checks
// ═══════════════════════════════════════════════════════════
IF vRecordCount < vExpectedRecords * 0.9 THEN
TRACE ⚠️ FEHLER: Nur $(vRecordCount) Zeilen geladen!;
TRACE ⚠️ Erwartet: mindestens $(vExpectedRecords);
// Alert senden
EXECUTE cmd /c echo "QVD Generation failed: Low record count" | mail -s "ALERT" admin@company.com;
// Script abbrechen (ohne QVD zu überschreiben!)
EXIT SCRIPT;
END IF
// ═══════════════════════════════════════════════════════════
// Schritt 3: In TEMP-Datei speichern
// ═══════════════════════════════════════════════════════════
LET vTimestamp = Timestamp(Now(), 'YYYYMMDD_hhmmss');
STORE Orders INTO
[lib://QVDs/Orders_TEMP_$(vTimestamp).qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// Schritt 4: Prüfen ob STORE erfolgreich
// ═══════════════════════════════════════════════════════════
IF ScriptErrorCount = 0 AND
FileSize('lib://QVDs/Orders_TEMP_$(vTimestamp).qvd') > 0 THEN
// ═══════════════════════════════════════════════════════════
// Schritt 5: Backup der alten QVD erstellen
// ═══════════════════════════════════════════════════════════
IF FileSize('lib://QVDs/Orders.qvd') > 0 THEN
EXECUTE cmd /c copy "$(vQVDPath)Orders.qvd" "$(vQVDPath)Orders_BACKUP.qvd";
TRACE ✓ Backup erstellt;
END IF
// ═══════════════════════════════════════════════════════════
// Schritt 6: Atomisches Replace (TEMP → FINAL)
// ═══════════════════════════════════════════════════════════
EXECUTE cmd /c move /Y "$(vQVDPath)Orders_TEMP_$(vTimestamp).qvd" "$(vQVDPath)Orders.qvd";
TRACE ═══════════════════════════════════════;
TRACE ✓ QVD erfolgreich aktualisiert!;
TRACE Zeilen: $(vRecordCount);
TRACE Zeitstempel: $(vTimestamp);
TRACE ═══════════════════════════════════════;
ELSE
TRACE ⚠️ FEHLER beim STORE - QVD NICHT überschrieben!;
TRACE ⚠️ Temp-Datei verbleibt zur Analyse;
END IF
DROP TABLE Orders;
Problem 4: Performance degradiert mit der Zeit
// ═══════════════════════════════════════════════════════════
// CLEANUP-SCRIPT: Alte/Temporäre QVDs aufräumen
// ═══════════════════════════════════════════════════════════
// Schedule: Wöchentlich
SET vQVDPath = 'lib://QVDs';
LET vRetentionDays = 30;
LET vCutoffDate = Today() - vRetentionDays;
// Liste aller QVD-Dateien
FileList:
LOAD
FileBaseName() as FileName,
FileTime() as FileDate,
FileSize() as FileSizeMB
FROM [$(vQVDPath)/*.qvd]
(fix, codepage is 1252);
// Alte Dateien identifizieren
FOR i = 0 to NoOfRows('FileList') - 1
LET vFile = Peek('FileName', i, 'FileList');
LET vFileDate = Peek('FileDate', i, 'FileList');
LET vFileSize = Peek('FileSizeMB', i, 'FileList');
// Löschen wenn:
// - Älter als Retention
// - ODER Name enthält "TEMP" oder "OLD"
// - ODER Größe = 0 (korrupt)
IF vFileDate < vCutoffDate OR
WildMatch('$(vFile)', '*TEMP*', '*OLD*', '*_test*') > 0 OR
vFileSize = 0 THEN
EXECUTE cmd /c del "$(vQVDPath)$(vFile).qvd";
TRACE ✓ Gelöscht: $(vFile) ($(vFileDate), $(vFileSize) MB);
END IF
NEXT i
DROP TABLE FileList;
// ═══════════════════════════════════════════════════════════
// WEEKLY FULL RELOAD (gegen Data Drift)
// ═══════════════════════════════════════════════════════════
// Auch bei Incremental Loads: Einmal pro Woche Full Reload!
IF WeekDay(Today()) = 0 THEN // Sonntag
TRACE ═══════════════════════════════════════;
TRACE WEEKLY FULL RELOAD gestartet;
TRACE ═══════════════════════════════════════;
// Incremental-Load-Tracking zurücksetzen
LET vLastLoadDate = '1900-01-01';
// Jetzt lädt alles als Full Reload...
END IF
Wie implementiert man Performance-Monitoring in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// QVD PERFORMANCE MONITORING
// ═══════════════════════════════════════════════════════════
// Diese Metrics in separate Monitoring-Tabelle loggen
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// Performance-Metriken sammeln
// ═══════════════════════════════════════════════════════════
PerformanceLog:
LOAD * INLINE [
Metric, Value, Unit, Timestamp
];
// Startzeit merken
LET vStartTime = Now();
// Daten laden
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);
// Endzeit und Dauer
LET vEndTime = Now();
LET vDuration = Interval(vEndTime - vStartTime, 's');
LET vRecordCount = NoOfRows('Orders');
LET vThroughput = vRecordCount / vDuration; // Rows per second
// Metriken in Tabelle schreiben
CONCATENATE (PerformanceLog)
LOAD * INLINE [
Metric, Value, Unit, Timestamp
Orders_LoadTime, $(vDuration), Seconds, $(vEndTime)
Orders_RecordCount, $(vRecordCount), Rows, $(vEndTime)
Orders_Throughput, $(vThroughput), Rows/Sec, $(vEndTime)
Orders_FileSize, $(FileSize('lib://QVDs/Orders.qvd')), Bytes, $(vEndTime)
];
// Logs persistent speichern
STORE PerformanceLog INTO [lib://Monitoring/Performance_Log.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// ALERTING: Performance-Schwellenwerte prüfen
// ═══════════════════════════════════════════════════════════
IF vDuration > 300 THEN // Mehr als 5 Minuten
TRACE ⚠️ PERFORMANCE ALERT: Load dauerte $(vDuration)s!;
// Email-Alert senden
EXECUTE powershell -Command "Send-MailMessage -To 'admin@company.com' -From 'qlik@company.com' -Subject 'QVD Performance Alert' -Body 'Orders.qvd Load time: $(vDuration)s' -SmtpServer 'smtp.company.com'";
END IF
IF vRecordCount < 1000000 * 0.9 THEN // 10% weniger als erwartet
TRACE ⚠️ DATA QUALITY ALERT: Nur $(vRecordCount) Zeilen!;
END IF
Was sind die Cloud-Native QVD-Strategien für 2024-2025?
Was sind die Besonderheiten und Limits von Qlik Cloud?
Kritische Unterschiede zu On-Premise:
| Aspekt | On-Premise | Qlik Cloud SaaS |
|---|---|---|
| Max QVD Size | Unbegrenzt (RAM-limitiert) | 6 GB empfohlen |
| Storage | Lokales Filesystem | S3/Azure Blob/Google Cloud |
| Connection Strings | lib:// mit lokalen Pfaden |
Space-aware Syntax |
| Scheduling | QMC/Task Scheduler | Cloud Automations |
| Parallel Processing | Multi-Core Server | Elastic Cloud Capacity |
Was ist die Cloud-QVD-Architektur in Bezug auf QVD-Optimierung?
// ═══════════════════════════════════════════════════════════
// QLIK CLOUD: Space-Aware QVD Access
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// SZENARIO 1: QVDs im gleichen Space
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://DataFiles/Orders.qvd] (qvd);
// Standard Syntax - funktioniert!
// ═══════════════════════════════════════════════════════════
// SZENARIO 2: QVDs in anderem Space (Cross-Space Access)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://ETL_Space:DataFiles/Orders.qvd] (qvd);
// ^^^^^^^^^^ Space-Name!
// Zugriff über Space-Grenzen hinweg
// ═══════════════════════════════════════════════════════════
// SZENARIO 3: Hybrid (On-Premise QVDs für Cloud-App)
// ═══════════════════════════════════════════════════════════
// Via Data Gateway verbunden:
Orders:
LOAD *
FROM [lib://OnPrem_Gateway:QVDs/Orders.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// SPEICHERN in verschiedene Spaces
// ═══════════════════════════════════════════════════════════
STORE Orders INTO
[lib://Production_Space:DataFiles/Orders.qvd] (qvd);
Was sind die Cloud Storage Connectors in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// AMAZON S3
// ═══════════════════════════════════════════════════════════
// Connection in Qlik Cloud erstellt als "S3_Bucket"
// Lesen
Orders:
LOAD *
FROM [lib://S3_Bucket/qlik/data/Orders.qvd] (qvd);
// Schreiben
STORE Orders INTO
[lib://S3_Bucket/qlik/data/Orders_$(vDate).qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// AZURE BLOB STORAGE
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://Azure_Storage/qlik-container/Orders.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// GOOGLE DRIVE (nur lesen!)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://GoogleDrive/Qlik Data/Orders.qvd] (qvd);
// ⚠️ STORE funktioniert NICHT - nur Read-Only!
// ═══════════════════════════════════════════════════════════
// GOOGLE CLOUD STORAGE
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://GCS_Bucket/qlik/Orders.qvd] (qvd);
STORE Orders INTO
[lib://GCS_Bucket/qlik/Orders_$(vDate).qvd] (qvd);
Was ist Cloud-Optimierte Partitionierung und wie funktioniert das 6GB-Limit?
// ═══════════════════════════════════════════════════════════
// CLOUD-STRATEGIE: Kleinere Partitionen für 6GB-Limit
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// On-Premise (alte Strategie):
// - Jahres-Partitionen: 12 Monate à 10 GB = 120 GB/Jahr
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// Cloud (neue Strategie):
// - Monats-Partitionen: 1 Monat à 5 GB = safe für Cloud!
// ═══════════════════════════════════════════════════════════
SET vS3Bucket = 'lib://S3_Bucket/qlik/partitions';
FOR i = 0 to 35 // Letzten 36 Monate
LET vMonth = Date(MonthStart(Today(), -$(i)), 'YYYY-MM');
LET vMonthStart = Date(MonthStart(Today(), -$(i)), 'YYYY-MM-DD');
LET vMonthEnd = Date(MonthEnd(Today(), -$(i)), 'YYYY-MM-DD');
Orders_Partition:
LOAD *
FROM [lib://SourceDB]
(SQL SELECT * FROM Orders
WHERE OrderDate >= '$(vMonthStart)'
AND OrderDate <= '$(vMonthEnd)');
// Partition Size prüfen
LET vPartitionSize = NoOfRows('Orders_Partition') * 100 / 1024 / 1024; // Grobe Schätzung in GB
IF vPartitionSize > 5 THEN
TRACE ⚠️ WARNUNG: Partition $(vMonth) ist $(vPartitionSize) GB!;
TRACE ⚠️ Überlegen Sie weitere Aufteilung (z.B. nach Region);
// Option: Nach Region weiter aufteilen
FOR EACH vRegion in 'EMEA', 'AMER', 'APAC'
RegionalOrders:
LOAD *
RESIDENT Orders_Partition
WHERE Region = '$(vRegion)';
STORE RegionalOrders INTO
[$(vS3Bucket)/Orders_$(vMonth)_$(vRegion).qvd] (qvd);
DROP TABLE RegionalOrders;
NEXT vRegion
ELSE
// Partition OK, normal speichern
STORE Orders_Partition INTO
[$(vS3Bucket)/Orders_$(vMonth).qvd] (qvd);
END IF
DROP TABLE Orders_Partition;
NEXT i
// ═══════════════════════════════════════════════════════════
// CONSUMER: Intelligentes Laden mit dynamischer Erkennung
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [$(vS3Bucket)/Orders_2024-*.qvd] (qvd);
// Lädt alle Monate UND alle Regionen via Wildcard!
Wie funktioniert CI/CD und Automation in der Cloud für QVD-Optimierung in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// QLIK CLOUD AUTOMATIONS: QVD-Generation automatisieren
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// AUTOMATION TRIGGER: Täglich 02:00 Uhr
// ═══════════════════════════════════════════════════════════
// 1. Start App Reload: "01_Extract_Orders"
// 2. Warten auf Success
// 3. Start App Reload: "02_Transform_Orders"
// 4. Warten auf Success
// 5. Start App Reload: "03_Sales_Dashboard"
// 6. Bei Fehler: Send Email Alert
// ═══════════════════════════════════════════════════════════
// ODER: Via REST API (für externe Orchestrierung)
// ═══════════════════════════════════════════════════════════
// PowerShell Script (läuft extern):
$tenant = "your-tenant.region.qlikcloud.com"
$appId = "abc123..."
$apiKey = "Bearer your-api-key"
$headers = @{
"Authorization" = $apiKey
"Content-Type" = "application/json"
}
$uri = "https://$tenant/api/v1/reloads"
$body = @{
"appId" = $appId
"partial" = $false
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
Write-Host "Reload started: $($response.id)"
// ═══════════════════════════════════════════════════════════
// GIT INTEGRATION: Version Control für QVD-Generator-Apps
// ═══════════════════════════════════════════════════════════
// In Qlik Cloud: App mit Git Repository verbinden
// 1. Settings > Version Control
// 2. Connect to Git (GitHub/GitLab/Azure DevOps)
// 3. Branch: main / develop / feature/*
// Workflow:
// - Developer: Ändert Script in "develop" Branch
// - Commit: "Added region-based partitioning"
// - Pull Request → Review
// - Merge to "main"
// - Qlik Cloud: Auto-Deploy zu Production Space
// Script-Beispiel für Git-freundliches Logging:
// vVersion = '1.5.2';
// vLastModified = '2024-01-15';
// vAuthor = 'data.team@company.com';
Ist Parquet eine Alternative oder Ergänzung zu QVD in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// PARQUET: Open-Source Alternative mit Cloud-Vorteilen
// ═══════════════════════════════════════════════════════════
// Seit Qlik Sense Mai 2024: Native Parquet Support!
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// Laden aus Parquet (neue Syntax!)
// ═══════════════════════════════════════════════════════════
Orders:
LOAD *
FROM [lib://DataLake/orders.parquet]
(parquet);
// ═══════════════════════════════════════════════════════════
// VERGLEICH: QVD vs PARQUET
// ═══════════════════════════════════════════════════════════
// QVD Vorteile:
// ✓ 10-30% schneller als Parquet in Qlik
// ✓ Qlik-spezifische Metadaten (Comments, Tags)
// ✓ Perfekt für Qlik-only Pipelines
// Parquet Vorteile:
// ✓ 50-100x kleinere Dateien (bessere Kompression!)
// ✓ Plattform-unabhängig (Python, Spark, ML)
// ✓ Cloud-native (S3, ADLS, GCS standard)
// ✓ Kostenersparnis (weniger Storage)
// ═══════════════════════════════════════════════════════════
// HYBRID-STRATEGIE: Best of Both Worlds
// ═══════════════════════════════════════════════════════════
// ┌──────────────────────────────────────────┐
// │ SOURCE SYSTEMS │
// │ (SQL, SAP, APIs) │
// └─────────────┬────────────────────────────┘
// ▼
// ┌──────────────────────────────────────────┐
// │ EXTRACT LAYER (QVD) │
// │ - Optimiert für Qlik-Reloads │
// │ - Schnell, Qlik-intern │
// └─────────────┬────────────────────────────┘
// ▼
// ┌──────────────────────────────────────────┐
// │ TRANSFORM LAYER │
// │ ├─ Business QVDs (für Qlik Apps) │
// │ └─ Parquet Files (für Data Science) │
// └──────────────┬───────────────────────────┘
// │
// ┌─────────┴──────────┐
// ▼ ▼
// ┌─────────┐ ┌──────────┐
// │ Qlik │ │ Python │
// │ Apps │ │ ML/AI │
// └─────────┘ └──────────┘
// Praktische Implementierung:
Transform_Orders:
LOAD * FROM [...]; // Daten transformieren
// Für Qlik: QVD
STORE Transform_Orders INTO
[lib://QVDs/Transform/Orders.qvd] (qvd);
// Für Data Science: Parquet
STORE Transform_Orders INTO
[lib://DataLake/transform/orders.parquet] (parquet);
// Nun können beide Teams optimal arbeiten!
Wie kann ich Governance, Monitoring und Best Practices für QVD-Optimierung umsetzen?
Was ist das QVD-Governance-Framework für schnellere Loads in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// QVD METADATA MANAGEMENT
// ═══════════════════════════════════════════════════════════
// Zentrale Metadata-Tabelle für alle QVDs
// ═══════════════════════════════════════════════════════════
QVD_Catalog:
LOAD * INLINE [
QVD_Name, Layer, Source_System, Owner, Refresh_Frequency, Description, Created_Date, Last_Modified
Orders.qvd, Extract, ERP_SQL, data.team@company.com, Daily, Raw order data from ERP system, 2023-01-15, 2024-01-15
Fact_Orders.qvd, Transform, Orders.qvd, data.team@company.com, Daily, Business-ready orders with transformations, 2023-01-15, 2024-01-15
Dim_Customer.qvd, Transform, CRM_SQL, data.team@company.com, Weekly, Customer master data, 2023-01-15, 2024-01-10
];
STORE QVD_Catalog INTO [lib://Metadata/QVD_Catalog.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// AUTOMATISCHES METADATA-SCANNING
// ═══════════════════════════════════════════════════════════
// Scannt QVD-Verzeichnis und extrahiert Metadata
// ═══════════════════════════════════════════════════════════
QVD_Inventory:
LOAD
QvdTableName(FileName) as TableName,
QvdNoOfRecords(FileName) as RecordCount,
QvdNoOfFields(FileName) as FieldCount,
FileSize(FileName) / 1024 / 1024 as FileSizeMB,
FileTime(FileName) as LastModified,
FileName
FROM [lib://QVDs/Transform/*.qvd]
(QVD);
// Feldliste für jedes QVD
FOR i = 0 to NoOfRows('QVD_Inventory') - 1
LET vFile = Peek('FileName', i, 'QVD_Inventory');
LET vTable = Peek('TableName', i, 'QVD_Inventory');
LET vFieldCount = Peek('FieldCount', i, 'QVD_Inventory');
FOR j = 0 to vFieldCount - 1
LET vFieldName = QvdFieldName('$(vFile)', j);
CONCATENATE (QVD_Fields)
LOAD * INLINE [
QVD_Name, Table_Name, Field_Name
$(vFile), $(vTable), $(vFieldName)
];
NEXT j
NEXT i
STORE QVD_Inventory INTO [lib://Metadata/QVD_Inventory.qvd] (qvd);
STORE QVD_Fields INTO [lib://Metadata/QVD_Fields.qvd] (qvd);
Wie funktioniert das Data Lineage Tracking in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// LINEAGE TRACKING: Wer nutzt welche QVDs?
// ═══════════════════════════════════════════════════════════
DataLineage:
LOAD * INLINE [
Source_QVD, Target_App, App_Owner, Load_Frequency
Extract_Orders.qvd, 01_Transform_Orders.qvf, ETL_Team, Daily
Fact_Orders.qvd, Sales_Dashboard.qvf, Sales_Team, Hourly
Fact_Orders.qvd, Finance_Report.qvf, Finance_Team, Daily
Fact_Orders.qvd, Executive_KPIs.qvf, Management, Daily
Dim_Customer.qvd, Sales_Dashboard.qvf, Sales_Team, Hourly
Dim_Customer.qvd, Marketing_Analytics.qvf, Marketing_Team, Weekly
];
// Impact Analysis: Wenn Fact_Orders.qvd sich ändert?
ImpactAnalysis:
LOAD
Source_QVD,
Count(DISTINCT Target_App) as Affected_Apps,
Concat(DISTINCT Target_App, ', ') as App_List
RESIDENT DataLineage
WHERE Source_QVD = 'Fact_Orders.qvd'
GROUP BY Source_QVD;
// Resultat: "3 Apps betroffen: Sales_Dashboard, Finance_Report, Executive_KPIs"
Wie wird Qualitätssicherung und Testing in QVD-Optimierung durchgeführt?
// ═══════════════════════════════════════════════════════════
// DATA QUALITY CHECKS für QVDs
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// Check 1: Record Count im erwarteten Bereich?
// ═══════════════════════════════════════════════════════════
QualityCheck_RecordCount:
LOAD
'Orders' as TableName,
QvdNoOfRecords('lib://QVDs/Orders.qvd') as ActualCount,
1000000 as ExpectedMin,
5000000 as ExpectedMax
AUTOGENERATE 1;
// Validation
IF Peek('ActualCount', 0) < Peek('ExpectedMin', 0) OR
Peek('ActualCount', 0) > Peek('ExpectedMax', 0) THEN
TRACE ⚠️ DATA QUALITY ALERT: Orders.qvd außerhalb erwarteten Bereichs!;
// Alert senden
END IF
// ═══════════════════════════════════════════════════════════
// Check 2: Duplikate in Primary Key?
// ═══════════════════════════════════════════════════════════
Orders_Test:
LOAD
OrderID,
Count(OrderID) as DuplicateCount
FROM [lib://QVDs/Orders.qvd] (qvd)
GROUP BY OrderID
HAVING Count(OrderID) > 1;
IF NoOfRows('Orders_Test') > 0 THEN
TRACE ⚠️ DATA QUALITY ALERT: $(NoOfRows('Orders_Test')) Duplikate in OrderID!;
// Duplikate loggen
STORE Orders_Test INTO [lib://Logs/Duplicates_$(Date(Today(),'YYYYMMDD')).qvd] (qvd);
END IF
DROP TABLE Orders_Test;
// ═══════════════════════════════════════════════════════════
// Check 3: NULL-Werte in kritischen Feldern?
// ═══════════════════════════════════════════════════════════
Orders_Nulls:
LOAD
'CustomerID' as FieldName,
Sum(If(IsNull(CustomerID), 1, 0)) as NullCount
FROM [lib://QVDs/Orders.qvd] (qvd)
UNION
LOAD
'Amount' as FieldName,
Sum(If(IsNull(Amount), 1, 0)) as NullCount
FROM [lib://QVDs/Orders.qvd] (qvd);
// Alle Checks zusammenfassen
QualityReport:
LOAD
Today() as CheckDate,
FieldName,
NullCount,
If(NullCount > 0, 'FAIL', 'PASS') as Status
RESIDENT Orders_Nulls;
STORE QualityReport INTO
[lib://Quality/DailyChecks_$(Date(Today(),'YYYYMMDD')).qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// Check 4: Datums-Konsistenz
// ═══════════════════════════════════════════════════════════
DateConsistency:
LOAD
OrderID,
OrderDate,
ShipDate
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE ShipDate < OrderDate; // Shipdate vor Orderdate = unmöglich!
IF NoOfRows('DateConsistency') > 0 THEN
TRACE ⚠️ DATA QUALITY ALERT: $(NoOfRows('DateConsistency')) Orders mit ShipDate < OrderDate!;
END IF
Was sind die Benennungsrichtlinien und die Ordnerstruktur in Qlik Sense?
📁 QVD Root Directory
│
├── 📁 01_Extract/ ← Layer 1: Raw Data
│ ├── Extract_SAP_BSEG_2024-01-15.qvd
│ ├── Extract_SAP_BSEG_CURRENT.qvd
│ ├── Extract_SQL_Orders_2024-01-15.qvd
│ ├── Extract_SQL_Orders_CURRENT.qvd
│ └── 📁 Archive/
│ └── Extract_SQL_Orders_2024-01-01.qvd
│
├── 📁 02_Transform/ ← Layer 2: Business Logic
│ ├── Fact_Sales.qvd
│ ├── Fact_Orders.qvd
│ ├── Dim_Customer.qvd
│ ├── Dim_Product.qvd
│ ├── Bridge_Order_Product.qvd
│ └── Master_Calendar.qvd
│
├── 📁 03_Partitions/ ← Partitionierte QVDs
│ ├── 📁 Orders/
│ │ ├── Orders_2024-01.qvd
│ │ ├── Orders_2024-02.qvd
│ │ └── Orders_2024-03.qvd
│ └── 📁 Transactions/
│ ├── Trans_2024-01-01.qvd
│ └── Trans_2024-01-02.qvd
│
├── 📁 04_Metadata/ ← Governance & Tracking
│ ├── QVD_Catalog.qvd
│ ├── QVD_Inventory.qvd
│ ├── DataLineage.qvd
│ └── QualityChecks.qvd
│
├── 📁 05_Temp/ ← Temporäre QVDs
│ └── (werden täglich bereinigt)
│
└── 📁 06_Archive/ ← Alte Versionen (Backup)
└── (Retention: 90 Tage)
═══════════════════════════════════════════════════════════
NAMING CONVENTIONS
═══════════════════════════════════════════════════════════
Extract Layer:
Format: Extract_<SourceSystem>_<TableName>_<Timestamp>.qvd
Beispiel: Extract_SAP_BSEG_2024-01-15.qvd
Transform Layer:
Format: <Type>_<EntityName>.qvd
Typen:
- Fact_* (Faktentabellen)
- Dim_* (Dimensionen)
- Bridge_* (Many-to-Many)
- Master_* (Referenzdaten)
Beispiele:
- Fact_Sales.qvd
- Dim_Customer.qvd
- Bridge_Order_Product.qvd
- Master_ExchangeRates.qvd
Partitions:
Format: <EntityName>_<PartitionKey>.qvd
Beispiele:
- Orders_2024-01.qvd (Zeit)
- Orders_2024_EMEA.qvd (Jahr × Region)
- Trans_2024-01-15_H14.qvd (Tag × Stunde)
Temporäre Files:
Format: <Name>_TEMP_<Timestamp>.qvd
Beispiel: Orders_TEMP_20240115_143022.qvd
Wie funktioniert Section Access mit QVDs in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// SECTION ACCESS: Row-Level Security
// ═══════════════════════════════════════════════════════════
// ⚠️ WICHTIG: Section Access BRICHT QVD-Optimierung!
// Aber das ist OK - Security geht vor Performance
// ═══════════════════════════════════════════════════════════
Section Access;
// Benutzer-Tabelle (normalerweise aus AD/LDAP oder QVD)
ACCESS:
LOAD * INLINE [
ACCESS, USERID, REGION
USER, JOHN.DOE, EMEA
USER, JANE.SMITH, AMER
USER, MIKE.JONES, APAC
ADMIN, ADMIN, *
];
Section Application;
// Daten laden (wird per User gefiltert!)
Orders:
LOAD
OrderID,
CustomerID,
Region, // ← Muss in Section Access sein!
Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE 1=1; // Erzwingt Non-Optimized (für Section Access nötig)
// User sieht nur seine Region:
// - JOHN.DOE sieht nur EMEA
// - JANE.SMITH sieht nur AMER
// - ADMIN sieht alles (*)
// ═══════════════════════════════════════════════════════════
// ALTERNATIVE: Separate QVDs pro Region (optimiert!)
// ═══════════════════════════════════════════════════════════
// Besser für Performance:
// 1. Erstelle Orders_EMEA.qvd, Orders_AMER.qvd, Orders_APAC.qvd
// 2. User lädt nur sein QVD (Region via Variable)
// 3. Bleibt optimiert!
LET vUserRegion = OSUser(); // Oder aus Datenbank laden
IF vUserRegion = 'EMEA' THEN
Orders:
LOAD * FROM [lib://QVDs/Orders_EMEA.qvd] (qvd);
ELSEIF vUserRegion = 'AMER' THEN
Orders:
LOAD * FROM [lib://QVDs/Orders_AMER.qvd] (qvd);
END IF
// Resultat: Optimized Load + Security!
Was sind Advanced Patterns und Techniken zur QVD-Optimierung in Qlik Sense?
Wie funktioniert das dynamische QVD-Loading in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// PATTERN: User wählt Zeitraum → App lädt nur benötigte QVDs
// ═══════════════════════════════════════════════════════════
// Variable: User-Selection (aus UI oder Parameter)
LET vStartDate = '2024-01-01';
LET vEndDate = '2024-06-30';
// Berechne benötigte Monate
LET vStartMonth = Date(MonthStart(vStartDate), 'YYYY-MM');
LET vEndMonth = Date(MonthStart(vEndDate), 'YYYY-MM');
// Loop durch Monate zwischen Start und End
LET vCurrentMonth = vStartMonth;
DO WHILE Date#(vCurrentMonth, 'YYYY-MM') <= Date#(vEndMonth, 'YYYY-MM')
LET vQVDFile = 'lib://QVDs/Partitions/Orders_' & vCurrentMonth & '.qvd';
IF FileSize('$(vQVDFile)') > 0 THEN
CONCATENATE (Orders)
LOAD *
FROM [$(vQVDFile)] (qvd);
TRACE ✓ Geladen: Orders_$(vCurrentMonth);
ELSE
TRACE ⚠ Nicht gefunden: Orders_$(vCurrentMonth);
END IF
// Zum nächsten Monat
LET vCurrentMonth = Date(AddMonths(Date#(vCurrentMonth, 'YYYY-MM'), 1), 'YYYY-MM');
LOOP
// ═══════════════════════════════════════════════════════════
// RESULTAT
// ═══════════════════════════════════════════════════════════
// User wählt Jan-Jun 2024:
// → App lädt 6 Partitionen (6 × 500 MB = 3 GB)
//
// User wählt komplettes Jahr:
// → App lädt 12 Partitionen (12 × 500 MB = 6 GB)
//
// Flexibel und performant!
Wie funktioniert die Multi-Threaded QVD Generation in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// PARALLEL QVD GENERATION via separate Apps
// ═══════════════════════════════════════════════════════════
// Ideal für Multi-Core-Server!
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// ORCHESTRATOR APP: Startet parallel Apps
// ═══════════════════════════════════════════════════════════
// Liste aller zu generierenden Partitionen
PartitionList:
LOAD * INLINE [
Partition
2024-01
2024-02
2024-03
2024-04
2024-05
2024-06
];
// Via QMC Tasks parallel starten (oder API Calls):
FOR i = 0 to NoOfRows('PartitionList') - 1
LET vPartition = Peek('Partition', i, 'PartitionList');
// REST API Call um Task zu starten
EXECUTE powershell -Command "Invoke-RestMethod -Uri 'https://qlik-server/qrs/task/start/full?name=Generate_$(vPartition)' -Method Post -Headers @{...}";
TRACE Gestartet: Generate_$(vPartition);
NEXT i
// ═══════════════════════════════════════════════════════════
// WORKER APP: Generate_2024-01.qvf (eine pro Partition)
// ═══════════════════════════════════════════════════════════
LET vPartition = '2024-01'; // Über Variable übergeben
Orders_Partition:
LOAD *
FROM [lib://DB]
(SQL SELECT * FROM Orders WHERE ... $(vPartition) ...);
STORE Orders_Partition INTO
[lib://QVDs/Orders_$(vPartition).qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH
// ═══════════════════════════════════════════════════════════
// 12 Partitionen à 5 Min = 60 Min sequential
// 12 Partitionen parallel auf 8-Core = 15 Min!
// → 4x schneller!
Was sind Self-Healing QVD Pipelines in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// SELF-HEALING: Automatische Retry und Fallback
// ═══════════════════════════════════════════════════════════
SET ErrorMode = 0; // Fehler nicht abbrechen
LET vMaxRetries = 3;
LET vRetryCount = 0;
LET vSuccess = 0;
// ═══════════════════════════════════════════════════════════
// RETRY LOOP
// ═══════════════════════════════════════════════════════════
DO WHILE vRetryCount < vMaxRetries AND vSuccess = 0
TRACE ═══════════════════════════════════════;
TRACE QVD Generation Versuch $(vRetryCount + 1)/$(vMaxRetries);
TRACE ═══════════════════════════════════════;
// Daten laden
Orders:
LOAD *
FROM [lib://DB_Connection]
(SQL SELECT * FROM Orders);
// Erfolgreich?
IF ScriptErrorCount = 0 AND NoOfRows('Orders') > 0 THEN
// QVD speichern
STORE Orders INTO [lib://QVDs/Orders_TEMP.qvd] (qvd);
IF ScriptErrorCount = 0 THEN
// Atomisches Replace
EXECUTE cmd /c move /Y "$(vQVDPath)Orders_TEMP.qvd" "$(vQVDPath)Orders.qvd";
LET vSuccess = 1;
TRACE ✓ Erfolgreich im Versuch $(vRetryCount + 1);
END IF
ELSE
// Fehler aufgetreten
LET vRetryCount = vRetryCount + 1;
TRACE ⚠ Fehler - warte 60 Sekunden vor Retry...;
// Warten vor Retry
EXECUTE timeout /t 60 /nobreak > nul;
END IF
LOOP
// ═══════════════════════════════════════════════════════════
// FALLBACK: Wenn alle Retries fehlschlagen
// ═══════════════════════════════════════════════════════════
IF vSuccess = 0 THEN
TRACE ⚠️⚠️⚠️ KRITISCHER FEHLER ⚠️⚠️⚠️;
TRACE Alle $(vMaxRetries) Versuche fehlgeschlagen;
// Fallback: Verwende gestrige QVD
IF FileSize('lib://QVDs/Orders_BACKUP.qvd') > 0 THEN
TRACE → Fallback: Verwende Backup-QVD;
EXECUTE cmd /c copy "$(vQVDPath)Orders_BACKUP.qvd" "$(vQVDPath)Orders.qvd";
// Alert: Admins benachrichtigen
EXECUTE powershell -Command "Send-MailMessage -To 'admin@company.com' -Subject 'QVD FALLBACK ACTIVATED' -Body 'Orders.qvd verwendet Backup!' ...";
ELSE
TRACE ⚠️ Kein Backup verfügbar - kritischer Fehler!;
EXIT SCRIPT;
END IF
END IF
Wie funktioniert die QVD-Compression-Analyse in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// COMPRESSION ANALYSIS: Wie gut komprimiert Ihre QVD?
// ═══════════════════════════════════════════════════════════
Orders:
LOAD * FROM [lib://DB] (SQL SELECT * FROM Orders);
// Memory-Größe im RAM
LET vMemorySizeMB = Num(DocumentSize() / 1024 / 1024, '#,##0.00');
// QVD speichern
STORE Orders INTO [lib://QVDs/Orders_Test.qvd] (qvd);
// QVD-Dateigröße
LET vFileSizeMB = Num(FileSize('lib://QVDs/Orders_Test.qvd') / 1024 / 1024, '#,##0.00');
// Compression Ratio
LET vCompressionRatio = Num(vMemorySizeMB / vFileSizeMB, '#,##0.0');
TRACE ═══════════════════════════════════════;
TRACE COMPRESSION ANALYSIS:
TRACE Memory Size: $(vMemorySizeMB) MB;
TRACE File Size: $(vFileSizeMB) MB;
TRACE Compression: $(vCompressionRatio):1;
TRACE ═══════════════════════════════════════;
// Interpretation:
// - Compression < 2:1 → Schlecht (viele unique values)
// - Compression 2-5:1 → Normal
// - Compression > 5:1 → Gut (viele repetitive values)
// ═══════════════════════════════════════════════════════════
// FIELD-LEVEL COMPRESSION ANALYSIS
// ═══════════════════════════════════════════════════════════
FOR i = 0 to NoOfFields('Orders') - 1
LET vField = FieldName(i, 'Orders');
LET vDistinctValues = FieldValueCount('$(vField)');
LET vTotalRows = NoOfRows('Orders');
LET vCardinality = vDistinctValues / vTotalRows;
TRACE Field: $(vField);
TRACE Distinct: $(vDistinctValues);
TRACE Cardinality: $(Num(vCardinality, '#,##0.0%'));
TRACE Compression Potential: $(If(vCardinality < 0.1, 'Hoch', If(vCardinality < 0.5, 'Mittel', 'Niedrig')));
TRACE;
NEXT i
// Hohe Cardinality (> 50%) → Kandidat für:
// - AutoNumber() (bei IDs)
// - Separate Dimension Table (bei Strings)
// - Splitting (bei Timestamps)
Wie sieht die Zusammenfassung und die Checklisten zur QVD-Optimierung aus?
Was sind die Schritte in der QVD-Optimierungs-Checkliste?
For a holistic approach to Qlik performance beyond QVD files — covering expressions, memory management, and model optimization — see the comprehensive performance tuning guide. You can also find additional patterns in the QVD best practices on Qlik Community.
Grundlagen (Jedes Projekt!)
- [ ] QVDs nutzen statt direkte DB-Connections für wiederholte Loads
- [ ] Optimierte Loads prüfen im Script-Log («qvd optimized»)
- [ ] Keine Berechnungen im QVD-LOAD – erst laden, dann transformieren
- [ ] WHERE EXISTS() statt WHERE-Vergleiche für Filter
- [ ] DROP TABLE nach STORE um RAM freizugeben
- [ ] Atomische QVD-Updates (via TEMP → FINAL Rename)
Performance (für Apps > 1M Zeilen)
- [ ] Zwei-Stufen-Load implementiert (Optimized + Resident Transform)
- [ ] Felder-Auswahl – nur benötigte Felder laden
- [ ] AutoNumber() für hochkardinalische ID-Felder
- [ ] Timestamp-Splitting (Date + Time statt Timestamp)
- [ ] Incremental Loading für große Tabellen
- [ ] Performance-Monitoring mit Benchmarks
Enterprise (für Production-Umgebungen)
- [ ] Drei-Schichten-Architektur (Extract-Transform-Presentation)
- [ ] Naming Conventions etabliert und dokumentiert
- [ ] Folder-Struktur klar gegliedert (01_Extract, 02_Transform, etc.)
- [ ] Error Handling mit Retries und Alerting
- [ ] Backup-Strategie (wöchentliche Kopien, Archivierung)
- [ ] Metadata-Management (Katalog, Lineage, Inventory)
Skalierung (für > 50M Zeilen)
- [ ] Partitionierung nach Zeit/Region/Hash
- [ ] Cloud-Limits beachtet (6 GB max pro QVD)
- [ ] Parallele Verarbeitung wo möglich
- [ ] Monitoring-Dashboard für QVD-Health
- [ ] Automatisierte Cleanup-Prozesse
Governance
- [ ] Data Lineage dokumentiert (wer nutzt was?)
- [ ] Quality Checks automatisiert
- [ ] Version Control (Git-Integration)
- [ ] Security (Section Access wo nötig)
- [ ] Documentation (Inline-Comments + Wiki)
Was sind die Performance-Benchmarks zum Vergleich in Qlik Sense?
| Szenario | Ohne QVD | Mit QVD (Non-Opt) | Mit QVD (Optimized) | Verbesserung |
|---|---|---|---|---|
| 1M Zeilen, täglicher Load | 8 Min | 3 Min | 15 Sek | 32x |
| 10M Zeilen, simpler Load | 45 Min | 15 Min | 2 Min | 22x |
| 50M Zeilen, gefiltert | 4 Std | 1 Std | 10 Min | 24x |
| 100M Zeilen, partitioniert | N/A | 3 Std | 15 Min | 12x |
Typische Performance-Faktoren:
- Database → QVD (Non-Opt): 3-5x schneller
- Non-Optimized → Optimized: 10-20x schneller
- Kombination: 10-100x Gesamtverbesserung
Wie kann ich die QVD-Optimierung für 100x schnellere Loads in Qlik Sense nutzen?
| Problem | Ursache | Lösung |
|---|---|---|
| Load nicht optimized | Berechnungen im LOAD | Zwei-Stufen-Load (Optimized + Resident) |
| Out of Memory | Alle Tabellen gleichzeitig im RAM | DROP TABLE nach Verarbeitung |
| QVD korrupt | Unterbrochener STORE | Atomisches Update (TEMP → FINAL) |
| Performance sinkt | QVD-Anhäufung über Zeit | Automatischer Cleanup-Job |
| Inkonsistente Daten | Fehlerhafte Incremental Logic | Weekly Full Reload + Sanity Checks |
| Cloud 6GB-Limit | QVD zu groß | Weitere Partitionierung (Monat → Tag) |
Was sind die nächsten Schritte für meine Implementation in Qlik Sense?
Phase 1: Fundament (Woche 1-2)
- 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?
Was sind Blogs und Tutorials zur QVD-Optimierung in Qlik Sense?
- Quick Intelligence – UK Qlik-Experten
- Barry Harmsen’s Qlik Sense Blog
Welche Tools und Utilities helfen bei der QVD-Optimierung in Qlik Sense?
- QVD Monitor – Qlik Admin Playbook
- 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 – Nur Changes laden
→ Nächster Artikel: Synthetic Keys & Circular References auflösen
Verwandte Artikel:
- Load-Strategien – RESIDENT vs PRECEDING vs INLINE
- Star Schema in Qlik – Performance & Klarheit
- Deployment & Version Control Patterns
Previous: Incremental Loading | Next: Data Model Problems