4.2 Composite Keys, Cardinality & AutoNumber Memory Optimization

Key Takeaways

  • Composite keys are constructed by concatenating multiple dimensional attributes (FieldA & '|' & FieldB) to establish unique relationship linkages in multi-fact and link table architectures.

  • Delimiters (such as '|' or '~') are mandatory when concatenating string composite keys to prevent value collision where distinct attribute combinations accidentally produce identical concatenated strings.

  • String composite keys impose severe memory penalties by creating multi-gigabyte Symbol Tables of long text strings and widening Data Table bit-stuffed pointers across tens of millions of records.

  • The AutoNumber() function replaces arbitrary string composite keys with compact sequential integers (1, 2, 3...) at reload time, virtually eliminating symbol table memory and minimizing pointer bit-widths.

  • In incremental reload and multi-tier QVD pipelines, AutoNumber() integer stability must be preserved across reloads by persisting master key mapping QVDs or utilizing deterministic hashing functions.

Last updated: October 2026

Composite Keys, Cardinality & AutoNumber Memory Optimization

Quick Summary: In enterprise Qlik Sense architectures, linking tables with multi-attribute relationships—such as connecting multi-grain fact tables through a Link Table or resolving synthetic keys—requires creating composite keys. However, naive string concatenation (FieldA & '|' & FieldB) generates long, high-cardinality string keys that severely inflate Symbol Table RAM and widen Data Table bit pointers. The AutoNumber() function and its cryptographic hash variants (AutoNumberHash128(), AutoNumberHash256()) solve this bottleneck by replacing sprawling string keys with compact, 1-based sequential integers, sharply reducing symbol-table memory.


1. Composite Keys in Qlik Sense Data Models

A composite key (or compound key) is a surrogate identifier constructed by combining two or more distinct data fields into a single linkage field. In relational database design, composite primary and foreign keys are resolved through multi-column join clauses (ON a.Col1 = b.Col1 AND a.Col2 = b.Col2). However, the QIX associative engine operates under a strict relational law: tables associate if and only if they share an identically named field.

When two tables share multiple identical field names, Qlik does not execute a multi-column join; instead, it automatically spawns a Synthetic Key Table ($Syn). To prevent synthetic keys while preserving the multi-attribute relationship, the data architect must combine the shared attributes into a single composite key field.

The Mandatory Delimiter Rule

When concatenating fields to form a composite key, architects must always include an explicit delimiter that cannot appear within the underlying data values:

// DANGEROUS ANTI-PATTERN: No delimiter causes accidental key collision
CustomerID & ProductID as %CompositeKey

// Examples of Collision:
// CustomerID = '12'  and ProductID = '345'  --> '12345'
// CustomerID = '123' and ProductID = '45'   --> '12345' (COLLISION! Corrupts associative link)

// ENTERPRISE BEST PRACTICE: Explicit pipe or tilde delimiter guarantees uniqueness
CustomerID & '|' & ProductID as %CompositeKey

// Result:
// '12|345'  <> '123|45' (Distinct keys preserved)

2. The Severe Memory Penalty of Concatenated String Keys

While string concatenation (FieldA & '|' & FieldB & '|' & FieldC) solves relational ambiguity, it creates a massive performance and memory bottleneck in large enterprise datasets.

The Anatomy of String Key Bloat

Consider a supply chain data model containing 25,000,000 shipment fact rows and a shared Link Table connecting Shipments, Orders, and Invoices across three attributes: CompanyCode (4 chars), PlantID (8 chars), and DocumentNumber (18 chars).

String Composite Key Construction:
CompanyCode & '|' & PlantID & '|' & DocumentNumber as %ShipmentLinkKey
Example Key Value: "US10|PLANT-EAST-04|DOC-2026-9874102948"
Average String Length: 36 characters (36 bytes in UTF-8)

