Fact Table vs Dimension Table

A fact table records business events, 1 row per event at a declared grain, with numeric measures you sum and foreign keys to the things involved. A dimension table describes those things with the attributes you filter and group by: 1 row per customer in a customer dimension, per product in a product dimension, and so on for stores and dates.

Last updated: Proudly published by: Jeff Wahl14 min read

Fact table vs dimension table: the difference

A fact table records the events a business measures, 1 row per event, holding numbers you sum or count and keys to the things involved. A dimension table describes those things and holds the labels you filter and group by: 1 row per customer in a customer dimension, per product in a product dimension, and so on for stores and dates. In a query, the fact table supplies the SUM(amount) and the dimension tables supply the WHERE and the GROUP BY.

Ralph Kimball's definition is the one most teams work from. Facts are the measurements that result from a business process event, and are almost always numeric. Dimensions answer the who and what of that event, and the where, when, why and how. The fact table also carries a grain, which Kimball defines as exactly what a single row represents, as his notes on declaring the grain put it: 1 row per order line, per page view, per account per day.

Most of the practical differences follow from that split. Events keep happening, so fact tables grow tall and stay narrow. Entities are few and change slowly, so dimension tables stay short and grow wide with descriptive columns. Fact rows are mostly written once, dimension rows are revised as the things they describe change, and the 2 are joined by keys that the dimension owns and the fact borrows.

Fact table vs dimension table at a glance

Fact table
  • RecordsBusiness events
  • 1 row is1 event at the grain
  • Main columnsKeys and measures
  • Typical row countMillions to billions
  • Gains rows whenAn event happens
  • Primary keyImplied by its keys
  • Queries use it toSUM, COUNT, AVG
  • Relationship sideMany
Dimension table
  • RecordsEntities and context
  • 1 row is1 entity or 1 version
  • Main columnsDescriptive attributes
  • Typical row countHundreds to millions
  • Gains rows whenAn entity is new
  • Primary keySurrogate key
  • Queries use it toFilter and group
  • Relationship sideOne

What goes in a fact table

A fact table holds 4 kinds of column. Dimension keys, 1 per dimension, each a foreign key to a dimension's surrogate key. Measures, the numbers the event produced: quantity and extended_amount, for example, or discount_amount. Degenerate dimensions, identifiers such as an order or invoice number that belong to the event but have no attributes worth a table of their own. And, optionally, audit columns that record which load wrote or changed the row.

Everything in the table must be true at the declared grain. Quantity belongs on an order line fact because each order line has exactly 1 quantity, and the same goes for extended price. The store manager's salary does not, even though every sale happens in a store with a manager: it'sn't a measurement of the order line, and summing it across lines would multiply it by the number of lines. Kimball's rule is that only facts consistent with the declared grain are allowed.

Fact tables rarely declare a primary key. Microsoft's Fabric warehouse guidance leaves it off because the combination of dimension keys and degenerate identifiers already implies 1, and an extra key only adds storage. What must hold is that the grain is unique: 2 rows for the same order line is a double count waiting to happen. Nearly every fact table also carries at least 1 date key, since almost every event happens on a day.

Choosing the grain comes before choosing the columns. The first 2 steps of dimensional modeling are to pick the business process and declare its grain, and the atomic grain, the lowest level the source system records, is the safe default because a detailed table can always be rolled up and a summary can never be split back down.

Prepare for the interview
01 / Open invite
02min.

Know Fact vs dimension tables the way the interviewer who asks it knows it.

a Fact vs dimension tables query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1fact_orders
2 order_id bigint PK
3 customer_sk bigint FK
4 order_date date SCD2
5
Execute your solution0.4s avg.

What goes in a dimension table

A dimension table holds 1 row per member of a business entity: a customer, a product, a store, a calendar day. Its columns are the words people use in reports: customer_name, city, segment, brand, category, fiscal_quarter, is_holiday. The more descriptive attributes a dimension carries, the more ways a fact table can be sliced, which is why dimensions are wide and why Kimball calls them the soul of the warehouse.

Every dimension row has a surrogate key, a meaningless integer the warehouse assigns, beside the natural key the source system uses, such as customer_id C-1042 or a SKU. Hierarchies are usually flattened into the same row: each product row in dim_product also carries its subcategory and category, so a report can drill from 1 level to the next without extra joins. The date dimension is the common exception to the surrogate rule, since a key such as 20260927 is stable and readable.

Dimensions also hold special members for the cases the facts cannot fill: an Unknown customer for an anonymous sale, a Not applicable ship date for an order that hasn't shipped. Pointing a fact at 1 of these rows keeps every foreign key NOT NULL, so inner joins never silently drop events.

