(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

How to implement SCD Type 2 in ETL: step by step

João Barros 06 de July de 2026 5 min read

Keeping the history of your dimensions is one of the classic problems in any data warehouse: when a customer's address changes, you want to know both the old value and the new one, without deleting anything. The technique that solves this is called SCD Type 2 (Slowly Changing Dimension), and you will learn to implement it in an ETL pipeline, step by step and with simple SQL examples.

Prerequisites

  • A relational database (SQL Server, PostgreSQL, MySQL, or equivalent) where you can create tables.
  • Basic SQL knowledge: SELECT, INSERT, and UPDATE.
  • A source with the current customer data — your operational system or a file you load into a staging area.

Step 1: Design the dimension with history columns

The core idea of SCD Type 2 is simple: instead of overwriting a value that changed, you store a new row and keep the old one. To do that, the dimension table needs three control columns in addition to the normal attributes — a validity start date, an end date, and a flag that tells you which version is current.

CREATE TABLE dim_cliente (
    cliente_sk  INT IDENTITY(1,1) PRIMARY KEY,  -- chave substituta
    cliente_id  INT          NOT NULL,          -- chave de negócio
    nome        VARCHAR(100),
    cidade      VARCHAR(100),
    valido_de   DATE         NOT NULL,
    valido_ate  DATE         NULL,
    ativo       BIT          NOT NULL DEFAULT 1
);

Notice the difference between the two keys. The surrogate key (cliente_sk) is unique for each version and is what your fact tables link to. The business key (cliente_id) identifies the real customer, who over time may have several rows — one per change.

Step 2: Load the current data into staging

A good ETL pipeline never compares directly against the source system. Instead, it copies the current snapshot of the data into a staging table and works from there. This isolates the transformation and lets you re-run the process without overloading the source.

CREATE TABLE stg_cliente (
    cliente_id INT,
    nome       VARCHAR(100),
    cidade     VARCHAR(100)
);
-- a extracao (E do ETL) preenche esta tabela com o estado de hoje

Step 3: Detect what changed

Now you compare each customer in staging with their active version in the dimension. If a relevant attribute differs — for example, the cidade (city) — then that customer changed and their current version must be replaced.

SELECT s.cliente_id, s.nome, s.cidade
FROM   stg_cliente AS s
JOIN   dim_cliente AS d
       ON  d.cliente_id = s.cliente_id
       AND d.ativo = 1
WHERE  s.cidade <> d.cidade
   OR  s.nome   <> d.nome;

The JOIN on ativo = 1 makes sure you only compare against the current version, ignoring the historical rows that are already closed. The <> (not equal) operator returns only the records that actually changed.

Step 4: Close the old versions

For each changed customer, mark the active version as ended: fill in the end date and set the ativo flag to 0. This "soft close" is the heart of SCD Type 2 — you never delete the row, you just say it is no longer in effect.

UPDATE d
SET    d.valido_ate = CAST(GETDATE() AS DATE),
       d.ativo      = 0
FROM   dim_cliente AS d
JOIN   stg_cliente AS s
       ON  d.cliente_id = s.cliente_id
       AND d.ativo = 1
WHERE  s.cidade <> d.cidade
   OR  s.nome   <> d.nome;
Tip: always use the same date (for example, the pipeline run date) for the end of one version and the start of the next. That way you avoid gaps in the timeline.

Step 5: Insert the new versions

What is left is to insert a new row for every customer that no longer has an active version. And here is the elegant part: because Step 4 just closed the changed versions, both the new customers and the changed ones now have no row with ativo = 1. A single INSERT handles both cases.

INSERT INTO dim_cliente (cliente_id, nome, cidade, valido_de, valido_ate, ativo)
SELECT s.cliente_id, s.nome, s.cidade, CAST(GETDATE() AS DATE), NULL, 1
FROM   stg_cliente AS s
LEFT JOIN dim_cliente AS d
       ON  d.cliente_id = s.cliente_id
       AND d.ativo = 1
WHERE  d.cliente_sk IS NULL;

The order is mandatory: close first (Step 4), then insert (Step 5). If you reverse it, you risk having two active versions of the same customer at once — and the history stops making sense.

Check the result

To confirm everything went well, query a customer you know has changed. You should see two rows: the old one, now with valido_ate filled in and ativo = 0, and the new one, still open and active.

SELECT cliente_id, cidade, valido_de, valido_ate, ativo
FROM   dim_cliente
WHERE  cliente_id = 42
ORDER  BY valido_de;

The golden rule for validating an SCD Type 2: each cliente_id must have exactly one row with ativo = 1. If two show up, something went wrong in the order of the steps.

Conclusion

You have just built a complete SCD Type 2 dimension — you prepared the table, detected the changes, closed the old versions, and inserted the new ones, without losing a single piece of historical data. The next step is to wrap Steps 4 and 5 in a single transaction (so they never end up half-done) and schedule the whole process in your orchestrator, whether that is Azure Data Factory, dbt, or a simple SQL job. Before you move on, sit with this question: of your dimension's attributes, which ones truly need history — and which can you simply overwrite with an SCD Type 1?