5.2 Merging Tables: Joins, Keeps & Concatenation Semantics
Key Takeaways
JOIN physically merges two tables horizontally into a single table based on shared field names, while KEEP filters records associatively but preserves both tables as distinct physical entities in the data model.
Unqualified JOIN statements default to full OUTER JOIN, whereas LEFT JOIN, RIGHT JOIN, and INNER JOIN explicitly control which table rows are preserved or discarded.
Cartesian explosions occur when joining tables across non-unique key fields, multiplying row counts geometrically and corrupting additive measure aggregations in the presentation layer.
Implicit (automatic) concatenation occurs automatically whenever two tables share an identical field list, which must be overridden with NOCONCATENATE when tables must remain separate.
Explicit CONCATENATE vertically appends rows from one table into an existing target table, inserting nulls for mismatched columns.
Merging Tables: Joins, Keeps & Concatenation Semantics
In Qlik Sense, data modeling centers on managing how tables associate, consolidate, or append within the associative engine. Data architects have three primary script primitives for combining tabular datasets: JOIN, KEEP, and CONCATENATE. Choosing the wrong consolidation mechanism can lead to catastrophic memory bloat, duplicate records, distorted aggregations, or the unintended vanishing of staging tables.
1. Table Consolidation Primitives: The Core Architectural Choices
When two datasets share common fields, the architect must determine how they should exist in the final data model:
┌──────────────────────────────────────────────┐
│ How should two related tables be structured? │
└──────────────────────┬───────────────────────┘
│
┌─────────────────────────────────┼─────────────────────────────────┐
▼ ▼ ▼
Merge Horizontally Filter Associatively Append Vertically
(Single Wider Table) (Keep Separate Tables) (Single Taller Table)
│ │ │
▼ ▼ ▼
JOIN KEEP CONCATENATE
(Columns Combined) (Rows Filtered, 2 Tables) (Rows Stacked)
- JOIN: Physically combines the columns of two tables into a single in-memory table based on matching values in common fields. The secondary table ceases to exist independently.
- KEEP: Compares common field values between two tables to filter rows (retaining matching, non-matching, or directional subsets), but leaves both tables as distinct, separate entities in the data model linked associatively.
- CONCATENATE: Physically appends the rows of one table to another vertically, unifying records under a shared column structure.
2. JOIN Semantics & Prefixes
A JOIN statement precedes a LOAD or SELECT statement and merges its output into another table. In Qlik Sense, joining is always evaluated on all common field names shared between the two tables.
Table A (Left): Table B (Right):
┌───────────┬─────────────┐ ┌───────────┬──────────────┐
│ AccountID │ CompanyName │ │ AccountID │ CreditRating │
├───────────┼─────────────┤ ├───────────┼──────────────┤
│ 101 │ Alpha Corp │ │ 101 │ AAA │
│ 102 │ Beta LLC │ │ 103 │ BBB │
└───────────┴─────────────┘ └───────────┴──────────────┘
│
▼
Result of LEFT JOIN (Table A) Table B:
┌───────────┬─────────────┬──────────────┐
│ AccountID │ CompanyName │ CreditRating │
├───────────┼─────────────┼──────────────┤
│ 101 │ Alpha Corp │ AAA │
│ 102 │ Beta LLC │ - (Null) │
└───────────┴─────────────┴──────────────┘
The Four JOIN Types
LEFT JOIN [(TargetTable)]: Preserves every record from the primary (target/left) table. If a matching key exists in the incoming (right) table, its attributes are appended. If no match exists, the new columns are populated withNULL.RIGHT JOIN [(TargetTable)]: Preserves every record from the incoming (right) table. Records in the primary table that have no match in the incoming table are dropped.INNER JOIN [(TargetTable)]: Preserves only records that have matching keys in both tables. Unmatched rows from both sides are discarded from the final combined table.OUTER JOIN [(TargetTable)](or unqualifiedJOIN): Preserves all records from both tables. Unmatched rows from either side retain their existing attributes and receiveNULLfor missing columns.
Caution
The Unqualified JOIN Default Trap: In Qlik Sense script, writing JOIN LOAD ... without specifying LEFT, RIGHT, or INNER defaults to a full OUTER JOIN. This can inject unexpected null-padded rows into fact tables and cause severe memory inflation.
Specifying the Target Table
Always specify the target table explicitly in parentheses: LEFT JOIN (Fact_Sales) LOAD .... If the target table name is omitted, Qlik joins the incoming data to the immediately preceding loaded table. In complex multi-tab scripts, relying on default target resolution frequently causes data to join into the wrong table, silently corrupting the data model.
The Cartesian Product Disaster
Relational joins in Qlik require strict uniqueness on the join key in at least one of the tables. If both the left table and right table contain duplicate occurrences of the same key value, Qlik performs a many-to-many Cartesian multiplication for that key ( rows):
Orders Table: OrderTags Table:
OrderID | Sales OrderID | TagName
1001 | $50 1001 | Expedited
1001 | $50 (Split Line) 1001 | Fragile
Result of LEFT JOIN:
OrderID | Sales | TagName
1001 | $50 | Expedited
1001 | $50 | Fragile
1001 | $50 | Expedited ◄── 4 Rows Generated! (2 x 2)
1001 | $50 | Fragile ◄── Total Sales now sums to $200 instead of $100!
When a Cartesian join occurs in a fact table, quantitative metrics like revenue and profit are multiplied, producing invalid financial reports. If an architect must attach a single descriptive attribute from a dimension with duplicate keys, they should deduplicate the dimension or use ApplyMap() instead of JOIN.
3. KEEP Semantics: Associative Filtering Without Consolidation
The KEEP prefix provides the same filtering logic as JOIN, but with a fundamental architectural difference: it does not merge the two tables into one. Both tables survive the reload as independent, distinct tables in the associative data model.
Table A (Orders): Table B (Returns):
┌─────────┬────────┐ ┌─────────┬──────────────┐
│ OrderID │ Amount │ │ OrderID │ ReturnReason │
├─────────┼────────┤ ├─────────┼──────────────┤
│ 1 │ $100 │ │ 1 │ Damaged │
│ 2 │ $200 │ │ 3 │ Wrong Item │
└─────────┴────────┘ └─────────┴──────────────┘
│
▼
Result of INNER KEEP (Orders) Returns:
Table Orders (Survives): Table Returns (Survives):
┌─────────┬────────┐ ┌─────────┬──────────────┐
│ OrderID │ Amount │ │ OrderID │ ReturnReason │
├─────────┼────────┤ ├─────────┼──────────────┤
│ 1 │ $100 │ │ 1 │ Damaged │
└─────────┴────────┘ └─────────┴──────────────┘
▲ ▲
└──────── Connected via OrderID ─────┘
The Three KEEP Variants
LEFT KEEP [(TargetTable)]: The incoming table is filtered to retain only records whose shared key values exist in the target table. The target table remains untouched. Both tables persist in the model.RIGHT KEEP [(TargetTable)]: The target table is filtered to retain only records matching the incoming table. Both tables persist in the model.INNER KEEP [(TargetTable)]: Both tables are filtered so that only records with shared keys present in both tables survive. Both tables persist in the model.
Architectural Use Cases for KEEP
- Preserving Distinct Entity Grain: When two tables have different granularities (e.g.,
Customersat 1 row per customer, andTransactionsat millions of rows per customer). AnINNER JOINwould collapse the customer dimension into the fact table, destroying customer-only list box exploration for inactive accounts. AnINNER KEEPfilters both tables to active customers while keeping them separate. - Filtering Without Merging Columns:
KEEPfilters on all common field names, exactly likeJOIN, but leaves both tables in the model. It does not prevent synthetic keys: if the two kept tables share more than one field, Qlik still builds a$Synkey, so rename or combine those fields first. - Staging Pipeline Reduction: Filtering large transaction tables against a small list of active stores before downstream transformations, without collapsing the store table into the transactions.
4. Concatenation Semantics: Explicit vs. Implicit
Concatenation is the vertical stacking of records. Instead of widening a table by adding columns horizontally, concatenation lengthens a table by appending rows.
Explicit Concatenation
Explicit concatenation uses the CONCATENATE [(TargetTable)] prefix before a LOAD statement. It forces the incoming rows to append directly to the target table:
Fact_Sales:
LOAD
OrderID,
CustomerID,
OrderDate,
SalesAmount
FROM [lib://QVD/CurrentYearSales.qvd] (qvd);
// Explicitly append archive records to Fact_Sales
CONCATENATE (Fact_Sales)
LOAD
OrderID,
CustomerID,
OrderDate,
SalesAmount,
ArchiveFlag
FROM [lib://QVD/ArchiveSales.qvd] (qvd);
In explicit concatenation, if the incoming table contains fields that do not exist in the target table (such as ArchiveFlag above), Qlik automatically adds that column to the target table and populates previous rows with NULL. Conversely, fields missing in the incoming table are populated with NULL for the new rows.
Implicit (Automatic) Concatenation
Warning
The Automatic Concatenation Trap: If two tables loaded sequentially in the script possess the exact same number of fields and identical field names, the QIX engine will automatically concatenate the second table into the first, even if no CONCATENATE prefix was written!
Consider this real-world disaster scenario:
// Developer intends to load two distinct tables for comparison
Budget_2026:
LOAD DepartmentID, Month, TargetAmount FROM [lib://Data/Budget.qvd] (qvd);
Actuals_2026:
LOAD DepartmentID, Month, TargetAmount FROM [lib://Data/Actuals.qvd] (qvd);
Because both tables contain the exact fields DepartmentID, Month, and TargetAmount, Qlik automatically concatenates Actuals_2026 into Budget_2026. The table Actuals_2026 completely disappears from the data model! Users searching for Actuals_2026 in the sheet editor will find nothing, and budget figures will be doubled with actuals.
Preventing Automatic Concatenation: NOCONCATENATE
To prevent the QIX engine from automatically concatenating tables with identical schemas, the developer must prepend the NOCONCATENATE keyword:
Budget_2026:
LOAD DepartmentID, Month, TargetAmount FROM [lib://Data/Budget.qvd] (qvd);
// Force Qlik to create a distinct, standalone table (the label comes first, then the prefix)
Actuals_2026:
NOCONCATENATE
LOAD DepartmentID, Month, TargetAmount FROM [lib://Data/Actuals.qvd] (qvd);
With the NOCONCATENATE prefix, Qlik creates two distinct tables. Because they still share DepartmentID, Month, and TargetAmount, Qlik then links them through a synthetic key, so in a real model either rename the non-key fields (TargetAmount AS ActualAmount) or deliberately concatenate both into one fact table with a FactType flag (Section 3.2).
5. Comprehensive Comparison: JOIN vs. KEEP vs. CONCATENATE
| Architectural Attribute | JOIN | KEEP | CONCATENATE |
|---|---|---|---|
| Resulting Table Count | Exactly 1 (secondary table is absorbed) | Exactly 2 (both tables remain independent) | Exactly 1 (incoming rows append to target) |
| Merge Orientation | Horizontal (appends columns) | Associative link (no physical consolidation) | Vertical (appends rows) |
| Key Association Required | Yes (merges on all common field names) | Yes (filters on all common field names) | No (merges schema; unmapped columns receive null) |
| Cartesian Multiplication Risk | Severe: Multiplies rows if keys are non-unique | None: Filters rows; never duplicates records | None: Appends rows; never multiplies existing records |
| Impact on Unmatched Rows | Depends on prefix (LEFT, RIGHT, INNER, OUTER) | Depends on prefix (LEFT, RIGHT, INNER) | All rows from both datasets are preserved |
| Primary Architectural Use Case | Denormalizing snowflake dimensions into flat tables | Filtering related tables while preserving grain | Unifying multi-source transaction logs or time periods |
6. Enterprise Scenario: Merging Multi-Source Sales and Claims Data
The Problem
A national health insurance provider extracts two massive transactional datasets: MedicalClaims (25 million rows) and PharmacyClaims (40 million rows). Additionally, they maintain a MemberRegistry (2 million members) and a FraudInvestigationQueue (15,000 members flagged for auditing).
The business requirements specify:
- Medical and pharmacy claims must be analyzed together across time in unified KPI dashboard charts.
- The fraud analytics team needs an isolated audit application that focuses exclusively on claims generated by members who appear in the
FraudInvestigationQueue. - The member demographic attributes (
RiskTier,Region) must be attached to every claim row, but inactive members who made no claims must not be added to the transaction tables.
The Architectural Implementation
// Step 1: Load the Fraud Investigation Target Members
Fraud_Queue:
LOAD
MemberID,
InvestigationStatus,
InvestigatorAssigned
FROM [lib://QVD/Fraud_Registry.qvd] (qvd);
// Step 2: Use INNER KEEP to restrict claims to audited members without merging tables
Fact_MedicalClaims:
INNER KEEP (Fraud_Queue)
LOAD
ClaimID,
MemberID,
ClaimDate,
ProviderID,
ClaimAmount,
'Medical' as ClaimType
FROM [lib://QVD/Medical_Claims.qvd] (qvd);
// Step 3: Explicitly concatenate Pharmacy Claims into the unified claims table
CONCATENATE (Fact_MedicalClaims)
LOAD
PharmacyClaimID as ClaimID,
MemberID,
DispenseDate as ClaimDate,
PharmacyID as ProviderID,
PrescriptionCost as ClaimAmount,
'Pharmacy' as ClaimType
FROM [lib://QVD/Pharmacy_Claims.qvd] (qvd)
WHERE Exists(MemberID); // Prune to members already known in memory
// Step 4: Left join member attributes into the unified fact table
LEFT JOIN (Fact_MedicalClaims)
LOAD
MemberID,
MemberAge,
RiskTier,
MemberRegion
FROM [lib://QVD/Dim_Members.qvd] (qvd);
// Rename table to reflect unified architecture
RENAME TABLE Fact_MedicalClaims TO Fact_UnifiedClaims;
Architectural Evaluation
INNER KEEP (Fraud_Queue)filtered the massive 25-million medical claims down to only the 15,000 audited members without collapsingFraud_Queueinto the claims table, allowing the fraud team to independently filter onInvestigatorAssigned.CONCATENATE (Fact_MedicalClaims)unified medical and pharmacy events into a single normalized fact table with standardized column names (ClaimID,ClaimDate,ClaimAmount).LEFT JOIN (Fact_UnifiedClaims)enriched each claim with member demographics without duplicating claims, becauseMemberIDis guaranteed unique inDim_Members.
7. Common Exam Pitfalls & Traps
Warning
Pitfall 1: Omitting the Target Table Name in Joins
Writing LEFT JOIN LOAD ... without designating (TargetTable). The QIX engine automatically joins the data into the immediately preceding table loaded in script. If a developer inserts a temporary calendar or lookup table above the join, the join attaches to the wrong table, causing silent data corruption.
Warning
Pitfall 2: Silent Implicit Concatenation Swallowing Staging Tables
Loading two tables that have identical column names and counts without using NOCONCATENATE. The second table is invisibly merged into the first, leaving no trace in the data model viewer and breaking any subsequent scripts that reference the second table by name.
Warning
Pitfall 3: The Many-to-Many Cartesian Join Disaster
Performing a LEFT JOIN where the secondary table contains non-unique keys. For every duplicate key in the secondary table, Qlik clones the primary fact row, artificially multiplying revenue, inventory, or claim metrics. Always ensure the secondary join table is deduplicated on the join key.
Warning
Pitfall 4: Confusing INNER KEEP with INNER JOIN
Assuming that INNER KEEP produces a single merged table. An INNER KEEP filters both tables to their mutual intersection but keeps both tables physically separate in the data model. If your intention was to combine attributes into a single table, INNER JOIN is required.
A data architect loads two tables in Qlik Sense script: Orders (containing OrderID, CustomerID, OrderAmount) and Refunds (containing OrderID, ReturnReason). The architect executes an INNER KEEP (Orders) statement when loading Refunds. What is the resulting physical structure in the Data Model Viewer?
A single unified table named Orders containing OrderID, CustomerID, OrderAmount, and ReturnReason, retaining only orders that had refunds.
A single synthetic table named $Syn containing OrderID, linked to two unassociated dimension islands.
Two distinct tables (Orders and Refunds), but Refunds is dropped from memory and its fields are converted to variables.
Two distinct physical tables (Orders and Refunds) linked associatively by OrderID, where both tables have been filtered to contain only the OrderID records common to both.
A developer writes a script to load two separate monthly target tables: Q1_Targets and Q2_Targets. Both source files contain exactly four columns: SalesRepID, TargetMonth, RevenueQuota, and MarginQuota. When inspecting the Data Model Viewer after reload, Q2_Targets does not exist, and Q1_Targets contains twice the expected number of rows. What caused this behavior, and how should it be corrected?
The QIX associative engine automatically executed implicit concatenation because both tables possessed identical field names and counts; the developer must prepend NOCONCATENATE before the Q2_Targets load statement.
The QIX engine created an automatic outer join on SalesRepID; the developer must rename the fields using QUALIFY * to force distinct tables.
The file path for Q2_Targets was invalid, causing the QIX engine to duplicate the Q1_Targets buffer to prevent reload termination.
A synthetic key was generated between the four identical fields, collapsing the tables into a single relational entity.
An enterprise financial dashboard displays total sales revenue of $142 million. The data architect adds a LEFT JOIN to Fact_Sales to attach a ProductCategory description from Dim_Products based on ProductSKU. After reloading, the total sales revenue reported on the dashboard surges to $198 million, even though no new sales transactions occurred. What is the root cause of this discrepancy?
The ProductCategory field in Dim_Products contained NULL values, forcing Qlik to replace them with zero-value bit pointers in the data table.
The Dim_Products table contained duplicate rows for certain ProductSKU keys, causing a Cartesian row multiplication in Fact_Sales during the LEFT JOIN.
The QIX associative engine automatically converted additive currency measures into non-additive dual strings during the join.
The architect omitted the NOCONCATENATE prefix before the LEFT JOIN, causing implicit concatenation of products into sales.
Sections you finish are checked off in the contents.