A sales star schema: 1 fact table and 4 dimensions

fact_order_line
order_numberPKVARCHAR
line_numberPKINT
date_keyFKINT
customer_keyFKINT
product_keyFKINT
store_keyFKINT
quantityINT
unit_priceDECIMAL
discount_amountDECIMAL
extended_amountDECIMAL
dim_date
date_keyPKINT
full_dateDATE
day_of_weekVARCHAR
fiscal_quarterVARCHAR
is_holidayBOOLEAN
dim_customer
customer_keyPKINT
customer_idVARCHAR
customer_nameVARCHAR
cityVARCHAR
segmentVARCHAR
valid_fromDATE
valid_toDATE
is_currentBOOLEAN
dim_product
product_keyPKINT
skuVARCHAR
product_nameVARCHAR
brandVARCHAR
categoryVARCHAR
list_priceDECIMAL
dim_store
store_keyPKINT
store_idVARCHAR
store_nameVARCHAR
cityVARCHAR
regionVARCHAR

order_number and line_number fix the grain at 1 row per order line. order_number is also a degenerate dimension with no table of its own. The fact carries 4 foreign keys and the numbers of the sale; each dimension pairs a surrogate key with its natural key and adds the attributes a report filters and groups by. dim_customer keeps history in valid_from and valid_to, with is_current flagging the live row. On dim_product, list_price is a descriptive attribute, while unit_price on the fact is the price actually paid.

Why fact tables grow tall and dimension tables stay small

The size difference falls out of what each table counts: a fact table gains a row for every event, and a dimension gains a row only when a new entity appears or a tracked attribute changes. Microsoft's guidance says dimension tables generally hold a relatively small number of rows, while fact tables can hold a large number and keep growing.

Take a retailer that sells 50,000 order lines a day while signing up 120 new customers and adding 5 products each day. Over 365 days, fact_order_line gains 18,250,000 rows. In the same period dim_customer gains only 43,800, and dim_product gains 1,825. The fact table grows about 417 times faster than the customer dimension, and the gap widens every year because events accumulate while the customer base only creeps up.

Rows each table gains in 1 year on 1 scale: fact_order_line gains 18,250,000 rows, 417 times dim_customer, which gains 43,800, while dim_product gains 1,825Rows each table gains in 1 year on 1 scale: fact_order_line gains 18,250,000 rows, 417 times dim_customer, which gains 43,800, while dim_product gains 1,825

The shapes run the other way. The order line fact has roughly 10 columns. Nearly all of them are integers or decimals, so each row is small. The customer dimension can carry 40 or more columns, mostly text such as names and addresses, plus segments. So the storage and query cost of a warehouse sits in its facts, which is why facts get partitioned by date, and a join from a large fact to a small dimension is the cheap direction.

How fact and dimension tables connect

Each foreign key on the fact row holds the surrogate key of exactly 1 dimension row, so many fact rows point to 1 dimension row. That many to one direction is what makes a table a fact or a dimension: Microsoft's star schema guidance notes that no table property sets the type, and that in a 1 to many relationship the one side is always the dimension and the many side always the fact. Draw the fact in the middle with its dimensions around it and you have a star schema.

A typical query joins the fact to the dimensions it needs and aggregates the measures, using dimension attributes to filter and group: SELECT d.fiscal_quarter, p.category, SUM(f.extended_amount) FROM fact_order_line f JOIN dim_date d ON f.date_key = d.date_key JOIN dim_product p ON f.product_key = p.product_key GROUP BY 1, 2. Every column in the select list is either a dimension attribute or an aggregate of a fact, which is a quick way to check a query against the model.

1 dimension can play several roles. An order line with an order date and a ship date carries 2 keys to the same dim_date, order_date_key and ship_date_key. Which role a query sees depends on the join column. Analysts often expose each role as its own view with renamed columns, so a report never confuses order year with ship year.

Why fact tables store surrogate keys, not natural keys

Many first designs have the fact row store the natural key, the source system's own identifier. The trouble starts when a dimension keeps history. Customer C-1042 places 3 orders while living in Austin, then moves to Denver and places 2 more. If the warehouse tracks the move as a new version of the customer, C-1042 now matches 2 rows, and a join on the natural key has no way to tell which one an order belongs to.

The surrogate key fixes this. The Austin version is key 501 and the Denver version is key 877, and each fact row stores whichever key was current when the order happened. Sales by city then come out as $340 for Austin and $210 for Denver. Join on the natural key to the current row instead and all $550 lands in Denver, which rewrites history.

