Unlock a world of possibilities! Login now and discover the exclusive benefits awaiting you.
By reading the Product Innovation blog, you will learn about what's new across all of the products in our growing Qlik product portfolio.
The Support Updates blog delivers important and useful Qlik Support information about end-of-product support, new service releases, and general support topics.
This blog was created for professors and students using Qlik within academia.
Hear it from your Community Managers! The Community News blog provides updates about the Qlik Community Platform and other news and important announcements.
The Qlik Digest is your essential monthly low-down of the need-to-know product updates, events, and resources from Qlik.
The Qlik Learning blog offers information about the latest updates to our courses and programs, as well as insights from the Qlik Learning team.
In a well visited post on the community forum, John Witherspoon some time ago asked “Should We Stop Worrying and Love the Synthetic Key?” John’s post begins: “Synthetic keys have a bad reputation. The consensus seems to be that they cause performance and memory problems, and should usually or even always be removed. I believe that the consensus is wrong.” Here’s my view on this topic.
The creation of synthetic keys is simply QlikView’s way of managing composite keys. There is nothing strange or magic around it.
A single key is easy to manage: Just list all unique values in a symbol table (see Symbol Tables and Bit-Stuffed Pointers), and then link the data tables using a natural join.

But a composite key is slightly different – there is no single symbol table that contains the relevant combinations of the multiple key fields. So QlikView needs to create such a table for all combinations: the $Syn table. In a way, you can say that the $Syn table is a symbol table for composite keys. In the data tables, the multiple keys are replaced by an identifier that uniquely identifies the combination of the values of the original keys: the $Syn key.
Hence, if you have the same set of multiple keys in two or more tables, the QlikView synthetic keys create a general, correct, compact and efficient solution. Synthetic keys do not per se cause performance and memory problems. They do not use a lot more memory than if you autonumber your own concatenated key. And they treat NULLs correctly, as opposed to an explicit concatenated key.
Hence: The synthetic key is in itself good and we should all love it.

