Free tool

PostgreSQL data check SQL generator

Freshness, row count, null rate, duplicates and a job heartbeat, written for your database. Pick a dialect, name your table, copy the SQL. Runs in your browser, nothing you type is sent anywhere.

See the SQL

When each row was loaded or created.

Comma separated. Several columns check them as a combination.

Comma separated.

Output style
Advanced

Your checks

Freshness

Zero rows means healthy

Returns a row when the newest loaded_at is older than 30 hours.

SELECT newest
FROM (
  SELECT MAX(loaded_at) AS newest FROM analytics.orders
) s
WHERE newest IS NULL
   OR newest < now() - interval '30 hours' -- 30h threshold

Row count

Zero rows means healthy

Returns a row when the last 24 hours are more than 30% away from the same weekday over the last 4 weeks. Needs 28 days of history.

SELECT rows_24h, rows_prior_avg
FROM (
  SELECT
    COUNT(CASE WHEN loaded_at >= now() - interval '24 hours' THEN 1 END) AS rows_24h,
    (
      COUNT(CASE WHEN loaded_at >= now() - interval '192 hours' AND loaded_at < now() - interval '168 hours' THEN 1 END) +
      COUNT(CASE WHEN loaded_at >= now() - interval '360 hours' AND loaded_at < now() - interval '336 hours' THEN 1 END) +
      COUNT(CASE WHEN loaded_at >= now() - interval '528 hours' AND loaded_at < now() - interval '504 hours' THEN 1 END) +
      COUNT(CASE WHEN loaded_at >= now() - interval '696 hours' AND loaded_at < now() - interval '672 hours' THEN 1 END)
    ) / 4.0 AS rows_prior_avg
  FROM analytics.orders
  WHERE loaded_at >= now() - interval '696 hours'
) s
WHERE rows_prior_avg > 0
  AND ABS(rows_24h - rows_prior_avg) * 100 > rows_prior_avg * 30 -- 30% threshold

Null rate

Zero rows means healthy

Returns a row for each column whose null rate is above 1%.

  • Scans the whole table. Add a WHERE on the timestamp column to limit it to recent rows.
SELECT column_name, null_pct
FROM (
  SELECT 'customer_id' AS column_name, 100.0 * (COUNT(*) - COUNT(customer_id)) / NULLIF(COUNT(*), 0) AS null_pct FROM analytics.orders
  UNION ALL
  SELECT 'email' AS column_name, 100.0 * (COUNT(*) - COUNT(email)) / NULLIF(COUNT(*), 0) AS null_pct FROM analytics.orders
) s
WHERE null_pct > 1 -- 1% threshold

Duplicates

Zero rows means healthy

Returns each order_id value that appears more than once.

SELECT order_id, COUNT(*) AS occurrences
FROM analytics.orders
GROUP BY order_id
HAVING COUNT(*) > 1

Heartbeat

Zero rows means healthy

Returns a row when no load has landed since the last expected run, so a job that never started still gets caught.

SELECT rows_in_window
FROM (
  SELECT COUNT(*) AS rows_in_window
  FROM analytics.orders
  WHERE loaded_at >= now() - interval '24 hours' -- 24h window
) s
WHERE rows_in_window = 0

How to run these

Each check is a plain SELECT. Save it to a file and run it from whatever already runs jobs for you.

Cron and psql

# hourly; mails you when the query returns any rows (grep . exits 0 only when it sees output)
0 * * * * psql "$DATABASE_URL" -At -f freshness.sql | grep . && echo "freshness check failed" | mail -s "data check" [email protected]

dbt singular test

Save the zero rows version as tests/orders_freshness.sql. dbt fails the test when the query returns any row.

Airflow

Use the One number style with SQLValueCheckOperator. The operator fails the task when the query returns no rows, so the zero rows style would fail a healthy check. The count always returns one row, andpass_value=0 passes only when it is 0.

SQLValueCheckOperator(
    task_id="orders_freshness",
    conn_id="warehouse",
    sql=FRESHNESS_SQL,
    pass_value=0,
)

arcodash monitor

Paste the One number version as a query, save it, and add a monitor on the failing column with the condition above 0. The monitor runs on a schedule and posts to Slack or email when it trips.

PostgreSQL notes

Postgres folds unquoted names to lower case, so a table created as "Orders" has to be quoted every time. The generator quotes a name only when it has capitals, spaces or a reserved word, and says so under the check.

Time math uses now() and interval '30 hours'. If your column is timestamp without time zone, now() is compared in the session time zone, so store UTC and run the session in UTC to avoid a check that is off by your offset.

Every check is a plain SELECT with no extensions, so it runs the same on RDS, Aurora, Supabase, Neon and self-hosted Postgres.

Connecting PostgreSQL to arcodash is covered on the PostgreSQL connector page.

Questions

Is anything sent anywhere?

No. The SQL is generated by JavaScript in your browser and nothing you type leaves the page. The link you can copy keeps your settings in the part of the address after the #, which browsers never send to a server.

Why do the checks return zero rows when everything is fine?

Each check selects only the problems: a stale table, a count that moved too far, a column with too many nulls, a duplicated key. No problems means no rows, which is also how dbt tests and most scheduler checks read a result. If your tool needs a single value, switch to One number and the check returns a count of failing rows instead.

Why does the row count check need 28 days of history?

It compares the last 24 hours with the average of the same weekday over the previous 4 weeks, so a quiet Sunday is judged against other Sundays instead of a busy Tuesday. With less than 4 weeks of data the average is built from fewer days and the check is noisier.

What about time zones?

The checks compare your timestamp column with the database clock. If the column stores UTC and the session runs in another zone, the freshness threshold shifts by the offset. Each dialect page lists what to set so the two agree.

How do I get alerted when a check fails?

Run the SQL on a schedule and act on the result. A cron job, a dbt test or an Airflow task works. In arcodash, save the check as a query, wrap it as SELECT COUNT(*) AS failing FROM (<check>) t, and add a monitor that alerts Slack or email when failing is above 0.

get started

Trust the number. Try it on your data.

The demo is a live dashboard with its checks running. The trial is the full product for 14 days, no card.

querying in minutesunlimited seats