DWH Interview Question 26 - What is a junk dimension?

Data Warehouse Interview Q&A · Question 26

In this post we are going to cover the junk dimension: a small dimension that collects the little flags and indicators that don't belong anywhere else.

Every order in a shop arrives with a handful of small flags: was it online or in store, how was it paid, is it a gift, was a coupon used? Each flag has only two or three possible values. A junk dimension combines them into one table with one row per combination, and the fact table stores a single key that points to that combination.

We will look at the problem these flags cause, build a junk dimension from staging data, load the fact with one key, and then analyze sales by the flags.

Question 26 of the Real DWH Question and Answers series on TechBrothersIT.

The interview answer

Say this first

A junk dimension combines several low-cardinality flags and indicators, like channel, payment method, gift and coupon flags, into one small dimension that stores each combination once. The fact table then holds one junk key instead of many flag columns or many tiny dimensions.

The problem: too many little flags

There are three ways to store these flags, and the first two both have problems.

  • Option 1: text flags on every fact row. With billions of rows, that is a lot of repeated text, and the fact table gets wide and messy.
  • Option 2: four tiny dimensions. Each has two or three rows, and the fact table gets four extra keys. The model fills up with tables nobody wants to see.
  • Option 3: one junk dimension. One small table of flag combinations, and one key on the fact.

The junk dimension is cleaner than both, so let's build one.

Let's create some sample data

We need three tables: stg_orders, where orders arrive with their four flags, an empty junk dimension dim_order_profile, and an empty fact_orders.

Sample data — run this first
DROP TABLE IF EXISTS fact_orders;
DROP TABLE IF EXISTS dim_order_profile;
DROP TABLE IF EXISTS stg_orders;

-- staging: each order arrives with four low-cardinality flags
CREATE TABLE stg_orders (
  order_number VARCHAR(10),
  amount       DECIMAL(10,2),
  channel      VARCHAR(10),   -- Online / Store
  payment      VARCHAR(10),   -- Card / Cash / PayPal
  is_gift      CHAR(1),       -- Y / N
  coupon_used  CHAR(1)        -- Y / N
);
INSERT INTO stg_orders VALUES
  ('SO-1', 53.00, 'Online', 'Card',   'N', 'N'),
  ('SO-2', 25.00, 'Store',  'Cash',   'N', 'N'),
  ('SO-3', 74.00, 'Online', 'Card',   'Y', 'N'),
  ('SO-4', 30.00, 'Online', 'PayPal', 'N', 'Y'),
  ('SO-5', 27.00, 'Store',  'Card',   'N', 'N'),
  ('SO-6', 40.00, 'Online', 'Card',   'N', 'N');

-- the junk dimension: one row per COMBINATION of flags
CREATE TABLE dim_order_profile (
  profile_key INT PRIMARY KEY,
  channel     VARCHAR(10),
  payment     VARCHAR(10),
  is_gift     CHAR(1),
  coupon_used CHAR(1)
);
CREATE TABLE fact_orders (
  order_number VARCHAR(10),
  profile_key  INT REFERENCES dim_order_profile(profile_key),
  amount       DECIMAL(10,2)
);

Let's look at the staging data.

Query
SELECT * FROM stg_orders ORDER BY 1;
Result6 rows · 6 columns
order_numberamountchannelpaymentis_giftcoupon_used
SO-153.00OnlineCardNN
SO-225.00StoreCashNN
SO-374.00OnlineCardYN
SO-430.00OnlinePayPalNY
SO-527.00StoreCardNN
SO-640.00OnlineCardNN

Six orders, each with a channel, a payment method, a gift flag and a coupon flag. Notice that SO-1 and SO-6 have exactly the same flags: Online, Card, N, N.

Step 1: Build the junk dimension

Now let's take the distinct combinations of the four flags from staging and give each combination a surrogate key with ROW_NUMBER().