However… still, also I avoid synthetic keys. Why?
But in principle, I totally agree with John’s initial conclusion: Any problem around synthetic keys is really a data modeling problem and not a problem with the synthetic key itself.
The short answer to John’s question is Yes and No. Yes, we should love the synthetic key. But, No, we should not stop worrying. We should always be alert and ask ourselves: “Do I want this synthetic key? Is the data model OK?”
And so, because of the automated and irrevocable data-modeling process which rules out human meddling, the Synthetic Keys are scaring. But they are simple to understand. And completely credible and convincing.
Dr HIC
Further reading on Qlik data modelling:
In my previous blog posts (part 1, part 2), I explained how we can use enigma.js to communicate with the Qlik Associative Engine and get access to data. We also went through the concept of Generic Objects and saw how they can be used to do many things including getting raw data and using it to build visualizations.
In this post, we are going to expand on that and take a look at a real world example where enigma.js can be used to get Master Measures data that is rendered as KPIs in a web app, and monitor them for any changes to reflect the latest values.
This post is based on the following tutorial on qlik.dev where you can find the boilerplate code and more resources to help you get started: https://qlik.dev/embed/control-the-experience/dimensions-and-measures/get-master-measures/
You will find the full example code attached at the end of the post, I recommend you download it and open it in your favorite text editor as I will only feature some parts of it to keep this post short.
1- First, let’s take a look at the index.html:
const TENANT = '<INSERT YOUR TENANT HERE (example: xxxx.us.qlikcloud.com)>';
const WEB_INTEGRATION_ID = '<INSERT WEB INTEGRATION ID HERE>';
const APP_ID = '<INSERT APP ID HERE>';
const MASTER_MEASURE_NAMES = ['# of Invoices', 'Average Sales per Invoice', 'Sales (LYTD)', 'Sales LY'];
const IDENTITY = '1234';
(async function main() {
const isLoggedIn = await qlikLogin();
const qcsHeaders = await getQCSHeaders();
const [session, enigmaApp] = await getEnigmaSessionAndApp(qcsHeaders, APP_ID, IDENTITY);
handleDisconnect(session);
const allMasterMeasuresList = await getMeasureList(enigmaApp);
const masterMeasureValuesDct = await masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, MASTER_MEASURE_NAMES);
})();
2- Now, let’s take a look at the different functions that make this happen:
// LOGIN
async function qlikLogin() {
const loggedIn = await fetch(`https://${TENANT}/api/v1/users/me`, {
mode: 'cors',
credentials: 'include',
headers: {
'qlik-web-integration-id': WEB_INTEGRATION_ID,
},
})
if (loggedIn.status !== 200) {
if (sessionStorage.getItem('tryQlikAuth') === null) {
sessionStorage.setItem('tryQlikAuth', 1);
window.location = `https://${TENANT}/login?qlik-web-integration-id=${WEB_INTEGRATION_ID}&returnto=${location.href}`;
return await new Promise(resolve => setTimeout(resolve, 10000)); // prevents further code execution
} else {
sessionStorage.removeItem('tryQlikAuth');
const message = 'Third-party cookies are not enabled in your browser settings and/or browser mode.';
alert(message);
throw new Error(message);
}
}
sessionStorage.removeItem('tryQlikAuth');
console.log('Logged in!');
return true;
}
// QCS HEADERS
async function getQCSHeaders() {
const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
mode: 'cors',
credentials: 'include',
headers: {
'qlik-web-integration-id': WEB_INTEGRATION_ID
},
})
const csrfToken = new Map(response.headers).get('qlik-csrf-token');
return {
'qlik-web-integration-id': WEB_INTEGRATION_ID,
'qlik-csrf-token': csrfToken,
};
}
// ENIGMA ENGINE CONNECTION
async function getEnigmaSessionAndApp(qcsHeaders, appId, identity) {
const params = Object.keys(qcsHeaders)
.map((key) => `${key}=${qcsHeaders[key]}`)
.join('&');
return (async () => {
const schema = await (await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')).json();
try {
return await createEnigmaAppSession(schema, appId, identity, params);
}
catch {
const waitSecond = await new Promise(resolve => setTimeout(resolve, 1500));
try {
return await createEnigmaAppSession(schema, appId, identity, params);
}
catch (e) {
throw new Error(e);
}
}
})();
}
async function createEnigmaAppSession(schema, appId, identity, params) {
const session = enigma.create({
schema,
url: `wss://${TENANT}/app/${appId}/identity/${identity}?${params}`
});
const enigmaGlobal = await session.open();
const enigmaApp = await enigmaGlobal.openDoc(appId);
return [session, enigmaApp];
}
// GET LIST OF ALL MASTER MEASURES
async function getMeasureList(enigmaApp) {
const measureListProp = {
"qInfo": {
"qType": "MeasureList",
"qId": ""
},
"qMeasureListDef": {
"qType": "measure",
"qData": {
"title": "/qMetaDef/title",
"tags": "/qMetaDef/tags"
}
}
}
const measureListObj = await enigmaApp.createSessionObject(measureListProp);
const measureList = await measureListObj.getLayout();
return measureList.qMeasureList.qItems;
}
// CREATE HYPERCUBE WITH MULTIPLE MASTER MEASURES (INCLUDE MATCHING NAMES ONLY)
async function masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, desiredMasterMeasureNamesList) {
let matchingMeasures = [];
allMasterMeasuresList.forEach(measureObject => {
if (desiredMasterMeasureNamesList.includes(measureObject.qMeta.title)) {
matchingMeasures.push({
"qLibraryId": measureObject.qInfo.qId
})
}
});
if (!matchingMeasures.length > 0) {
console.log('No matching master measures found! Exiting...');
return
}
const measureDef = {
"qInfo": {
"qType": 'hypercube',
},
"qHyperCubeDef": {
"qDimensions": [],
"qMeasures": matchingMeasures,
"qInitialDataFetch": [
{
"qHeight": 1,
"qWidth": matchingMeasures.length,
},
],
},
};
const measureObj = await enigmaApp.createSessionObject(measureDef);
const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;
// LISTEN FOR CHANGES AND GET UPDATED LAYOUT
measureObj.on('changed', async () => {
const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;
processAndPlotMeasureHypercube(measureObjHypercube);
})
processAndPlotMeasureHypercube(measureObjHypercube);
}
// HELPER FUNCTION TO PROCESS HYPERCUBE INTO USER FRIENDLY DICTIONARY
function processAndPlotMeasureHypercube(hypercube) {
const masterMeasureValuesDict = Object.create(null);
hypercube.qMeasureInfo.forEach((measure, i) => {
masterMeasureValuesDict[measure.qFallbackTitle] = hypercube.qDataPages[0].qMatrix[0][i].qText;
});
const masterMeasureKeys = Object.keys(masterMeasureValuesDict);
masterMeasureKeys.sort();
const sortedMasterMeasureValuesDict = Object.create(null);
masterMeasureKeys.forEach(name => {
sortedMasterMeasureValuesDict[name] = masterMeasureValuesDict[name];
})
renderKpis(sortedMasterMeasureValuesDict);
}
// RENDER KPIs
function renderKpis(masterMeasureValuesDict) {
let kpiData = [];
Object.entries(masterMeasureValuesDict).forEach(([key, value]) => {
kpiData.push({
label: key,
value: Number(value).toLocaleString()
});
});
const kpisContainer = document.querySelector('#kpis');
kpisContainer.innerHTML = '';
kpiData.forEach((kpi) => {
const kpiCard = document.createElement('div');
kpiCard.classList.add('kpi-card');
const labelElement = document.createElement('div');
labelElement.classList.add('kpi-label');
labelElement.innerText = kpi.label;
const valueElement = document.createElement('div');
valueElement.classList.add('kpi-value');
valueElement.innerText = kpi.value;
kpiCard.appendChild(labelElement);
kpiCard.appendChild(valueElement);
kpisContainer.appendChild(kpiCard);
});
}
This is how the KPIs are rendered to the page:
To show how the on-change event listener works, let's simulate a change by editing the # of Invoices Master Measure in Qlik:
Looking back the web page, the change is instantly reflected on the KPI:
That’s all, I hope you found this post helpful, do not forget to check out more tutorials on qlik.dev that cover other important use cases!
P.S: to make it easier to run the web app, I have included a server.py file to easily serve the files via https://localhost:8000. You can run it with the command: python server.py.
Also, don’t forget to whitelist this localhost domain when generating a new Web Integration ID:
Thanks for reading!
Welcome to this ongoing video series focused on our strategy, capabilities and direction for AI and ML. During this series, we describe our unique approach to AI and ML, demonstrate existing and new capabilities for Augmented Analytics in Qlik Sense, AutoML, and Predictive Analytics, and offer a look at our future direction and what’s coming next.
In this second video, we cover Automated Insights and natural language interaction through Insight Advisor in Qlik Sense.
Hierarchies are very common in all database and business intelligence solutions. Usually they are balanced and with a fix number of levels, and then they do not pose any problems. Just load the data, add a drill-down group, and you’re done.