Assume there are 18,000,000 distinct combinations across the dataset:

  1. Symbol Table Footprint:
    • Unique symbols (NN): 18,000,00018,000,000.
    • String data: 18,000,000×36 bytes≈648 MB18,000,000 \times 36\text{ bytes} \approx 648\text{ MB}.
    • Symbol Table overhead (pointers, hash slots, tree nodes): 18,000,000×32 bytes≈576 MB18,000,000 \times 32\text{ bytes} \approx 576\text{ MB}.
    • Total Symbol Table RAM: ≈1.22 GB\approx 1.22\text{ GB} just to store the dictionary of string keys!
  2. Data Table Pointer Array Footprint:
    • Distinct cardinality (NN): 18,000,00018,000,000.
    • Pointer bit-width (WW): ceil(log⁡2(18,000,000))=25 bits\text{ceil}(\log_2(18,000,000)) = \mathbf{25\text{ bits}} per cell (225=33,554,4322^{25} = 33,554,432).
    • Fact Table Pointers: 25,000,000×25 bits≈78.1 MB25,000,000 \times 25\text{ bits} \approx 78.1\text{ MB}.
    • Link Table Pointers: 18,000,000×25 bits≈56.3 MB18,000,000 \times 25\text{ bits} \approx 56.3\text{ MB}.
    • Total Pointer RAM: ≈134.4 MB\approx 134.4\text{ MB}.
  3. Associative Join Hash Tables:
    • When a user filters by plant or company, the QIX engine must evaluate associative relationships across this 36-byte string key. String hashing, cache misses, and memory bus transfers significantly degrade CPU responsiveness.

3. The AutoNumber() Function: Engine Mechanics & Compression

The AutoNumber() function is the primary tool for optimizing composite keys in Qlik Sense. It evaluates an arbitrary string expression and replaces it with a compact, 1-based sequential integer (1,2,3,…1, 2, 3, \dots) on the fly during script execution.

Syntax

AutoNumber(Expression [, AutoNumberId])

Where:

  • Expression: The string or concatenated expression to convert into an integer.
  • AutoNumberId (Optional): A distinct integer or string identifier representing an isolated lookup namespace. If omitted, the default namespace is used.

How AutoNumber() Operates Internally

During reload, Qlik maintains a dynamic in-memory hash table for each AutoNumberId namespace:

  1. When AutoNumber() encounters a value for the first time (e.g., 'US10|PLANT-EAST-04|DOC-001'), it inserts the string into its temporary lookup table, assigns it the next integer in the sequence (1), and returns 1.
  2. When it encounters a previously seen string value, it immediately retrieves the existing assigned integer (1) and returns it.
  3. At the conclusion of the reload script, the temporary hash table is completely purged from RAM. Only the resulting integer keys remain in the data model.
Raw Concatenated String                   AutoNumber() Integer Key
'US10|PLANT-01|DOC-901'          ──►      1
'US10|PLANT-02|DOC-902'          ──►      2
'US10|PLANT-01|DOC-901' (Seen)   ──►      1 (Reused)
'CA20|PLANT-05|DOC-903'          ──►      3

The Memory Revolution of Integer Keys

Replacing the 36-byte string composite key with AutoNumber() transforms the memory architecture:

  • Symbol Table Transformation: Instead of storing 18 million 36-byte string literals, the Symbol Table stores a dense sequence of 32-bit integers (11 to 18,000,00018,000,000). The QIX engine applies specialized bit-packing optimizations for dense sequential integers, shrinking symbol table overhead from 1.22 GB1.22\text{ GB} to under 75 MB75\text{ MB}.
  • Faster Comparisons: Compact integer keys are cheaper to store and compare than long string keys, which helps both reload time and selection response.

4. Cryptographic Hashing: AutoNumberHash128() and AutoNumberHash256()

Qlik also provides hash-based functions that accept the key fields as separate arguments, so you do not have to build a delimited string yourself.

The Hashing Function Family

FunctionReturn TypeScope & StabilityPrimary Use Case
AutoNumber(Expr)Sequential Integer (1,2,3…1, 2, 3\dots)Session-only (Reload sequence)Fast compression within a single script run
Hash128(A, B, ...)128-bit hash returned as a 22-character stringDeterministic across reloads and appsCross-app linkage, incremental loads, QVD storage
Hash256(A, B, ...)256-bit hash returned as a 43-character stringDeterministic across reloads and appsUltra-high cardinality (>109> 10^9 rows) collision prevention
AutoNumberHash128(A, B, ...)Sequential Integer (1,2,3…1, 2, 3\dots)Session-only (Reload sequence)Maximum speed: hashes parameters in C++ and assigns integer
AutoNumberHash256(A, B, ...)Sequential Integer (1,2,3…1, 2, 3\dots)Session-only (Reload sequence)Maximum speed on extreme multi-attribute composite keys

