If you’ve ever worked on a data warehouse with multiple subject areas, you’ve likely run into a seemingly small issue that turns into a huge problem: inconsistent dimension values — especially for something as basic as countries, Gender and others.
Recently, I faced this exact challenge and wanted to share how I tackled it, along with a few architecture design options I evaluated (and one I highly recommend). The challenge is on Data Warehouse deployed on one Oracle 19c Database, but the concept is the same with other database engines also.
🎯 The Challenge: Same Country, Different Codes
Imagine a scenario where you have three subject areas in your DW, each with its own FACT table like this:
SA1_FACT: usescountry_idfromSA1_COUNTRYSA2_FACT: usescountry_idfromSA2_COUNTRYSA3_FACT: usescountry_idfromSA3_COUNTRY
Each of these country tables contains similar but not standardized data. For example:
| Subject Area | Code | Country Name |
|---|---|---|
| SA1 | 10 | America |
| SA2 | 100 | United States of America |
| SA3 | 70 | US |
Same country, three different codes and names. So when business users ask,
💬 “Give me a report showing sales for the UK across all subject areas”,
we can’t easily unify or filter — because the concept of “UK” is fragmented.
🧠 Why We Can’t Just Standardize?
Simple answer: historical data and client restrictions.
Each subject area has its own data lineage and rewriting the country codes globally isn’t feasible without corrupting historical integrity. Also, Business Users are used to the current codes in their daily work.
💡 Exploring the Design Options
Let’s talk about the solutions I considered:
✅ Option 1: Global COUNTRY Table + Mapping Table per Subject Area
- A central
DW_COUNTRYtable. - A separate mapping table per subject area (e.g.,
SA1_COUNTRY_MAPPING,SA2_COUNTRY_MAPPING, etc.) - Pros: Clean, scalable.
- Cons: Too many mapping tables, joins get verbose.
✅ Option 2: All Mappings Inside the Global Table
Structure like:
DW_COUNTRY (
GLOBAL_CODE,
GLOBAL_NAME,
SA1_CODE, SA1_NAME,
SA2_CODE, SA2_NAME,
...
)
- Pros: Only one table
- Cons: Not normalized, not scalable, breaks if a subject area changes or gets added.
✅ Option 3: Embed Global ID Inside Each Local Country Table
- Add a
GLOBAL_IDcolumn toSA1_COUNTRY,SA2_COUNTRY, etc. - Pros: Keeps mappings close to data
- Cons: Requires modifying subject area schema (which is often off-limits)
✅ Option 4 (🏆 Chosen): Central Global Table + Single Mapping Table for All Areas
This was the winner. Here’s the logic:
- A centralized
DW_COUNTRYtable with unified country definitions - One
COUNTRY_CODE_MAPPINGtable, like this: SUBJECT_AREA SUBJECT_COUNTRY_CODE GLOBAL_COUNTRY_ID SA1 10 1 SA2 100 1 SA3 70 1 - A VIEW that joins all fact tables with this mapping — perfect for Power BI, Oracle APEX, and more.
🛠 Bonus: What About Fact Table Keys?
During this design, I also revisited a common modeling question:
Should fact tables have a surrogate primary key?
✅ Here’s the TL;DR:
| Question | Recommendation |
|---|---|
| Add surrogate PK to fact table? | ❌ Not unless your tools need it |
| Unique constraint on all key columns? | ✅ Yes, if it enforces grain |
| Foreign keys to dimensions? | ✅ Logically yes, physically optional |
📊 The Final Output: Unified Reporting
Using this model, we created a VW_COUNTRY_FACTS_UNIFIED view that lets our BI tools report consistently on any country, regardless of its local code in each subject area.
Power BI users can now say:
“Show me the trend for the United Kingdom”,
…and the system knows that’s Code 30 in SA1, Code 300 in SA2, and Code 90 in SA3 — all tied to one global identity.
🧭 Conclusion
If your data warehouse spans multiple domains, avoid fighting the urge to force everything into a single schema. Instead, bridge dimensions smartly using a global mapping strategy.
💡 Want a copy of the PL/SQL and view logic we used for this? Check it out below:
— =============================================— PL/SQL Framework for Option 1 (Bridge Mapping)
— =============================================
— 1. GLOBAL COUNTRY DIMENSION
CREATE TABLE DW_COUNTRY (
GLOBAL_COUNTRY_ID NUMBER PRIMARY KEY,
UNIFIED_NAME VARCHAR2(100) NOT NULL,
ISO_CODE VARCHAR2(10),
REGION_ID NUMBER,
CREATED_DATE DATE DEFAULT SYSDATE
);
— 2. SUBJECT AREA BRIDGE TABLES
CREATE TABLE SA1_COUNTRY_BRIDGE (
SA1_COUNTRY_CODE NUMBER PRIMARY KEY,
GLOBAL_COUNTRY_ID NUMBER NOT NULL,
FOREIGN KEY (GLOBAL_COUNTRY_ID) REFERENCES DW_COUNTRY(GLOBAL_COUNTRY_ID)
);
CREATE TABLE SA2_COUNTRY_BRIDGE (
SA2_COUNTRY_CODE NUMBER PRIMARY KEY,
GLOBAL_COUNTRY_ID NUMBER NOT NULL,
FOREIGN KEY (GLOBAL_COUNTRY_ID) REFERENCES DW_COUNTRY(GLOBAL_COUNTRY_ID)
);
CREATE TABLE SA3_COUNTRY_BRIDGE (
SA3_COUNTRY_CODE NUMBER PRIMARY KEY,
GLOBAL_COUNTRY_ID NUMBER NOT NULL,
FOREIGN KEY (GLOBAL_COUNTRY_ID) REFERENCES DW_COUNTRY(GLOBAL_COUNTRY_ID)
);
— 3. MAPPING AUDIT TABLE
CREATE TABLE COUNTRY_MAPPING_AUDIT (
SUBJECT_AREA VARCHAR2(10),
ORIGINAL_CODE NUMBER,
ORIGINAL_NAME VARCHAR2(100),
GLOBAL_COUNTRY_ID NUMBER,
MAPPED_DATE DATE DEFAULT SYSDATE,
MAPPED_BY VARCHAR2(50)
);
— 4. PROCEDURE TO INSERT NEW COUNTRY INTO DW_COUNTRY
CREATE OR REPLACE PROCEDURE INSERT_COUNTRY(
p_name IN VARCHAR2,
p_iso_code IN VARCHAR2,
p_region_id IN NUMBER,
p_global_id OUT NUMBER
) AS
BEGIN
SELECT NVL(MAX(GLOBAL_COUNTRY_ID), 0) + 1 INTO p_global_id FROM DW_COUNTRY;
INSERT INTO DW_COUNTRY(GLOBAL_COUNTRY_ID, UNIFIED_NAME, ISO_CODE, REGION_ID)
VALUES (p_global_id, p_name, p_iso_code, p_region_id);
END;
/
— 5. PROCEDURE TO INSERT INTO A BRIDGE TABLE
CREATE OR REPLACE PROCEDURE INSERT_SA1_MAPPING(
p_sa1_code IN NUMBER,
p_sa1_name IN VARCHAR2,
p_global_country_id IN NUMBER,
p_user IN VARCHAR2
) AS
BEGIN
INSERT INTO SA1_COUNTRY_BRIDGE (SA1_COUNTRY_CODE, GLOBAL_COUNTRY_ID)
VALUES (p_sa1_code, p_global_country_id);
INSERT INTO COUNTRY_MAPPING_AUDIT
(SUBJECT_AREA, ORIGINAL_CODE, ORIGINAL_NAME, GLOBAL_COUNTRY_ID, MAPPED_BY)VALUES (‘SA1’, p_sa1_code, p_sa1_name, p_global_country_id, p_user);
END;
/
— 6. GLOBAL UNION VIEW FOR REPORTING
CREATE OR REPLACE VIEW VW_GLOBAL_FACT_COUNTRY AS
SELECT ‘SA1’ AS SUBJECT_AREA, b.GLOBAL_COUNTRY_ID, f.INDICATOR_ID, f.DATE_DIM_ID, f.VALUE
FROM SA1_FACT f
JOIN SA1_COUNTRY_BRIDGE b ON f.COUNTRY_ID = b.SA1_COUNTRY_CODE
UNION ALL
SELECT ‘SA2’ AS SUBJECT_AREA, b.GLOBAL_COUNTRY_ID, f.INDICATOR_ID, f.DATE_DIM_ID, f.VALUE
FROM SA2_FACT f
JOIN SA2_COUNTRY_BRIDGE b ON f.COUNTRY_ID = b.SA2_COUNTRY_CODE
UNION ALLSELECT 'SA3' AS SUBJECT_AREA, b.GLOBAL_COUNTRY_ID, f.INDICATOR_ID, f.DATE_DIM_ID, f.VALUEFROM SA3_FACT fJOIN SA3_COUNTRY_BRIDGE b ON f.COUNTRY_ID = b.SA3_COUNTRY_CODE;— Done.
Would you like help designing a similar pattern for other shared dimensions like GENDER or INDICATORS?
Drop a comment or message — happy to help!