But there is one type of hierarchy that is somewhat tricky to get right – an unbalanced, n-level hierarchy. Typical for this type of hierarchy is that the levels are not named, and you really don’t know on which level you need to search for a specific node.
Usually such a hierarchy is stored in an Adjacent Nodes table, i.e. a table that has one record per node and each node has a reference to its parent.
Such a table can be loaded into QlikView directly using the Hierarchy prefix. This prefix will transform the Adjacent Nodes table into an Expanded Nodes table that has additional fields that you can use in your app.

With the fields in this table, you can easily create a pivot table and a tree-view list box. Below you can see some wine districts displayed in both these object types:

One challenge with hierarchies is that you can refer to a node in two different ways: Either to the node including the entire sub-tree, or to the node only, excluding all sub-nodes. In the example with the wine districts, it would mean any wine from Bordeaux, and unspecified Bordeaux, respectively. In the pivot table above, the difference is obvious: Any wine from Bordeaux sums up to 150 units, and the unspecified Bordeaux sums up to 18 units.
A user usually wants to make selections referring to the entire sub-tree, but the above solution does not have any field for this. To create such a field, you need the second hierarchy-resolving prefix – the HierarchyBelongsTo.
This prefix will also transform the hierarchy table. The result will be a table containing one record per descendant-ancestor pair. In other words, the ancestor (tree ID) will link to all its descendants (node ID), and can thus be used to make selections of entire sub-trees. (The “TreeBridge” table in the picture below.)
But it doesn’t stop here… The above solution creates one field in which tree searches can be made, but in order to create a drill-down for trees, you need an additional table – an expanded nodes table for the trees. This can be created with a second Hierarchy statement, but now one that links to the tree ID instead of the node ID. (The “Trees” table in the picture below.)

The data model with the three hierarchy tables is the one I recommend: It generates all fields you need.
A more elaborate explanation with script examples can be found in the technical brief about Hierarchies.
Further reading related to this topic:
今年 4月、毎年恒例の最大級のデータイベント「QlikWorld 2023」が、米国ラスベガスで開催されました。
特別ゲストおよび Qlik エグゼクティブによる基調講演をはじめ、100 以上のセッションや実践的なワークショップ、グローバルトランスフォーメーションアワード授与式など、データ愛好家が一堂に会した 4 日間は、連日盛況のうちに終了しました。
本 Web セミナーでは、Qlik の日本の営業および技術者が、QlikWorld 2023 でご紹介した内容をわかりやすくまとめてお届けします。Qlik のビジョンや製品ポートフォリオ、最新のユーザー事例、Qlik で先進的なビジネス成果を挙げている企業に授与されるグローバルトランスフォーメーションアワードのほか、実際に参加した営業担当者が現地でのイベントの様子をご紹介します。ぜひ、データ活用のすべてを網羅した「QlikWorld 2023」をダイジェストでご視聴ください。
※ 参加費無料。パソコン・タブレット・スマートフォンで、どこからでもご視聴いただけます。日本語字幕付きでお届けします。

Investigate how the national football/soccer teams of all 32 participating countries in the 2022 Qatar World Cup are made up of players from various leagues and clubs, using remarkable Decomposition Tree visualizations in Qlik Sense. (Re)arrange the order of the represented dimensions depending on how you want to disaggregate the totals. Analyze goals and caps for national teams similarly using intuitive decomposition trees.

Check out how AnyChart's Decomposition Tree works and how it can help you explore data across multiple dimensions.

Anyone who wants to analyze the 2022 Qatar World Cup's teams and see an example of a decomposition tree in Qlik Sense in action.

This application features decomposition trees built with AnyChart's Decomposition Tree extension for Qlik Sense.
🔗 >> VIEW IT LIVE OR DOWNLOAD (.QVF) <<