Skip to the content
Software Made Clear Diagrams that show the mechanism About

The star schema

Choose one fact-row grain, then compare a flat dimension with a snowflake. See what changes in the query and what still needs measuring.

7 min read Updated September 27, 2026 Last verified September 27, 2026
ANSWER

A star schema keeps measurements in a fact table at one declared grain and descriptions in dimension tables joined to it. A flat dimension can simplify a report's query; whether it is faster than a normalised snowflake depends on the engine and workload.

IN PLAIN TERMS

Give each product line on a receipt one record with its amount and keys for the day, shop and product. The shop and product cards supply the names used to group those amounts. Keep receipt totals separate or you will add the same sale twice.

A bookshop wants sales by product and region. One product line on one receipt is the event it records; product and shop tables give that event names to group by. Before choosing between a flat shop dimension and separate region table, decide what each sales row means. Otherwise a report can double-count correct-looking rows. This bookshop is an illustrative example.

fact_salesdate_keyproduct_keyshop_keycustomer_keyΣ quantityΣ revenuedim_datefull_dateday_of_weekmonthquarteryearis_weekendfiscal_periodholiday_namedim_productproduct_namecategorysubcategorybrandcolorsizematerialsupplierdim_shopshop_nameregioncitycountryformatsize_classmanageropened_yeardim_customercustomer_namesegmentage_bandgenderregionchannelloyalty_tieracquisition_sourcefact_salesdate_keyproduct_keyshop_keycustomer_keyΣ quantityΣ revenuedim_datefull_dateday_of_weekmonthquarteryearis_weekendfiscal_periodholiday_namedim_shopshop_nameregioncitycountryformatsize_classmanageropened_yeardim_productproduct_namecategorysubcategorybrandcolorsizematerialsupplierdim_customercustomer_namesegmentage_bandgenderregionchannelloyalty_tieracquisition_source

The figure sketches the four joins to dimensions. It omits the receipt ID and line number that distinguish two fact rows with the same dimension keys; the SQL example below includes them.

The grain decides everything else#

The grain is the sentence that says what one fact row represents. Here it is one product line on one receipt. A receipt subtotal or a daily product total has a different grain and belongs in another fact table or a separate aggregation. Kimball’s grain rule requires choosing this level before dimensions and measures.

Try it: what does one fact row mean?

Illustrative receipt: line 1 is £40 and line 2 is £60. A sales report sums the revenue field.

Rows: £40 line + £60 line

SUM(revenue) = £100

The two line rows add to the receipt total once.

This shows one double-counting mistake, not a database benchmark. Keep the line-grain fact table and any receipt-level summary in separate tables, or query only one grain.

Each dimension key must describe the event at the chosen grain. Measures also need an aggregation rule. Revenue from separate product lines can be summed by shop, day or product without counting one line twice. Unit price and percentage are different: summing their values does not give total revenue or an overall rate. A snapshot of an account balance can be summed across accounts for one date, but summing the same account’s balances across dates counts holdings repeatedly. That makes the balance semi-additive. Kimball’s measure rules spell out this time boundary.

In the example above, £40 and £60 are the two lines; £100 is the receipt subtotal. Summing all three rows gives £200 for a £100 sale. The values are individually correct, but they cannot share a fact table used for an unfiltered line-level sum.

fact_sales.sql
-- Grain: one product line on one receipt, identified by receipt and line.
CREATE TABLE fact_sales (
  receipt_id   BIGINT NOT NULL,
  line_number  INT NOT NULL,
  date_key     INT NOT NULL REFERENCES dim_date(date_key),
  product_key  INT NOT NULL REFERENCES dim_product(product_key),
  shop_key     INT NOT NULL REFERENCES dim_shop(shop_key),
  customer_key INT NOT NULL REFERENCES dim_customer(customer_key),
  quantity     INT NOT NULL,
  revenue      NUMERIC NOT NULL,
  PRIMARY KEY (receipt_id, line_number)
);

The receipt ID and line number make this example’s grain explicit and unique. A second line could have the same product, customer, shop and date keys, so those dimension keys alone do not identify a line. The dimension tables referenced here are omitted for brevity.

Why the dimensions stay denormalised#

Look at the shop dimension in the figure above: name, region, city, country, format, size class, manager, the year it opened — eight columns, all sitting in one table, all describing the same shop. A region name gets written once per shop, and every shop in that region writes the same region name again. Textbook normalisation would pull region into its own table and leave a reference behind. A star schema deliberately does not, and the reason is what that repetition buys.

