← All lessons
Warehouses and pipelines · Lesson 4 of 7

Transformation models and tests

In the dbt style of working (and tools like it), each transformation is a model: a file holding one SELECT statement. The tool turns each model into a view or table in the warehouse. A model refers to the ones it builds on, so the tool can work out the order to build them in and draw the dependency graph.

-- models/marts/fct_order_revenue.sql
select o.id as order_id, o.ordered_on, sum(i.qty * p.price) as revenue
from {{ ref('stg_orders') }} o
join {{ ref('stg_order_items') }} i on i.order_id = o.id
join {{ ref('stg_products') }} p on p.id = i.product_id
group by o.id, o.ordered_on

How a model is stored is its materialisation: a view (always fresh, worked out at query time), a table (rebuilt on each run) or incremental (only new or changed rows are processed each run, for big tables).

Tests are what make the warehouse trustworthy. A data test is a query that returns the rows that break a rule; zero rows means it passed. The common ones: a key is unique, a column is never null, a value is from an accepted list, and every foreign key points at a row that exists. They're declared next to the model:

models:
  - name: stg_payments
    columns:
      - name: payment_id
        data_tests: [unique, not_null]
      - name: order_id
        data_tests:
          - relationships:
              to: ref('stg_orders')
              field: id
      - name: currency
        data_tests:
          - accepted_values:
              values: ['USD']

Underneath, each test is plain SQL. The relationships test, for example, finds payments whose order doesn't exist:

SELECT p.id, p.order_id
FROM payments p
LEFT JOIN orders o ON o.id = p.order_id
WHERE p.order_id IS NOT NULL AND o.id IS NULL;
Returns
id | order_id
---+---------
10 | 999
(1 row)

Run the tests on every build and stop the pipeline (or at least alert) when one fails, before bad data reaches a dashboard. Keep models in version control, review changes like code, and build them in a development schema before production.

Practice

A not_null test on customers.city: return the id and name of the rows that break it.

The tables
  • customers (id, name, city, joined)
  • products (id, name, category, price)
  • orders (id, customer_id, ordered_on, status)
  • order_items (order_id, product_id, qty)
  • signups (id, email, name, signed_up_at)
  • payments (id, order_id, amount, currency, paid_at)
More info

Quick reference

ModelOne SELECT in a file; built into a view or table.
ref()dbt's way to refer to another model, which records the dependency.
View, table, incrementalMaterialisations: query-time, rebuilt, or only new rows.
Data testA query returning rows that break a rule; empty = pass.
unique, not_null, accepted_values, relationshipsThe four everyday tests.

The other AI and data engineering courses