Customer C-1042 has 2 rows in dim_customer: key 501 in Austin and key 877 in Denver; 3 orders worth $340 in fact_orders point to key 501 and 2 orders worth $210 point to key 877Customer C-1042 has 2 rows in dim_customer: key 501 in Austin and key 877 in Denver; 3 orders worth $340 in fact_orders point to key 501 and 2 orders worth $210 point to key 877

Surrogate keys also insulate the warehouse from the source: when a CRM reissues its IDs or 2 systems use the same number for different customers, only the mapping in the load changes and the fact table does not. The versioning itself, with its valid from and valid to dates, is the subject of slowly changing dimensions.

How to decide whether a column goes in the fact or dimension table

Ask what the column describes. If it's a measurement of the event at the fact table's grain, it's a fact. If it describes an entity involved in the event, and would be the same no matter which event you looked at, it's a dimension attribute. quantity on an order line describes that line; brand describes the product whatever order it appears on.

Then ask how it will be used. A value that mostly gets summed or averaged, or used as a divisor, belongs in the fact table. A stable value that mostly labels rows, or gets filtered and grouped on, belongs in a dimension. Kimball's guidance on numeric values uses the product's standard list price as the hard case: it is a number, but if analysts mostly ask for products priced over $100, it is an attribute, often paired with a price band column such as $50 to $100.

Price shows why some values live in both places. The price paid on a given order line is net of discounts and promotions and varies by event, so it is a fact. The current list price describes the product and is a dimension attribute. Keeping both lets a report compare what customers paid with what the catalogue asked.

How fast the value changes matters too. An attribute that changes on most events, such as a stock's market price, is really a measurement and is stored as a fact. An attribute that changes rarely, such as a customer's city, stays in the dimension and is versioned when history matters. Text is not a test: a text value captured with the event, such as a cancellation reason, is often best modelled as a small dimension, and a numeric value such as a store's floor area is a dimension attribute.

Additive, semi-additive and non-additive facts

Not every fact can be summed along every dimension, and knowing which can is what keeps totals right. An additive fact sums across every dimension of the table: day, product, store, customer. extended_amount and quantity both work this way. It's the most useful kind, and a model should store facts in additive form wherever it can. Revenue is additive only in 1 currency; a fact with a currency key can't be summed across currencies.

A semi-additive fact sums across some dimensions but not time. Balances are the classic case: an inventory value or an account balance can be added across products or stores on 1 day, but adding Monday's balance to Tuesday's counts the same stock twice. Across dates, take the balance at the end of the period or the average of the daily balances.

A non-additive fact can't be summed at all: unit prices, ratios, percentages, temperatures. Store its additive parts instead and compute the ratio at query time. A discount rate is better stored as discount_amount beside extended_amount, so a quarter's rate is SUM(discount_amount) / SUM(extended_amount), weighted correctly, rather than an average of line rates that treats a $5 line like a $5,000 one.

Which facts you can sum, and across what

extended_amount
  • KindAdditive
  • Sum across productsYes
  • Sum across storesYes
  • Sum across datesYes
  • Across dates, useSUM
inventory_value
  • KindSemi-additive
  • Sum across productsYes
  • Sum across storesYes
  • Sum across datesNo
  • Across dates, usePeriod end or AVG
unit_price
  • KindNon-additive
  • Sum across productsNo
  • Sum across storesNo
  • Sum across datesNo
  • Across dates, useWeighted average

Transaction, periodic snapshot and accumulating snapshot fact tables

Fact tables are often described as append only, but that's true of only 1 of the 3 kinds. A transaction fact inserts 1 row per event and never changes it except to correct an error. A periodic snapshot inserts 1 row per entity per period, such as each product's stock at the end of each day, and its measures are semi-additive. An accumulating snapshot keeps 1 row per process instance and updates it as the process reaches each milestone.

Follow 1 order that's placed on day 1, shipped on day 3 and delivered on day 6. A transaction fact of order events ends up with 3 rows, 1 per event. A daily snapshot of open orders writes 5 rows, 1 for each day end the order was still open. An accumulating snapshot of orders holds 1 row. It's inserted when the order is placed, then updated at shipping and again at delivery, 2 updates in all.

1 order placed on day 1, shipped on day 3 and delivered on day 6: a transaction fact inserts 3 rows, a daily periodic snapshot inserts 5 rows, and an accumulating snapshot holds 1 row updated 2 times1 order placed on day 1, shipped on day 3 and delivered on day 6: a transaction fact inserts 3 rows, a daily periodic snapshot inserts 5 rows, and an accumulating snapshot holds 1 row updated 2 times