Statement
INSERT INTO dim_order_profile
SELECT ROW_NUMBER() OVER (ORDER BY channel, payment,
                          is_gift, coupon_used),
       channel, payment, is_gift, coupon_used
FROM (SELECT DISTINCT channel, payment, is_gift, coupon_used
      FROM stg_orders) d;

Now let's check the result.

Query
SELECT * FROM dim_order_profile ORDER BY profile_key;
Result5 rows · 5 columns
profile_keychannelpaymentis_giftcoupon_used
1OnlineCardNN
2OnlineCardYN
3OnlinePayPalNY
4StoreCardNN
5StoreCashNN

Six orders, but only five different combinations. Online, Card, no gift, no coupon appears twice in staging, but it is stored once, as profile 1. That is the whole idea: a small table of profiles. Even with millions of orders, the number of real combinations stays small.

Step 2: Load the fact with one key

Now let's load the fact table. Each order looks up its combination by matching all four flags, and stores just the profile key.

Statement
INSERT INTO fact_orders
SELECT s.order_number, p.profile_key, s.amount
FROM stg_orders s
JOIN dim_order_profile p
  ON  p.channel = s.channel AND p.payment = s.payment
  AND p.is_gift = s.is_gift AND p.coupon_used = s.coupon_used;

Now let's check the result.

Query
SELECT * FROM fact_orders ORDER BY order_number;
Result6 rows · 3 columns
order_numberprofile_keyamount
SO-1153.00
SO-2525.00
SO-3274.00
SO-4330.00
SO-5427.00
SO-6140.00

Four flag columns became one small integer. SO-1 and SO-6 both point to profile 1, because they share the same flags. SO-2 points to profile 5, Store and Cash.

Step 3: Analyze by any flag

Now let's use it like any other dimension. We want sales by payment method and gift flag, so we join the fact to dim_order_profile and group by its columns.

Query
SELECT p.payment, p.is_gift, SUM(f.amount) AS sales
FROM fact_orders f
JOIN dim_order_profile p
  ON p.profile_key = f.profile_key
GROUP BY p.payment, p.is_gift
ORDER BY p.payment, p.is_gift;
Result4 rows · 3 columns
paymentis_giftsales
CardN120.00
CardY74.00
CashN25.00
PayPalN30.00

Card orders that were not gifts add up to 120.00, Card gift orders to 74.00, Cash to 25.00 and PayPal to 30.00. The flags behave exactly like normal dimension attributes.

Design choices

A few design choices come up in interviews.

  • Observed or all combinations? We stored only the combinations we have actually seen. You can also generate every possible combination up front with a CROSS JOIN. Here that would be 2 × 3 × 2 × 2 = 24 rows. Both work, and pre-generating means the load never has to insert new profiles.
  • Only small, unrelated flags. Anything with many values, or anything that describes the customer or the product, belongs in that dimension instead.
  • A meaningful name. Call it dim_order_profile, not dim_junk. Nobody wants to explain a table called junk to the business.

Junk vs degenerate vs mini-dimension

These three dimension types are easy to mix up, so here they are side by side.

TypeWhat it holdsHas a table?
DegenerateA single operational ID (order number)No, lives on the fact
JunkSmall, unrelated flags, combinedYes, small
MiniFast-changing attributes split from a big dimensionYes, small

A degenerate dimension is a single ID with no table. A junk dimension is a table of small, unrelated flags combined into profiles. A mini-dimension is a small table of fast-changing attributes split off from a big dimension, like a customer's age band and income band, and it gets its own post later in this series.

Follow-up questions interviewers ask

Junk vs degenerate dimension?

Degenerate: a single operational ID on the fact with no table. Junk: a small table of combined, unrelated low-cardinality flags.

Store observed or all combinations?

Either; pre-generating all combinations with a CROSS JOIN means the load never inserts new profiles.

What should not go in a junk dimension?

High-cardinality attributes, or attributes that describe an existing dimension (customer, product).

The full script

Here is everything from this post in one script. Copy it and run it top to bottom.