A query that filters by region and groups by shop format can join the fact table to dim_shop once when both attributes live there. If region lives in a separate table, a SQL query using it may need another join. That is a query-shape difference, not a measured speedup: indexes, table sizes, caching and the engine’s optimiser still decide execution cost.

A flat dimension may store a region name on many shop rows. Correcting a current name can then require several updates; a separate region table may need one. Historical dimensions may deliberately preserve old labels, so a rename policy is a modelling decision rather than an automatic bulk update.

That second shape — the one that keeps region in its own table and references it — is a snowflake schema: a star whose dimensions have themselves been normalised into further tables. The name describes the picture: what was one box per dimension grows branches, the way a snowflake’s arms do. Nothing about the fact table changes; only how far a dimension’s own attributes have been split out changes.

AspectStar schemaSnowflake schema
Joins in a region reportFact to shop when region is in the shop dimension.Fact to shop to region when region is separate.
Storage of repeated textA shared value, like a region name, is written on every dimension row that carries it.The value lives once, in the table it was split into, and every row that needs it holds a reference.
Current region-name correctionMay update each affected shop row, subject to history rules.May update one region row, subject to history rules.
How the query readsFilter and group by the dimension column directly, the way the question was asked.Filter and group through the chain of joins the schema was normalised into.

Neither shape is always best. A flat dimension may make authoring reports easier; a snowflake may reduce repeated values. Microsoft’s Power BI guidance treats the choice as dependent on data volume and model usability. Its product-specific performance observations should not be turned into a guarantee for every relational engine.

Normalising the dimension away#

Consider a proposed change: move category from each product row into a separate category table and leave a key on the product. The following questions must be answered before claiming a benefit or regression.

01

Which reports actually use category?

Their SQL may now join fact, product and category instead of fact and product. Reports that never use category need no new join. Inspect generated SQL as well as hand-written queries.

02

Did the actual plan and latency change?

Compare representative query plans and timings before and after the change. An extra relationship in the model does not establish a measurable slowdown on its own.

03

What happens when category changes?

Check whether reports need the current category or the category as it was at sale time. That history rule affects dimension keys and loading whether the dimension is flat or split.

The choice is between costs that can be inspected. Splitting category can reduce duplicated names and simplify some updates. Keeping it on product can make category reports easier to express. A migration should compare both the data-loading rule and the reports that use the attribute. The OLTP/OLAP comparison separates this schema decision from the separate row-versus-column storage question.

If an attribute changes independently and multiple models reuse it, a separate table may be useful. If history matters, simply overwriting a label in either shape may answer yesterday’s question incorrectly. Test that rule with sample changes before deciding from an assumed update frequency.

A star schema does not require every query to scan most of the fact table. Filters, indexes and storage layout can change the plan; the index-plan guide shows why the engine might choose a scan or an index. The persistence path connects this schema to the surrounding storage questions.

The schema also needs a loading rule. The warehouse reload experiment shows why replacing one date’s rows must undo the deletion when the new insert fails.

IF YOU REMEMBER ONE THING

State the grain before loading a fact table; keep line items and receipt totals separate. For dimension layout, compare report queries, update/history rules and measured plans instead of assuming an extra join guarantees a slowdown.

Questions people also ask

5 QUESTIONS
What is the difference between a star schema and a snowflake schema?

A star joins the fact table directly to each dimension used by a query. A snowflake splits some dimension attributes into related tables, so a query using those attributes may need another join. Duplication, update work and query shape change; measure performance rather than assuming every report slows down.

What is the grain of a fact table?

The plain-English statement of what one row means — one product on one receipt, say, or one day per product per shop, whatever the business process actually produces. Every dimension that can attach, every measure that can be summed, and every question the model can answer has to agree with that one sentence, which is why it gets decided first.

Should a fact table have its own primary key?

Not necessarily a single generated key. Choose a key that actually identifies the declared grain. For one product line on one receipt, receipt ID plus line number can distinguish two lines with the same product and dimension keys; those dimension keys alone might not.

How do I handle a dimension value that changes over time?

That is the slowly changing dimension problem. The plain option overwrites the value in place and loses the old one. A common alternative keeps history by inserting a new dimension row when the value changes and pointing new fact rows at it, so a fact recorded in the past keeps reading the value that was true when it happened.

Can I use a star schema in a normal relational database?

Yes — a star schema is a way of arranging ordinary tables, not a feature a database has to support. Any relational database that does foreign keys and joins can hold one. What changes at genuinely large volumes is usually indexing and storage layout, not the shape of the tables themselves.

The library Browse all articles