Free SQL template

Data-quality checks

Duplicates, missing values, orphans and impossible values, each as one row with a count.

It opens with this template loaded and runs in your browser. You don't need an account to use it; a free account saves your work and exports it.

The job it does

Before you trust a table: after an import, before a migration, or when a report looks wrong. It comes with its own checks, so after you change it you can see at a glance that it still does the job.

How to use it with your own data

Import your CSV (or paste your schema into setup.sql) and change the table and column names in each check.

What's inside

This is the template exactly as the SQL Lab opens it, read-only. Open it to run it, change it and export it.

  • Runs on the retail sample database (Mangrove Market: stores, customers, orders, payments)
  • 2 tabs
  • PostgreSQL in your browser
The brief: Data-quality checks

Before you trust a table, check it. Each row is one rule the data must keep and how many rows break it: 0 is good. On the sample data three orders were charged twice, 19 delivered orders were never paid, and 13 orders are older than the customer account they belong to; the criteria pin those counts, so a fix (or a new problem) shows up as a change.

Run it after every import and before a migration. The second tab lists the rows behind a check, so you can fix them.

Your own data: import your CSV (or paste your schema into setup.sql) and change the table and column names in each check.

Done when
  • Has the columns check_name, problems
  • Returns 6 rows
  • Equals the expected table
  • Does not change any data
checks
-- Data-quality checks: one row per check, with how many rows break it.
-- 0 is good. Add a check for every rule your data must keep.
SELECT 'payments: an order paid more than once' AS check_name, count(*) AS problems
FROM (SELECT order_id FROM payments WHERE status = 'paid' GROUP BY order_id HAVING count(*) > 1) d
UNION ALL
SELECT 'orders: delivered but never paid', count(*)
FROM orders o
WHERE o.status = 'delivered' AND NOT EXISTS (SELECT 1 FROM payments p WHERE p.order_id = o.id)
UNION ALL
SELECT 'orders: placed before the customer joined', count(*)
FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE o.ordered_at::date < c.joined_on
UNION ALL
SELECT 'customers: email missing or malformed', count(*)
FROM customers
WHERE email IS NULL OR email !~ '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'
UNION ALL
SELECT 'customers: the same email twice', count(*)
FROM (SELECT lower(email) FROM customers GROUP BY 1 HAVING count(*) > 1) d
UNION ALL
SELECT 'stock: negative quantity', count(*)
FROM stock WHERE qty < 0
ORDER BY problems DESC, check_name;
paid twice
-- The rows behind the first check: the orders paid twice.
SELECT p.order_id, count(*) AS payments, round(sum(p.amount_cents) / 100.0, 2) AS charged, min(p.paid_at) AS first_paid
FROM payments p
WHERE p.status = 'paid'
GROUP BY p.order_id
HAVING count(*) > 1
ORDER BY p.order_id;

The lesson behind it

Learn why it works, with a podcast and a quiz for each lesson.

  • Schema design for a new feature

    Tables, keys and constraints for a new feature, sample rows, and the queries the feature needs.

  • Monthly sales report

    Revenue, orders and average order value per month, with the change on the month before.

  • Cohort retention

    Customers grouped by the month of their first order: how many come back in each later month.

All SQL templates · Every template