Full script — copy and run top to bottom
-- ========================================================================
-- Data Warehouse Interview Q&A #26 - What is a junk dimension?
-- TechBrothersIT. Sample data is invented for teaching.
-- Standard SQL, tested on PostgreSQL 16. Runs top to bottom in one go.
-- Run it in an empty database or schema: other videos reuse table names
-- like dim_customer, and their leftover tables can block the DROPs below.
-- PostgreSQL example:  CREATE SCHEMA dwh_q26;  SET search_path TO dwh_q26;
-- ========================================================================

-- Step 0: create and load the sample data
DROP TABLE IF EXISTS fact_orders;
DROP TABLE IF EXISTS dim_order_profile;
DROP TABLE IF EXISTS stg_orders;

-- staging: each order arrives with four low-cardinality flags
CREATE TABLE stg_orders (
  order_number VARCHAR(10),
  amount       DECIMAL(10,2),
  channel      VARCHAR(10),   -- Online / Store
  payment      VARCHAR(10),   -- Card / Cash / PayPal
  is_gift      CHAR(1),       -- Y / N
  coupon_used  CHAR(1)        -- Y / N
);
INSERT INTO stg_orders VALUES
  ('SO-1', 53.00, 'Online', 'Card',   'N', 'N'),
  ('SO-2', 25.00, 'Store',  'Cash',   'N', 'N'),
  ('SO-3', 74.00, 'Online', 'Card',   'Y', 'N'),
  ('SO-4', 30.00, 'Online', 'PayPal', 'N', 'Y'),
  ('SO-5', 27.00, 'Store',  'Card',   'N', 'N'),
  ('SO-6', 40.00, 'Online', 'Card',   'N', 'N');

-- the junk dimension: one row per COMBINATION of flags
CREATE TABLE dim_order_profile (
  profile_key INT PRIMARY KEY,
  channel     VARCHAR(10),
  payment     VARCHAR(10),
  is_gift     CHAR(1),
  coupon_used CHAR(1)
);
CREATE TABLE fact_orders (
  order_number VARCHAR(10),
  profile_key  INT REFERENCES dim_order_profile(profile_key),
  amount       DECIMAL(10,2)
);


-- Step 1: Build the junk dimension from the distinct combinations
INSERT INTO dim_order_profile
SELECT ROW_NUMBER() OVER (ORDER BY channel, payment,
                          is_gift, coupon_used),
       channel, payment, is_gift, coupon_used
FROM (SELECT DISTINCT channel, payment, is_gift, coupon_used
      FROM stg_orders) d;

-- check the result
SELECT * FROM dim_order_profile ORDER BY profile_key;

-- Step 2: Load the fact with ONE junk key instead of four flag columns
INSERT INTO fact_orders
SELECT s.order_number, p.profile_key, s.amount
FROM stg_orders s
JOIN dim_order_profile p
  ON  p.channel = s.channel AND p.payment = s.payment
  AND p.is_gift = s.is_gift AND p.coupon_used = s.coupon_used;

-- check the result
SELECT * FROM fact_orders ORDER BY order_number;

-- Step 3: Analyze by any flag through the junk dimension
SELECT p.payment, p.is_gift, SUM(f.amount) AS sales
FROM fact_orders f
JOIN dim_order_profile p
  ON p.profile_key = f.profile_key
GROUP BY p.payment, p.is_gift
ORDER BY p.payment, p.is_gift;

Key points

  • Combines low-cardinality flags into one small dimension
  • One row per combination, with a surrogate key
  • The fact stores one key instead of many flags
  • Observed combinations, or all of them via CROSS JOIN
  • Give it a meaningful name, like order profile
Next in this series

Question 27 — What is a role-playing dimension? What is a role-playing dimension?

Every query and every result on this page was run on PostgreSQL 16 using the script above. The SQL is standard and runs on most databases with small changes (noted in the script). All tables and data are invented demo data for teaching.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.