Worked examples

Ten questions. Ten SQL patterns.

A useful prompt specifies the rows, metric, time range and output shape. Follow each question into SQL and see which assumptions change the answer.

Try TTSQL free

The sample schema and business rules

customers(id INTEGER PRIMARY KEY, name TEXT, country TEXT); orders(id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), ordered_at TIMESTAMP, status TEXT, total DECIMAL(12,2)). Each order has one customer. total is the order amount in a single reporting currency; paid means recognized revenue in this teaching dataset. Timestamps use UTC. These are example tables, not tables TTSQL creates.

  • Order totals already include the full order amount; do not sum them after a one-to-many items join.
  • All examples are SELECT queries. Replace these fictional identifiers and business rules with your actual schema.
  • The PostgreSQL examples are checked against isolated test fixtures, including a month boundary, a timestamp tie, cancelled orders and a customer with no orders.

Write the result contract before the query

Start with the unit of output: one row per order, customer, country or month. Name the measure and exclusions next. 'Revenue' might mean paid order value, invoice value or net receipts after refunds; the schema alone cannot settle that choice.

Use a fixed date range when reproducing an example. Then add an explicit ordering and a tie-breaker. A LIMIT without a deterministic ORDER BY gives a sample, not a reliable top list.

Move from a lesson to your project

TTSQL generates SQL using your project's database type and schema context. Review the generated joins, filters and aggregation before choosing Run in the workspace. POST /api/v1 generates SQL and returns executed: false; it does not run these examples.

PostgreSQL DISTINCT ON, FILTER and ::date casts do not transfer unchanged to every database. Use the dialect guides for the equivalent date, ranking and conditional aggregation patterns.

Worked SQL examples

Read the sample schema and assumptions alongside each query. Adapt names, dates, and definitions to your own database before running SQL.

01 / PostgreSQL

1. Filter a complete calendar month

List paid orders placed in January 2026, with their IDs and totals, ordered by ID.

SELECT id, total
FROM orders
WHERE status = 'paid'
  AND ordered_at >= TIMESTAMP '2026-01-01 00:00:00'
  AND ordered_at < TIMESTAMP '2026-02-01 00:00:00'
ORDER BY id;

The inclusive start and exclusive end include every January timestamp and exclude midnight on February 1. BETWEEN with a January 31 date would miss most of the last day. The status predicate defines which orders count; no date function is applied to the filtered column.

Expected result

One row per paid January order; later months and cancelled orders are excluded.
02 / PostgreSQL

2. Join customers to paid revenue

Show total paid order revenue by customer country, highest revenue first; break ties by country.

SELECT c.country, SUM(o.total) AS revenue
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid'
GROUP BY c.country
ORDER BY revenue DESC, c.country;

The join follows the foreign key from orders to customers. Each order contributes once because customers.id is unique. Adding an order-items join here would multiply order totals unless items were aggregated first. Countries with no paid orders are absent.

03 / PostgreSQL

3. Keep customers with zero purchases

For every customer, count paid orders, including customers with zero. Sort by customer ID.

SELECT c.id, c.name, COUNT(o.id) AS paid_orders
FROM customers AS c
LEFT JOIN orders AS o
  ON o.customer_id = c.id AND o.status = 'paid'
GROUP BY c.id, c.name
ORDER BY c.id;

The paid filter belongs in ON so the left join preserves non-buyers. COUNT(o.id) counts matching orders; COUNT(*) would count the placeholder row and incorrectly report one for a customer with no paid orders.

04 / PostgreSQL

4. Filter an aggregate with HAVING

Find customers with at least two paid orders across all time. Return customer ID and paid order count, ordered by customer ID.

SELECT customer_id, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING COUNT(*) >= 2
ORDER BY customer_id;

WHERE removes unpaid rows before counting; HAVING removes customer groups after counting. State the reporting period when asking for repeat customers: this example deliberately uses all recorded history.

05 / PostgreSQL

5. Find missing relationships

List customers who have never placed any order, including cancelled orders when checking existence. Sort by ID.

SELECT c.id, c.name
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1 FROM orders AS o WHERE o.customer_id = c.id
)
ORDER BY c.id;

NOT EXISTS asks whether any related row exists. It avoids the NULL trap of NOT IN and differs from 'no paid orders': a customer with only a cancelled order does not qualify.

06 / PostgreSQL

