Real SQL, correct dialect, the watch type and threshold already picked. Copy one, adjust the table names, done.
24 ready-made Watch This recipes, six per warehouse. Each one is real SQL you can paste straight into a new Watch, with the type, condition, and frequency already picked. Copy the query, adjust the table and threshold to match your own schema, and set it up in a couple of minutes.
Your loader silently stopped, or a job upstream is failing, and nobody noticed because the table still has old rows in it.
SELECT DATEDIFF('minute', MAX(loaded_at), CURRENT_TIMESTAMP()) AS minutes_since_last_load
FROM ANALYTICS.RAW.ORDERS;
A streaming or batch load into your events table has gone quiet longer than it should.
SELECT TIMESTAMP_DIFF(CURRENT_TIMESTAMP(), MAX(loaded_at), MINUTE) AS minutes_since_last_load FROM `project.raw.events`;
A cron job or ETL step that's supposed to touch this table nightly never fired.
SELECT EXTRACT(EPOCH FROM (NOW() - MAX(loaded_at))) / 60 AS minutes_since_last_load FROM raw.orders;
An upstream ingestion job into a bronze/raw table has silently stopped writing new rows.
SELECT (unix_timestamp(current_timestamp()) - unix_timestamp(max(loaded_at))) / 60 AS minutes_since_last_load FROM main.raw.orders;
Today's row count for a table that should grow steadily has dropped hard, often a sign a filter or WHERE clause upstream broke.
SELECT COUNT(*) AS row_count FROM ANALYTICS.PUBLIC.ORDERS WHERE order_date = CURRENT_DATE();
A partitioned table's newest partition landed with far fewer rows than the partition before it.
SELECT COUNT(*) AS row_count FROM `project.dataset.shipments` WHERE ship_date = CURRENT_DATE();
A table that normally gains rows every check now shows the same or a shrinking count, which usually means the write path broke, not that growth stopped.
SELECT COUNT(*) AS row_count FROM public.signups WHERE created_at >= NOW() - INTERVAL '1 day';
A transform step feeding a silver table produced far fewer output rows than its usual run, suggesting a broken join or filter.
SELECT COUNT(*) AS row_count FROM main.silver.orders_clean WHERE process_date = current_date();
A merge or upsert step didn't dedupe correctly, and the same primary key now has more than one row.
SELECT COUNT(*) AS duplicate_key_count FROM ( SELECT order_id FROM ANALYTICS.PUBLIC.ORDERS GROUP BY order_id HAVING COUNT(*) > 1 );
A CDC or full-refresh load appended instead of replacing, leaving duplicate customer_id rows behind.
SELECT COUNT(*) AS duplicate_key_count FROM ( SELECT customer_id FROM `project.dataset.customers` GROUP BY customer_id HAVING COUNT(*) > 1 );
A dimension table that should have one row per natural key picked up duplicates from a bad backfill.
SELECT COUNT(*) AS duplicate_key_count FROM ( SELECT product_sku FROM public.products GROUP BY product_sku HAVING COUNT(*) > 1 ) d;
A MERGE INTO on event_id didn't fully dedupe, and the same event now appears more than once.
SELECT COUNT(*) AS duplicate_key_count FROM ( SELECT event_id FROM main.gold.events GROUP BY event_id HAVING COUNT(*) > 1 );
An upstream form or integration started dropping a required field, and a growing share of new rows have it null.
SELECT ROUND(100.0 * SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null
FROM ANALYTICS.PUBLIC.CUSTOMERS
WHERE created_at >= DATEADD('hour', -24, CURRENT_TIMESTAMP());
A tracking parameter that normally populates on nearly every row started coming through empty.
SELECT ROUND(100.0 * SUM(CASE WHEN utm_source IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null FROM `project.dataset.sessions` WHERE event_date = CURRENT_DATE();
A checkout flow change stopped requiring a field your fulfillment process depends on.
SELECT ROUND(100.0 * SUM(CASE WHEN shipping_address IS NULL THEN 1 ELSE 0 END) / COUNT(*), 2) AS pct_null FROM public.orders WHERE created_at >= NOW() - INTERVAL '1 day';
A mobile SDK update or app version stopped sending a device identifier your joins rely on.
SELECT round(100.0 * sum(case when device_id is null then 1 else 0 end) / count(*), 2) AS pct_null FROM main.silver.app_events WHERE event_date = current_date();
A payment processor or webhook is degraded, and failed orders are piling up faster than usual.
SELECT COUNT(*) AS failed_orders
FROM ANALYTICS.PUBLIC.ORDERS
WHERE status = 'failed'
AND created_at >= DATEADD('hour', -1, CURRENT_TIMESTAMP());
A recent deploy broke checkout for some segment of users, and error-status rows are climbing.
SELECT COUNT(*) AS failed_orders FROM `project.dataset.orders` WHERE status = 'error' AND TIMESTAMP_TRUNC(created_at, HOUR) = TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), HOUR);
A pricing bug or fulfillment problem is driving refunds up faster than the recent baseline.
SELECT COUNT(*) AS refunded_orders FROM public.orders WHERE status = 'refunded' AND created_at >= NOW() - INTERVAL '1 hour';
A pipeline stage is failing more often than usual, tracked from its own run-log table.
SELECT count(*) AS failed_jobs FROM main.ops.job_runs WHERE status = 'FAILED' AND run_ts >= current_timestamp() - INTERVAL 1 HOURS;
A slow sales day, or more often, a tracking or attribution bug undercounting revenue that already happened.
SELECT SUM(amount) AS daily_revenue FROM ANALYTICS.PUBLIC.ORDERS WHERE order_date = CURRENT_DATE() AND status = 'completed';
Revenue for the current hour is running well below the same hour on a normal day, an early warning before a full-day miss.
SELECT SUM(amount) AS hourly_revenue FROM `project.dataset.orders` WHERE TIMESTAMP_TRUNC(order_ts, HOUR) = TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), HOUR) AND status = 'completed';
Active subscription revenue fell below a floor you set, worth checking before it shows up in a monthly report.
SELECT SUM(monthly_amount) AS active_mrr FROM public.subscriptions WHERE status = 'active';
Your aggregated daily revenue table, built by a transform job, came in under the floor you expect on a normal day.
SELECT sum(amount) AS daily_revenue FROM main.gold.daily_revenue WHERE revenue_date = current_date();
Most write-ups of an alerting feature describe what it can do in the abstract: "watch a value, get notified when it crosses a threshold." That's true and mostly useless until you're staring at a blank Watch form wondering what query to actually type. Every recipe above is copy-paste-ready SQL for a real, specific problem, in the dialect you're actually running, with the watch type, condition, and check frequency already filled in from what's worked for that kind of check in practice.
Watch This has three watch types: Row count watches how many rows a query returns over time, A value watches a single number your query returns (a sum, a percentage, a count), and Any change fires whenever the result differs from the last check at all, useful for a query that should return the exact same thing every time. Conditions are Changed, Greater than, Less than, Equals, Increased by, and Decreased by, compared either against a fixed threshold or against the previous check's result depending on the condition. Frequency options are 5m, 15m, 30m, 1h, 4h, and 1d; the recipes above default to a frequency that matches how fast the underlying problem usually needs catching; a payment failure spike is checked every 15 minutes, a monthly revenue floor once a day.
Every table name above is realistic but made up: swap ANALYTICS.PUBLIC.ORDERS, project.dataset.orders, public.orders, or main.gold.daily_revenue for your own database, schema, and table names, and check that the column names (status, created_at, order_date, amount) match what your table actually calls those fields. Thresholds are starting points, not universal constants; a duplicate-key count watch is safe to copy verbatim since zero duplicates is zero duplicates everywhere, but a revenue floor or a null-percentage threshold needs tuning to your own normal range before it's useful instead of noisy.
A freshness check with too tight a threshold fires every time a job runs a few minutes late for an ordinary reason, and a team that gets paged for nothing three times a week stops trusting the alert within a month. Start thresholds looser than your instinct says, watch a week of real check history, then tighten once you know what normal actually looks like for that specific table, not what you assumed it would look like before you had data.
The SQL is realistic and correct for its dialect, but table and column names are illustrative. Swap in your own before saving the Watch.
"A value" watching a COUNT(*) with condition "Greater than 0" is usually the right shape: the query returns one number, and any non-zero result means something's wrong.
Frequency matches how fast the underlying problem needs catching. A payment failure spike is worth knowing about within 15 minutes; a monthly revenue floor doesn't need checking more than once a day.
The SQL syntax is dialect-specific, but the underlying pattern (freshness, row count, duplicates, nulls, spikes, thresholds) applies to any connector Watch This supports; you'd adapt the syntax to match.
Watch This sends to whatever destination you've configured for that Watch, including Slack and Teams, per QueryFlow's own destination options.
Watch This ships in every QueryFlow plan, with Slack and Teams destinations.
No credit card. 14 days. Cancel in one click.