Each shape answers a different question cheaply. Counting shipments per day is a filter on the transaction fact. Open orders on any date is a lookup in the snapshot. The accumulating snapshot's single row carries 1 date key per milestone and measures that are mostly durations: 2 days from order to shipment here, and 5 days from order to delivery.

Factless fact tables, degenerate dimensions and tables that look like both

A factless fact table has no measures at all, only dimension keys. Each row records that some entities came together: a student attended a class on a date, a customer signed in, a product was on promotion in a store for a week. It's still a fact table, because each row is an event at a grain, and it's measured by counting rows. Paired with a coverage table of what could have happened, it answers what did not: products on promotion that sold nothing, students enrolled who never attended.

A degenerate dimension is the reverse case: a dimension key with no dimension table. The order number on an order line identifies which lines were bought together. Analysts filter and group by it, but every descriptive fact about the order already lives in other dimensions. It stays on the fact table as a plain column.

Tables that look like both usually come from mixing grains. An order header with a total and a customer, joined to order lines, looks like a dimension to the lines and a fact to the customer. The standard fix keeps the line as the fact, pushes header level amounts such as shipping cost down to lines by allocation, and keeps the order number as a degenerate dimension. A bridge table between 2 dimensions, such as salespeople assigned to several regions, is a factless fact in structure even though it's used like part of a dimension.

How fact vs dimension comes up in data modeling interviews

Interviewers rarely ask for the definition on its own. They hand over a business process, such as rides or subscriptions, and ask for a schema. Ad impressions come up as well. The fact vs dimension split is tested through the answer: whether the grain is stated before any column, whether measures sit at that grain, whether a balance is summed across days, whether history survives a customer's move.

The strongest answers say the grain out loud in 1 sentence, name the fact type it implies, and then place each column with a reason. The data modeling interview questions are drawn from real interviews and cover these follow ups. The fact table interview questions go deeper on grain and additivity, and on snapshots. In the data modeling practice problems you build the tables for a real prompt. The data modeling interview round guide explains how that round runs and what interviewers look for.

Fact table vs dimension table FAQ

What is the difference between a fact table and a dimension table?+
A fact table records business events at a declared grain, 1 row per event. Its columns are numeric measures and foreign keys. A dimension table describes the entities involved in those events. It has 1 row per entity or version, holding descriptive attributes under a surrogate key. Queries aggregate the facts and slice them by the dimension attributes.
What is a fact table in simple terms?+
A log of things that happened: sales, clicks, shipments, logins. Each row is 1 event at the table's grain, carrying the measures of that event (amount, quantity, duration) and a key for each dimension that describes it. Fact tables are usually the largest tables in a warehouse because they gain rows with every event.
What is a dimension table in simple terms?+
A table of the things the business talks about: customers, products, stores, dates. Each row describes 1 of them in columns such as name, category, city or segment. Fact rows point to it through its surrogate key. Dimensions stay small because they gain rows only when a new entity appears or a tracked attribute changes.
How are fact and dimension tables connected?+
Through foreign keys. The fact table holds 1 key column per dimension, and each value matches the surrogate key of 1 dimension row. Many fact rows point to 1 dimension row, so the fact table is always the many side of the relationship. With the fact in the middle and its dimensions around it, the result is a star schema.
Can a column be both a fact and a dimension attribute?+
Yes. A value used in calculations is a fact and a stable value used to filter or group is a dimension attribute, and some values are used both ways. The price actually paid on an order line is a fact, while the product's current list price, often with a price band beside it, is an attribute of the product dimension.
What is a factless fact table?+
A fact table with no measure columns, only dimension keys. Each row records that some entities came together at a moment, such as a student attending a class on a date or a customer signing in. You measure it by counting rows, and by comparing it with a table of what could have happened to find what did not.
Can a table be both a fact table and a dimension table?+
Not in a well designed star. A table's role follows from its relationships: the one side is a dimension and the many side is a fact. When an order header looks like both, the usual design keeps order lines as the fact at the atomic grain, stores the order number on each line as a degenerate dimension, and moves header attributes into dimensions.
Which is bigger, a fact table or a dimension table?+
The fact table, in rows, almost always. It gains 1 row per event while a dimension gains a row per new entity or tracked change. Dimensions are usually wider, with dozens of descriptive columns, while a fact table carries a few keys and measures.
02 / Why practice

The candidate who gets the offer

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    The round is won on tradeoffs, not on the diagram

    Grain, star vs snowflake, SCD type, conformed dimensions, late-arriving data. Modeling under live pushback is what separates the bands, and it is the half almost nobody rehearses

Related guides