Passing Fields Directly with AutoNumberHash128()

Instead of writing:

// Builds a delimited string first, then numbers it
AutoNumber(CompanyCode & '|' & PlantID & '|' & DocumentNumber) as %Key

Architects write:

// Passes the key fields directly; no delimiter needed
AutoNumberHash128(CompanyCode, PlantID, DocumentNumber) as %Key

Qlik documents AutoNumberHash128() as calculating a 128-bit hash of the combined input values and returning a unique integer for each distinct hash value. The result is the same kind of compact sequential key as AutoNumber(), and like AutoNumber() it is only stable within one reload.

The AutoNumber Statement

Qlik also provides an AutoNumber script statement that converts whole fields after they have been loaded:

// Load keys as readable strings or hashes; the QVD reads can stay optimized
Fact_Sales:
LOAD %SalesKey, SalesAmount FROM [lib://QVD/Fact_Sales.qvd] (qvd);

Dim_Store:
LOAD %SalesKey, StoreName FROM [lib://QVD/Dim_Store.qvd] (qvd);

// After the final LOAD: replace every value of matching fields with an integer
AutoNumber '%*Key';

Qlik notes that the AutoNumber() function inside a LOAD limits optimized QVD loads, whereas loading the QVD first and then running the statement keeps the load optimized. Place the statement after the last LOAD that touches those fields, quote field names when you use wildcards, and remember that each field gets its own key index unless you add Using namespace.


5. Cross-Reload Key Stability in Incremental Load Pipelines

Caution

The Critical Danger of AutoNumber(): AutoNumber() integers are non-deterministic across reloads. The integer assigned to a given key value is determined strictly by the chronological order in which rows are read into memory during that specific reload run.

Consider this failure scenario:

  1. On Monday, AutoNumber('Customer-A') is the first row loaded, receiving integer key 1 in Fact_Sales.qvd.
  2. On Tuesday, an incremental reload runs. Because of new incoming transactions, AutoNumber('Customer-Z') is processed first, receiving integer key 1. Meanwhile, Customer-A is loaded fifth, receiving integer key 5.
  3. The historical QVD stores Customer-A with key 1, while the newly loaded record stores Customer-A with key 5. The associative model is now catastrophically corrupted!

Enterprise Solution: Persistent Key Mapping QVD

To safely deploy AutoNumber() across multi-tier QVD architectures and incremental load pipelines, architects must ensure key stability. Two primary enterprise patterns exist:

Pattern A: Deterministic Hash128() in QVDs, AutoNumber() in Data Mart

In this recommended pattern, incremental ETL layers store deterministic 128-bit hashes in QVD files. The presentation layer app loads all QVDs and applies AutoNumber() in a single pass:

Extract Tier (Incremental)  ──► Stores Hash128(Company, Plant, Doc) as %HashKey in QVD
                                (Stable across any reload, any day)
                                     │
                                     ▼
Data Mart Model             ──► LOAD AutoNumber(%HashKey) as %IntegerKey
                                (Runs across all data at once; keys match perfectly!)

Pattern B: Persistent Key Registry (Master Key QVD)

If the architect requires integer keys stored directly within persistent QVD files, a Master Key Registry must be maintained:

// Step 1: Load the existing Master Key Registry from disk
KeyRegistry:
LOAD
    NaturalKey,
    IntegerKey
FROM [lib://Governance/KeyRegistry.qvd] (qvd);

// Determine the current maximum integer assigned
Let vMaxKey = Peek('IntegerKey', -1, 'KeyRegistry');
If Len('$(vMaxKey)') = 0 Then
    Let vMaxKey = 0;
End If

// Step 2: Extract distinct new natural keys from today's incremental batch
NewKeys:
NoConcatenate LOAD DISTINCT
    NaturalKey
FROM [lib://Extract/TodayIncrementalTransactions.qvd] (qvd)
WHERE NOT Exists(NaturalKey);

// Step 3: Assign new sequential integers starting from vMaxKey + 1
Concatenate (KeyRegistry)
LOAD
    NaturalKey,
    RecNo() + $(vMaxKey) as IntegerKey   // RecNo() restarts at 1 for this resident source; RowNo() would continue counting the concatenated registry
RESIDENT NewKeys;

DROP TABLE NewKeys;

// Step 4: Save the updated Master Key Registry
STORE KeyRegistry INTO [lib://Governance/KeyRegistry.qvd] (qvd);

// Step 5: Map new integer keys into transactional facts using ApplyMap
Map_NaturalToInteger:
MAPPING LOAD NaturalKey, IntegerKey RESIDENT KeyRegistry;

Fact_Sales:
LOAD
    TransactionID,
    ApplyMap('Map_NaturalToInteger', NaturalKey) as %IntegerKey,
    SalesAmount
FROM [lib://Extract/TodayIncrementalTransactions.qvd] (qvd);

DROP TABLE KeyRegistry;

6. Enterprise Scenario: Global Logistics Multi-Fact Link Table

Business Problem

GlobalFreight Inc. operates a worldwide logistics platform processing 30,000,000 shipments, 25,000,000 freight invoices, and 12,000,000 customs declaration filings annually. Management requires an interactive executive dashboard where analysts can select any vessel, port of origin, or customs broker and seamlessly cross-analyze freight billing variance against transit milestone delays.

A novice developer constructed a Link Table using a raw concatenated string composite key:

VesselIMO & '|' & OriginPortCode & '|' & DestPortCode & '|' & CarrierID as %ShipmentLinkKey

Performance Symptoms:

  • Server RAM consumption exceeded 42 GB on reload.
  • Interacting with port filter panes produced a 4.5-second UI freeze as the engine resolved string pointer intersections.
  • The data model viewer showed the Symbol Table for %ShipmentLinkKey consuming 2.1 GB alone.

Architectural Remediation:

  1. Replaced the 4-attribute string concatenation with AutoNumberHash128(VesselIMO, OriginPortCode, DestPortCode, CarrierID) in all three fact tables and the Link Table, all in the same reload so the integers match.
  2. Converted the Link Table from storing raw strings to pure dense sequential integers.

Illustrative Results (this scenario):

Performance MetricBefore (String Composite Key)After (AutoNumberHash128)Improvement
Symbol Table RAM2,140 MB2,140\text{ MB}48 MB48\text{ MB}97.8% reduction
Data Table Pointer RAM285 MB285\text{ MB}160 MB160\text{ MB}43.9% reduction
Total Application RAM42.6 GB42.6\text{ GB}14.2 GB14.2\text{ GB}66.7% overall savings
Sheet Filter Response Time4,500 ms4,500\text{ ms} (laggy)380 ms380\text{ ms} (instant)11.8x faster
Reload Duration18 min 40 sec18\text{ min } 40\text{ sec}11 min 15 sec11\text{ min } 15\text{ sec}40% faster

7. String Key vs. AutoNumber Memory Comparison Table

The following matrix contrasts the architectural properties of raw string keys versus AutoNumber() integer keys across enterprise dimensions:

Architectural VectorRaw String Composite Key (A & '~' & B)AutoNumber() Sequential Integer KeyAutoNumberHash128(A, B) Key
Underlying Data TypeAlphanumeric Text (UTF-8 String)Dense 32-bit IntegerDense 32-bit Integer
Symbol Table SizeProportional to string length ×\times cardinality (Gigabytes)Minimal (Underlying values are dense integers, Megabytes)Minimal (Underlying values are dense integers, Megabytes)
Pointer Array WidthIdentical bit-depth (W=ceil(log⁡2(N))W = \text{ceil}(\log_2(N)))Identical bit-depth (W=ceil(log⁡2(N))W = \text{ceil}(\log_2(N)))Identical bit-depth (W=ceil(log⁡2(N))W = \text{ceil}(\log_2(N)))
Concatenation in ScriptRequired (delimited string)Required (delimited string)Not needed (fields passed as separate arguments)
Hardware CPU Cache EfficiencyLow (heap memory dereferencing, cache misses)High (L1/L2 cache register optimization)High (L1/L2 cache register optimization)
Deterministic Across ReloadsYes (string literal is identical every time)No (dependent on row read sequence)No (dependent on row read sequence)
Recommended Use CaseSmall datasets (<100,000< 100,000 rows)Single-load data marts, static reloadsHigh-volume multi-attribute link tables

8. Common Exam Pitfalls & Traps

Warning

Pitfall 1: Omitting Delimiters During String Key Concatenation Concatenating keys without a delimiter (FieldA & FieldB). Inevitably, distinct values such as 'A1' & '23' and 'A12' & '3' merge into the identical string 'A123', causing accidental data loss and corrupted associations.

Warning

Pitfall 2: Using AutoNumber() Directly in Incremental Reload Scripts Executing AutoNumber() on daily incremental batches stored into historical QVDs. Because AutoNumber() assigns integers based on reload order, Monday's key 1 will not match Tuesday's key 1, permanently scrambling historical relational linkages.

Warning

Pitfall 3: Confusing Hash128() with AutoNumberHash128() Expecting Hash128() to return an integer. Hash128() returns a 22-character string; it is deterministic across reloads, but it remains a string and consumes symbol-table memory. AutoNumberHash128() returns a compact 1-based integer instead.

Warning

Pitfall 4: Neglecting the AutoNumberId Parameter Using AutoNumber() for unrelated keys without the second argument (AutoNumberId). Both keys then draw from one shared counter, so neither %CustomerOrderKey nor %VendorProductKey gets its own compact 1-to-N sequence, and identical strings from the two domains receive the same integer. Qlik provides the ID argument to create separate counter instances when AutoNumber() is used on different keys.

Test Your Knowledge

A data architect is designing an incremental load architecture for a 50-million-row sales fact table. Each night, 100,000 new transactions are extracted, transformed, and concatenated into a master QVD. The architect wants to replace a 3-field string composite key with an integer surrogate key to optimize RAM. Why is using AutoNumber(StoreID & '|' & DateID & '|' & RegisterID) directly inside the nightly incremental reload script an architectural failure?

A

AutoNumber() cannot evaluate expressions containing more than two concatenated fields.

B

AutoNumber() functions only when reading directly from relational databases and throws a syntax error when executed on Resident or QVD tables.

C

AutoNumber() causes QVD files to become uncompressed, increasing disk storage by over 300%.

D

AutoNumber() assigns sequential integers dynamically based on row read order during each specific reload; incremental batches will assign conflicting integer keys to different dimension entities compared to historical QVD records.

Test Your Knowledge

An architect builds a link key with AutoNumber(Company & '|' & Facility & '|' & OrderNumber) as %LinkKey. Which alternative produces an equivalent compact integer key by passing the three fields as separate arguments, so the script no longer builds a delimited string?

A

Replace the pipe delimiter with a tab delimiter: AutoNumber(Company & chr(9) & Facility & chr(9) & OrderNumber).

B

Replace the string concatenation with the cryptographic hash function: AutoNumberHash128(Company, Facility, OrderNumber) as %LinkKey.

C

Convert the fields into uppercase strings using Upper() before concatenating.

D

Prepend the tableName to each field using the QUALIFY statement before calling AutoNumber().

Test Your Knowledge

An enterprise Qlik Sense application contains two completely independent composite keys in different data domains: %CustomerTerritoryKey and %VendorAccountKey. Both keys are generated using AutoNumber(). What is the risk if the developer writes AutoNumber(Cust & '|' & Terr) as %CustomerTerritoryKey and AutoNumber(Vend & '|' & Acct) as %VendorAccountKey without providing a second argument?

A

The QIX engine will fail to reload with a circular reference error because AutoNumber() can only be called once per load script.

B

The two fields will share the identical symbol table and automatically associate with each other in the associative data model viewer.

C

Both keys draw integers from one shared default counter: neither field gets its own compact 1-to-N sequence, and identical strings in the two domains receive the same integer. Separate AutoNumberId values give each key its own counter.

D

AutoNumber() will generate 64-bit floating-point numbers instead of integers when the namespace identifier is omitted.

Sections you finish are checked off in the contents.