6. Count unique purchasing customers

Count distinct customers with a paid order in each calendar month of the stored UTC timestamps. Sort by month.

SELECT DATE_TRUNC('month', ordered_at)::date AS month,
       COUNT(DISTINCT customer_id) AS purchasing_customers
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;

COUNT(DISTINCT customer_id) counts a customer once within each month even when that customer places several paid orders. COUNT(*) would count orders instead. Monthly distinct counts cannot be summed to obtain all-time distinct customers: a repeat buyer can appear in several months. NULL customer IDs are excluded by COUNT(DISTINCT), although the teaching relationship expects every order to have a customer.

07 / PostgreSQL

7. Aggregate revenue by month

Show paid revenue by calendar month in the stored UTC timestamps, oldest month first.

SELECT DATE_TRUNC('month', ordered_at)::date AS month,
       SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;

DATE_TRUNC buckets timestamps into months and the date cast gives a readable month-start value. This returns months containing paid orders. For a chart that must show zero-activity months, join the aggregate to a calendar table or generate_series.

08 / PostgreSQL

8. Calculate cumulative revenue

Show cumulative paid revenue by month, starting with the first recorded month.

WITH monthly AS (
  SELECT DATE_TRUNC('month', ordered_at)::date AS month,
         SUM(total) AS revenue
  FROM orders
  WHERE status = 'paid'
  GROUP BY 1
)
SELECT month,
       SUM(revenue) OVER (
         ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS cumulative_revenue
FROM monthly
ORDER BY month;

The CTE creates one row per month before the window runs. The explicit ROWS frame adds every prior month to the current month. Filtering to a year inside the CTE would reset the accumulation to that year; filtering outside would retain earlier history.

09 / PostgreSQL

9. Compare with a scalar subquery

List paid orders whose total exceeds the average paid order total across all time. Sort by order ID.

SELECT id, total
FROM orders
WHERE status = 'paid'
  AND total > (SELECT AVG(total) FROM orders WHERE status = 'paid')
ORDER BY id;

Both the outer query and the average use paid orders, so a cancelled high-value order cannot distort the benchmark. This is an order-weighted average, not an average of customer averages. AVG ignores NULL totals; define a NULL policy if your real schema allows them.

10 / PostgreSQL

10. Compare two complete reporting periods

Compare paid revenue in January and February 2026 using stored UTC timestamps. Show both totals, February minus January, and percentage change rounded to two decimals. Treat a month with no paid orders as zero; return NULL percentage change when January revenue is zero.

WITH periods AS (
  SELECT
    COALESCE(SUM(total) FILTER (
      WHERE ordered_at < TIMESTAMP '2026-02-01'
    ), 0) AS january_revenue,
    COALESCE(SUM(total) FILTER (
      WHERE ordered_at >= TIMESTAMP '2026-02-01'
    ), 0) AS february_revenue
  FROM orders
  WHERE status = 'paid'
    AND ordered_at >= TIMESTAMP '2026-01-01'
    AND ordered_at < TIMESTAMP '2026-03-01'
)
SELECT january_revenue, february_revenue,
       february_revenue - january_revenue AS revenue_change,
       ROUND(100.0 * (february_revenue - january_revenue)
         / NULLIF(january_revenue, 0), 2) AS percent_change
FROM periods;

The half-open range includes both complete months and excludes March 1. Conditional SUM separates January and February without dropping the output row when either period is absent; COALESCE applies the explicit no-orders-means-zero policy. NULLIF makes growth from a zero January base undefined (NULL), rather than a division error or a fabricated percentage. The sample totals are in one reporting currency and are assumed known: missing amounts or an incomplete data load require investigation, not automatic replacement with zero. Compare complete periods with the same recognition rules and currency; month totals also reflect the different numbers of days, so request daily averages if that is the intended metric.

Answers before you connect a database.

Can I paste these examples into my database?

Only after matching the sample tables, columns, data types and business rules to your schema. The examples target PostgreSQL; dialect-specific guides cover other engines.

Why do similar English questions produce different SQL?

Changing 'every customer' to 'customers with paid orders' changes which rows must survive the join. Dates, NULL rules, ties and revenue definitions are part of the question, even when the wording leaves them implicit.

Bring your next question to TTSQL.

Start with one PostgreSQL project and 20 free requests a day. Explore your data, build a report, or integrate the API.