📚 Qlik Sense Kurs – Artikel 8 von 28
← Vorheriger Artikel: Incremental Loading – Nur Changes laden
→ Nächster Artikel: Synthetic Keys & Circular References auflösen
Was lernen Sie über QVD-Optimierung für 100x schnellere Loads in Qlik Sense?
QVD-Dateien (QlikView Data) sind die geheime Performance-Waffe von Qlik – optimierte QVD-Loads erreichen 10-100x schnellere Datenladungen als direkte Datenbankverbindungen. Dieser Performance-Gewinn verwandelt mehrstündige Reloads in Minuten und ermöglicht Enterprise-Analytics in bisher unvorstellbarem Umfang.
Wie kann ich QVD-Optimierung für 100x schnellere Loads in Qlik Sense nutzen?
- QVD-Grundlagen meistern: Verstehen Sie die interne Architektur von QVD-Dateien und warum sie so performant sind
- Optimierte vs. nicht-optimierte Loads: Kennen Sie alle Regeln, die über 100x Geschwindigkeitsunterschiede entscheiden
- Enterprise-Architekturen aufbauen: Implementieren Sie 3- bis 5-Layer-Architekturen für skalierbare Datenmodelle
- Incremental Loading perfektionieren: Reduzieren Sie Datenbanklasten um 99% durch clevere Delta-Load-Patterns
- Massive Datasets partitionieren: Handhaben Sie Milliarden von Zeilen durch intelligente Segmentierung
- Performance troubleshooten: Diagnostizieren und beheben Sie die häufigsten QVD-Probleme
- Cloud-native Strategien: Nutzen Sie moderne Patterns für Qlik Cloud und hybride Architekturen
- Governance implementieren: Etablieren Sie Monitoring, Versionierung und Qualitätssicherung
Zeitinvestition: 90 Min Lesen + 8-16 Std praktische Übungen
Voraussetzungen: Qlik Sense Desktop/Cloud + grundlegende Datenlade-Kenntnisse
Schwierigkeitsgrad: Anfänger bis Fortgeschrittene (alle Level finden wertvolle Insights)
Was sind die Grundlagen und die Architektur von QVD?
Wie erzielen QVD-Dateien ihre Performance in Qlik Sense?
QVD-Dateien nutzen keine traditionellen Kompressionsalgorithmen wie ZIP oder LZH. Stattdessen erreichen sie dramatische Größenreduktion durch Symbol-Tabellen kombiniert mit Bit-stuffed Pointers – eine Technik, die jeden eindeutigen Feldwert genau einmal speichert und dann mit minimaler Bit-Anzahl referenziert.
Die drei Teile einer QVD-Datei:
┌─────────────────────────────────────┐
│ 1. XML-Header (Metadata) │
│ - Feldnamen und Datentypen │
│ - Creation Timestamp │
│ - Original SQL Statements │
│ - Recordanzahl und Kompressionsinfo│
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 2. Symbol-Tabellen (Column-Major) │
│ - Jeder eindeutige Wert pro Feld │
│ - Sortiert und indiziert │
│ - Einmal gespeichert, oft genutzt │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 3. Index-Tabelle (Row-Major) │
│ - Bit-stuffed Pointers zu Symbolen │
│ - Minimale Bit-Anzahl pro Zeile │
│ - Hochkomprimiert │
└─────────────────────────────────────┘
Praktisches Beispiel: Wie Kompression funktioniert
Betrachten Sie eine Tabelle mit 1 Million Zeilen und einem Datumsfeld mit 1.460 eindeutigen Daten:
Ohne QVD (traditionelle Speicherung):
- 1.000.000 Zeilen × 8 Bytes (Datum) = 8.000.000 Bytes
- Gesamt: ~7,6 MB
Mit QVD (Symbol-Tabelle + Pointer):
- Symbol-Tabelle: 1.460 Daten × 8 Bytes = 11.680 Bytes
- Pointer: 1.000.000 × 11 Bits (2^11 = 2.048 kann 1.460 darstellen) = 1.375.000 Bytes
- Gesamt: ~1,3 MB (83% Reduzierung!)
Warum optimierte Loads so schnell sind:
Die QVD-Architektur spiegelt exakt wider, wie Qlik Daten im RAM speichert. Ein optimierter Load überträgt Daten direkt von der Festplatte in den RAM in diesem komprimierten Format ohne jegliche Transformation:
Optimized Load: Disk → RAM (direkte Block-Übertragung)
Geschwindigkeit: Pure I/O Speed
Non-Optimized: Disk → Entpacken → Transformieren →
Neu-Komprimieren → RAM
Geschwindigkeit: 10-100x langsamer
Wie starte ich in 10 Minuten mit der optimierten QVD in Qlik Sense?
Das Problem: Sie laden täglich 500.000 Zeilen aus einer SQL-Datenbank und jeder Load dauert 2 Minuten.
Die Lösung:
// ═══════════════════════════════════════════════════════════
// SCHRITT 1: QVD ERSTELLEN (Einmalig oder täglich geplant)
// ═══════════════════════════════════════════════════════════
// Daten aus der langsamen Datenbank laden
Raw_Customer_Data:
LOAD
CustomerID,
CustomerName,
Region,
Email,
CreatedDate,
LastModified
FROM [lib://DB_Connection] (SQL SELECT * FROM Customers);
// Als QVD-Datei speichern (unser "Download")
STORE Raw_Customer_Data INTO [lib://QVDs/Extract/Customers.qvd] (qvd);
// Original-Daten aus dem Speicher löschen (räumt auf)
DROP TABLE Raw_Customer_Data;
TRACE QVD erstellt: $(NoOfRows('Raw_Customer_Data')) Zeilen gespeichert;
// ═══════════════════════════════════════════════════════════
// SCHRITT 2: BLITZSCHNELL AUS DER QVD LADEN (Ihre tägliche App)
// ═══════════════════════════════════════════════════════════
Customers:
LOAD
CustomerID,
CustomerName,
Region
FROM [lib://QVDs/Extract/Customers.qvd] (qvd);
// ✓ Dieser Load ist OPTIMIERT = blitzschnell!
Performance-Ergebnis:
- Vorher (Database): 2 Minuten für 500k Zeilen
- Nachher (QVD): 8 Sekunden für 500k Zeilen
- Verbesserung: 15x schneller!
✓ Checkpoint: Öffnen Sie das Script-Log (Progress-Fenster). Sehen Sie «(qvd optimized)» neben dem Tabellennamen? Perfekt! Falls nicht, lesen Sie Abschnitt 2.2.
Was sind die Regeln für optimierte vs. nicht-optimierte Loads in Qlik Sense?
Was sind die kritischen Faktoren für die QVD-Optimierung in Qlik Sense?
Jede Transformation im LOAD-Statement erzwingt den nicht-optimierten Modus:
❌ Diese Operationen brechen die Optimierung:
// ══════════════════════════════════════════
// BERECHNETE FELDER
// ══════════════════════════════════════════
LOAD
OrderID,
Year(OrderDate) as OrderYear, // ❌ Datum-Funktion
Amount * 1.19 as GrossAmount // ❌ Berechnung
FROM [lib://QVDs/Orders.qvd] (qvd);
// ══════════════════════════════════════════
// WHERE-KLAUSELN MIT VERGLEICHEN
// ══════════════════════════════════════════
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Country = 'Germany'; // ❌ Vergleichsoperator
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Amount > 1000; // ❌ Numerischer Vergleich
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Year(OrderDate) = 2024; // ❌ Funktion in WHERE
// ══════════════════════════════════════════
// STRING-MANIPULATIONEN
// ══════════════════════════════════════════
LOAD
CustomerID,
Upper(CustomerName) as CustomerName // ❌ String-Funktion
FROM [lib://QVDs/Customers.qvd] (qvd);
// ══════════════════════════════════════════
// JOINS UND CONCATENATES
// ══════════════════════════════════════════
Orders:
LOAD * FROM [lib://QVDs/Orders.qvd] (qvd);
LEFT JOIN (Orders) // ❌ JOIN bricht Optimierung
LOAD * FROM [lib://QVDs/Customers.qvd] (qvd);
// ══════════════════════════════════════════
// DUPLIZIERTE FELDER
// ══════════════════════════════════════════
LOAD
OrderID,
OrderID as Order_Number // ❌ Feld zweimal laden
FROM [lib://QVDs/Orders.qvd] (qvd);
// ══════════════════════════════════════════
// ORDER BY, GROUP BY, DISTINCT (vor Feb 2018)
// ══════════════════════════════════════════
LOAD DISTINCT CustomerID // ❌ In älteren Versionen
FROM [lib://QVDs/Customers.qvd] (qvd)
ORDER BY CustomerID; // ❌ ORDER BY
✅ Diese Operationen bleiben optimiert:
// ══════════════════════════════════════════
// FELD-UMBENENNUNG
// ══════════════════════════════════════════
LOAD
OrderID as Order_Number, // ✓ Nur Label-Änderung
CustomerID as Customer_ID,
OrderDate as Date
FROM [lib://QVDs/Orders.qvd] (qvd);
// ══════════════════════════════════════════
// FELD-SELEKTION
// ══════════════════════════════════════════
LOAD
CustomerID, // ✓ Nur spezifische Felder
CustomerName,
Region // Andere Felder weglassen
FROM [lib://QVDs/Customers.qvd] (qvd);
// ══════════════════════════════════════════
// WHERE EXISTS() - Der Optimierungs-Freund
// ══════════════════════════════════════════
// Schritt 1: Kleine Tabelle mit gewünschten Keys laden
ValidCustomers:
LOAD DISTINCT CustomerID
FROM [lib://Dims/Customers.qvd] (qvd)
WHERE Region = 'DACH';
// Schritt 2: Große Tabelle mit WHERE EXISTS filtern
Orders:
LOAD
OrderID,
CustomerID,
Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE EXISTS(CustomerID); // ✓ Bleibt optimiert!
// ══════════════════════════════════════════
// WHERE NOT EXISTS()
// ══════════════════════════════════════════
NewOrders:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE NOT EXISTS(OrderID); // ✓ Bleibt optimiert!
// ══════════════════════════════════════════
// LOAD DISTINCT (ab Feb 2018)
// ══════════════════════════════════════════
UniqueCustomers:
LOAD DISTINCT
CustomerID,
CustomerName
FROM [lib://QVDs/Customers.qvd] (qvd); // ✓ In modernen Versionen optimiert
// ══════════════════════════════════════════
// FIRST/LAST (Zeilenbegrenzung)
// ══════════════════════════════════════════
TestData:
FIRST 1000 // ✓ Optimiert in neueren Versionen
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd);
Was ist die Zweistufige Lade-Strategie in Qlik Sense?
Für maximale Performance: Erst optimiert laden, dann transformieren!
// ═══════════════════════════════════════════════════════════
// PATTERN: OPTIMIZED LOAD + RESIDENT TRANSFORMATION
// ═══════════════════════════════════════════════════════════
// STUFE 1: Optimierter Load (super schnell!)
TempOrders:
LOAD
OrderID,
CustomerID,
OrderDate,
Amount,
Currency
FROM [lib://QVDs/Orders.qvd] (qvd);
// ← Hier steht "(qvd optimized)" im Log ✓
// STUFE 2: Transformationen via RESIDENT (im RAM, schnell!)
Orders:
LOAD
OrderID,
CustomerID,
Date(OrderDate, 'DD.MM.YYYY') as OrderDate,
Year(OrderDate) as OrderYear,
Month(OrderDate) as OrderMonth,
Quarter(OrderDate) as OrderQuarter,
Amount,
Amount * 1.19 as GrossAmount,
If(Amount > 1000, 'High Value', 'Standard') as OrderSegment,
Currency
RESIDENT TempOrders;
// STUFE 3: Aufräumen
DROP TABLE TempOrders;
// ═══════════════════════════════════════════════════════════
// WARUM DAS FUNKTIONIERT
// ═══════════════════════════════════════════════════════════
//
// 1. TempOrders lädt mit QVD-Geschwindigkeit (10-100x schnell)
// 2. Transformationen passieren im RAM (auch schnell!)
// 3. Statt 100% langsam → 5% langsam + 95% schnell
//
// Beispiel 10M Zeilen:
// - Alles non-optimized: 15 Minuten
// - Optimized + Resident: 2 Min (QVD) + 1 Min (Transform) = 3 Min
// - Faktor 5x schneller!
Wie funktioniert WHERE EXISTS() für große Datenmengen in Qlik Sense?
// ═══════════════════════════════════════════════════════════
// PROBLEM:
// Sie haben 100M Zeilen Orders, brauchen aber nur 5M für bestimmte Kunden
// ═══════════════════════════════════════════════════════════
// SCHLECHTE LÖSUNG (Non-Optimized, sehr langsam):
Orders:
LOAD *
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE Country = 'Germany';
// ❌ Muss alle 100M Zeilen prüfen!
// ═══════════════════════════════════════════════════════════
// GUTE LÖSUNG (Optimized, 4-8x schneller):
// ═══════════════════════════════════════════════════════════
// Schritt 1: Kleine Key-Tabelle laden
GermanCustomers:
LOAD DISTINCT CustomerID
FROM [lib://QVDs/Customers.qvd] (qvd)
WHERE Country = 'Germany';
// Nur wenige tausend Zeilen, schnell!
// Schritt 2: Große Tabelle mit EXISTS filtern
Orders:
LOAD
OrderID,
CustomerID,
OrderDate,
Amount
FROM [lib://QVDs/Orders.qvd] (qvd)
WHERE EXISTS(CustomerID);
// ✓ Optimized Load! Nur Key-Matching, keine Berechnungen
// ═══════════════════════════════════════════════════════════
// PERFORMANCE-VERGLEICH (Real-World-Daten):
// ═══════════════════════════════════════════════════════════
// 94M Zeilen, Filter auf 5M Zeilen:
// - WHERE Country = 'Germany': 4 Minuten
// - WHERE EXISTS(CustomerID): 30 Sekunden
// - Verbesserung: 8x schneller!
Wie prüfe ich die Optimierung im Script-Log in Qlik Sense?
// So sieht ein optimierter Load im Progress-Log aus:
Orders << Orders.qvd (qvd optimized)
500,000 Zeilen abgerufen
Execution time: 00:00:08
// Non-optimized sieht so aus:
Orders << Orders.qvd
500,000 Zeilen abgerufen
Execution time: 00:02:15
// 🎯 MERKE: Kein "(qvd optimized)" = Problem!
Log-Dateien finden:
- Qlik Sense Desktop:
C:Users<Username>DocumentsQlikSenseApps<AppName>ScriptLog.txt - Qlik Sense Server:
C:ProgramDataQlikSenseLogScript<AppID> - QlikView: Gleicher Ordner wie die QVW-Datei (wenn Logging aktiviert)
Wie funktionieren Enterprise-Architekturen mit QVD-Layern in Qlik Sense?
3.1 Warum Layer-Architekturen?
In kleinen Projekten laden Sie vielleicht direkt aus der Datenbank in Ihre App. Aber in Enterprise-Umgebungen führt das zu:
❌ Problemen ohne Layer-Architektur:
- Jede App lädt redundant aus der Datenbank (DB-Overload!)
- Geschäftslogik wird in 20 Apps dupliziert (Wartungsalptraum)
- Inkonsistente Berechnungen über Apps hinweg (Datenqualitätsprobleme)
- Änderungen an Quelldaten brechen alle Apps gleichzeitig
✅ Vorteile mit Layer-Architektur:
- Einmal extrahieren, x-fach nutzen (DB-Last um 90% reduziert)
- Zentrale Geschäftslogik (eine Änderung, überall wirksam)
- Wiederverwendbare Daten-Assets (Zeit sparen bei neuen Apps)
- Entkopplung (Source-Änderungen betreffen nur Extract-Layer)
Was ist die Drei-Schichten-Architektur (Industry Standard) in Qlik Sense?
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (Layer 3) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Sales │ │ Finance │ │ HR │ │ Ops │ │
│ │Dashboard │ │Dashboard │ │Dashboard │ │Dashboard │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────────────┴─────────────┘ │
│ │ │
└──────────────────────────┼───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ TRANSFORM LAYER (Layer 2) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Business-Ready QVDs mit Transformationen: │ │
│ │ • Fact_Sales.qvd │ │
│ │ • Dim_Customer.qvd │ │
│ │ • Dim_Product.qvd │ │
│ │ • Master_Calendar.qvd │ │
│ │ │ │
│ │ Hier passiert: │ │
│ │ ✓ Datenbereinigung │ │
│ │ ✓ Geschäftslogik │ │
│ │ ✓ Typ-Konvertierungen │ │
│ │ ✓ Star-Schema-Aufbau │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ EXTRACT LAYER (Layer 1) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Raw QVDs - unveränderte Kopien der Quellen: │ │
│ │ • Extract_Orders.qvd │ │
│ │ • Extract_Customers.qvd │ │
│ │ • Extract_Products.qvd │ │
│ │ │ │
│ │ Hier passiert: │ │
│ │ ✓ Pure Extraktion (keine Transformationen!) │ │
│ │ ✓ Minimale DB-Connection-Zeit │ │
│ │ ✓ 1:1 Abbild der Quelle │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
▼
┌─────────────┐
│ DATA │
│ SOURCES │
│ │
│ • SAP │
│ • SQL │
│ • Oracle │
│ • Files │
└─────────────┘
Wie extrahiere ich Raw Data QVDs in Layer 1?
Ziel: Minimale Connection-Zeit zu Quellsystemen, keine Transformationen!
// ═══════════════════════════════════════════════════════════
// EXTRACT LAYER - SCHRITT 1: RAW DATA EXTRACTION
// ═══════════════════════════════════════════════════════════
// App: 01_Extract_ERP_Orders.qvf
// Schedule: Täglich 02:00 Uhr
// Connection Time: < 5 Minuten
// ═══════════════════════════════════════════════════════════
SET ErrorMode = 0; // Fehler abfangen, nicht abbrechen
// Verbindung zur Quelldatenbank
LET vStartTime = Now();
// ═══════════════════════════════════════════════════════════
// Rohdaten 1:1 extrahieren
// ═══════════════════════════════════════════════════════════
Extract_Orders:
LOAD
OrderID,
CustomerID,
ProductID,
OrderDate,
ShipDate,
Amount,
Currency,
Status,
CreatedBy,
CreatedDate,
ModifiedBy,
ModifiedDate
FROM [lib://ERP_Connection] (SQL SELECT * FROM Orders
WHERE ModifiedDate >= '$(vLastExtract)');
// ═══════════════════════════════════════════════════════════
// Speichern mit Timestamp im Dateinamen
// ═══════════════════════════════════════════════════════════
LET vTimestamp = Date(Today(), 'YYYY-MM-DD');
STORE Extract_Orders INTO
[lib://QVDs/01_Extract/Orders_$(vTimestamp).qvd] (qvd);
// Aktuellste Version auch als "current" speichern
STORE Extract_Orders INTO
[lib://QVDs/01_Extract/Orders_CURRENT.qvd] (qvd);
// ═══════════════════════════════════════════════════════════
// Metadaten loggen
// ═══════════════════════════════════════════════════════════
LET vRowCount = NoOfRows('Extract_Orders');
LET vEndTime = Now();
LET vDuration = Interval(vEndTime - vStartTime, 'mm:ss');
TRACE ═══════════════════════════════════════════════;
TRACE EXTRACT ABGESCHLOSSEN;
TRACE Tabelle: Orders;
TRACE Zeilen: $(vRowCount);
TRACE Dauer: $(vDuration);
TRACE Zeitstempel: $(vTimestamp);
TRACE ═══════════════════════════════════════════════;
// Aufräumen
DROP TABLE Extract_Orders;
// Fehlerprüfung
IF ScriptErrorCount > 0 THEN
TRACE FEHLER: $(ScriptErrorCount) Fehler aufgetreten!;
// Alert an Monitoring-System senden
EXECUTE cmd /c echo "Extract Orders failed" | mail -s "QVD Alert" admin@company.com;
END IF
Naming Convention für Extract Layer:
- Format:
Extract_<SourceSystem>_<TableName>_<Timestamp>.qvd - Beispiele:
Extract_SAP_BSEG_2024-01-15.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?
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: