What does an SQL explainer provide?
An SQL explainer translates a query into plain language, including clause-level behavior, expected output, and common edge cases like join cardinality or grouping scope.
This page focuses on readable SQL explanations for real PostgreSQL query shapes, including filters, joins, CTEs, window functions, time grouping, and JSON expansion. Use these examples to understand query intent and communicate behavior clearly.
Each sample is rendered in structured markdown with original SQL, detailed explanations, summary notes, and expected output schema so teams can reuse explanations in docs and reviews.
Understand how SQL queries work with line-by-line explanations and result expectations.
Original SQL query
SELECT
product_id,
name,
price,
stock_quantity
FROM
products
WHERE
category = 'Electronics'
AND price > 500.00
AND stock_quantity > 0
ORDER BY
price DESC; Explainer output
-- 1. Project four columns to return basic product details: identifier, display name, price and available quantity:
SELECT
product_id,
name,
price,
stock_quantity
-- 2. Source data comes from the products table:
FROM
products
-- 3. Filter to only include items in the 'Electronics' category that cost more than 500.00 and have stock_quantity > 0 β i.e., highβvalue electronics that are currently available for sale:
WHERE
category = 'Electronics'
AND price > 500.00
AND stock_quantity > 0
-- 4. Sort results by price in descending order so the most expensive matching products appear first:
ORDER BY
price DESC;
| # | Explanation |
|---|---|
| 1 | The SELECT clause lists four columns to return: product_id (the product identifier), name (the display name), price (the product price), and stock_quantity (how many units are available). These are the fields the query will project for each matching row. |
| 2 | FROM products specifies the source table named products β the query reads rows from that table. |
| 3 | The WHERE clause applies three filters combined with AND: category = 'Electronics' restricts to items in the Electronics category; price > 500.00 keeps only items with price greater than 500.00; stock_quantity > 0 ensures only products that are currently in stock. Together this yields highβvalue Electronics that are available for sale. |
| 4 | ORDER BY price DESC sorts the returned rows by the price column in descending order, so the most expensive matching products appear first. |
Return identifier, name, price and available quantity for products in the products table that are Electronics, cost more than 500.00, and have stock on hand; present results with the most expensive items first.
| product_id | name | price | stock_quantity |
|---|---|---|---|
| 101 | 4K OLED TV 65" | 2499.99 | 5 |
| product_id | name | price | stock_quantity |
|---|---|---|---|
| integer | text | numeric | integer |
Original SQL query
SELECT
u.username,
COUNT(o.order_id) AS total_orders
FROM
users u
JOIN
orders o ON u.user_id = o.user_id
GROUP BY
u.user_id, u.username
HAVING
COUNT(o.order_id) > 3
ORDER BY
total_orders DESC; Explainer output
-- 1. Select the user's username and the aggregated count of their orders, aliased as total_orders:
SELECT
u.username,
COUNT(o.order_id) AS total_orders
-- 2. From the users table joined to orders using user_id; this is an inner join so only users with matching orders are included:
FROM
users u
JOIN
orders o ON u.user_id = o.user_id
-- 3. Group results by user (user_id and username) so the COUNT is per-user; HAVING filters to only include users with more than 3 orders:
GROUP BY
u.user_id, u.username
HAVING
COUNT(o.order_id) > 3
-- 4. Order the final result set by total_orders in descending order to list the most-active buyers first:
ORDER BY
total_orders DESC;
| # | Explanation |
|---|---|
| 1 | SELECT u.username, COUNT(o.order_id) AS total_orders: returns each user's username and the aggregated number of orders they have. COUNT(o.order_id) tallies non-NULL order_id values and is aliased as total_orders for readability. |
| 2 | FROM users u JOIN orders o ON u.user_id = o.user_id: performs an inner join between users and orders on the user_id key. Because it is an inner join, only users who have at least one matching order in orders will appear in the results. |
| 3 | GROUP BY u.user_id, u.username HAVING COUNT(o.order_id) > 3: GROUP BY collapses rows so the COUNT is computed per user. Both user_id and username are listed to satisfy grouping rules and uniquely identify each user. HAVING filters those groups after aggregation, keeping only users with more than 3 orders. |
| 4 | ORDER BY total_orders DESC: sorts the final result so users with the largest total_orders appear first (most-active buyers listed at the top). |
List usernames and their order counts for users who have placed more than three orders, showing only users with orders and sorted by highest order count first.
| column 1 | column 2 |
|---|---|
| alice | 12 |
| bob | 7 |
| carol | 4 |
| column 1 | column 2 |
|---|---|
| text | bigint |
Original SQL query
WITH ProductRevenue AS (
SELECT
product_id,
SUM(quantity * price_at_purchase) AS total_revenue
FROM
order_items
GROUP BY
product_id
)
SELECT
p.name,
pr.total_revenue
FROM
products p
JOIN
ProductRevenue pr ON p.product_id = pr.product_id
ORDER BY
pr.total_revenue DESC
LIMIT 5; Explainer output
-- 1. Defines a CTE named ProductRevenue that calculates total_revenue per product by summing quantity * price_at_purchase from order_items:
WITH ProductRevenue AS (
SELECT
product_id,
SUM(quantity * price_at_purchase) AS total_revenue
FROM
order_items
GROUP BY
product_id
)
-- 2. Main SELECT pulls the product name and the computed total_revenue from the products table joined to the CTE:
SELECT
p.name,
pr.total_revenue
FROM
products p
-- 3. The JOIN is an inner join: only products that appear in order_items (have sales) will be included:
JOIN
ProductRevenue pr ON p.product_id = pr.product_id
-- 4. Results are ordered by total_revenue descending to rank products by revenue:
ORDER BY
pr.total_revenue DESC
-- 5. LIMIT 5 returns only the top five revenue-generating products; note that ties in revenue have no deterministic secondary ordering:
LIMIT 5;
| # | Explanation |
|---|---|
| 1 | The CTE ProductRevenue computes total_revenue per product by aggregating order_items: it multiplies quantity * price_at_purchase for each order item and SUMs those values, grouping by product_id. Only product_ids present in order_items appear in this result. |
| 2 | The main SELECT returns the product name (p.name) and the precomputed total_revenue (pr.total_revenue) by reading products (alias p) and the CTE (alias pr). |
| 3 | The JOIN is an inner join (JOIN ... ON p.product_id = pr.product_id), so only products that have matching rows in the ProductRevenue CTE (i.e., have at least one sale in order_items) are included. |
| 4 | ORDER BY pr.total_revenue DESC sorts the output from highest revenue to lowest, ranking products by their revenue. |
| 5 | LIMIT 5 restricts the output to the top five rows. If multiple products share the same total_revenue, their relative order is not deterministic unless you add a secondary ORDER BY (e.g., product_id or name). |
Return the top 5 products by total sales revenue, showing each product's name and aggregated revenue computed from order_items.
| name | total_revenue |
|---|---|
| SuperWidget | 12500.75 |
| MegaGadget | 9875.00 |
| BasicWidget | 5400.00 |
| ProThing | 4320.50 |
| MiniGizmo | 3100.00 |
| name | total_revenue |
|---|---|
| text | numeric |
Original SQL query
WITH RankedProducts AS (
SELECT
name,
category,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) as price_rank
FROM
products
)
SELECT
name,
category,
price
FROM
RankedProducts
WHERE
price_rank <= 2; Explainer output
-- 1. Defines a CTE named RankedProducts that will hold product rows augmented with a computed price_rank:
WITH RankedProducts AS (
-- 2. Selects name, category, price and computes price_rank using RANK() OVER (PARTITION BY category ORDER BY price DESC) β ranking products within each category by descending price (highest price gets rank 1):
SELECT
name,
category,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) as price_rank
-- 3. Reads rows from the products table to feed the window function; the CTE body ends here:
FROM
products
)
-- 4. Main query selects name, category, price from the CTE (RankedProducts):
SELECT
name,
category,
price
FROM
RankedProducts
-- 5. Filters to keep only the top two ranks per category (price_rank <= 2). Note: because RANK() is used, ties at a rank will include all tied rows and can return more than two products for a category:
WHERE
price_rank <= 2;
| # | Explanation |
|---|---|
| 1 | The WITH clause defines a CTE (common table expression) named RankedProducts β a temporary, named result set that the main query can read from. It will contain product rows plus a computed price_rank column. |
| 2 | Inside the CTE we SELECT name, category, price and compute price_rank with RANK() OVER (PARTITION BY category ORDER BY price DESC). This ranks products within each category by descending price so the highest-priced product(s) get rank 1. RANK() gives equal ranks to ties and can leave gaps in subsequent ranks. |
| 3 | The FROM products clause supplies the rows the window function processes; the window (partitioning and ordering) is applied across those product rows. The CTE body ends after this SELECT. |
| 4 | The main query reads from the CTE (RankedProducts) and selects the name, category, and price columns produced by the CTE. |
| 5 | The WHERE price_rank <= 2 filter keeps only rows whose rank within their category is 1 or 2. Because RANK() preserves ties, categories with tied prices at rank 2 can return more than two products. |
Returns products (name, category, price) that are ranked first or second by price within each category; ties at a rank are included.
| name | category | price |
|---|---|---|
| Phone X | Electronics | 999.99 |
| Phone Y | Electronics | 899.99 |
| Phone Z | Electronics | 899.99 |
| Novel A | Books | 19.99 |
| Novel B | Books | 14.99 |
| Toaster | Appliances | 29.99 |
| Kettle | Appliances | 29.99 |
| name | category | price |
|---|---|---|
| TEXT | TEXT | NUMERIC |
Original SQL query
SELECT
DATE_TRUNC('month', o.order_date)::date AS sales_month,
SUM(oi.quantity * oi.price_at_purchase) AS monthly_revenue
FROM
orders o
JOIN
order_items oi ON o.order_id = oi.order_id
WHERE
o.order_date >= '2023-01-01' AND o.order_date < '2024-01-01'
GROUP BY
sales_month
ORDER BY
sales_month; Explainer output
SELECT
-- 1. DATE_TRUNC('month', o.order_date)::date creates the month bucket (first day of the month) used as sales_month; SUM(oi.quantity * oi.price_at_purchase) computes revenue using the recorded price at the time of purchase (protects against later price changes):
DATE_TRUNC('month', o.order_date)::date AS sales_month,
SUM(oi.quantity * oi.price_at_purchase) AS monthly_revenue
FROM
-- 2. Inner JOIN between orders and order_items ensures only order items linked to an order are counted (orders without items are excluded). The join uses order_id to relate records:
orders o
JOIN
order_items oi ON o.order_id = oi.order_id
WHERE
-- 3. WHERE restricts to the calendar year 2023 using a half-open interval (>= '2023-01-01' AND < '2024-01-01'), which is index-friendly and avoids inclusive/exclusive endpoint ambiguity:
o.order_date >= '2023-01-01' AND o.order_date < '2024-01-01'
-- 4. GROUP BY aggregates revenue per sales_month and ORDER BY returns months in ascending order. Note: in PostgreSQL the SELECT alias (sales_month) is not visible to GROUP BY β use GROUP BY DATE_TRUNC('month', o.order_date)::date or GROUP BY 1 to avoid an error:
GROUP BY
sales_month
ORDER BY
sales_month;
| # | Explanation |
|---|---|
| 1 | DATE_TRUNC('month', o.order_date)::date β truncates each order_date to the first day of its month (e.g. 2023-03-15 -> 2023-03-01) and casts to date, producing the sales_month bucket. SUM(oi.quantity * oi.price_at_purchase) β multiplies each item's purchased quantity by the price recorded at purchase and sums those values, so revenue reflects the price at time of sale (protects against later price changes). |
| 2 | JOIN order_items oi ON o.order_id = oi.order_id β an inner join that keeps only rows where an order has matching order_items. Orders with no items are excluded. The relationship is made via the order_id key in both tables. |
| 3 | WHERE o.order_date >= '2023-01-01' AND o.order_date < '2024-01-01' β a half-open interval that selects all timestamps from the start of 2023 up to but not including the start of 2024. This pattern is index-friendly and avoids ambiguity about end-of-day inclusivity. |
| 4 | GROUP BY sales_month and ORDER BY sales_month β the query aggregates revenue per month and returns months ascending. Note: PostgreSQL does not allow referencing the SELECT alias (sales_month) directly in GROUP BY; use GROUP BY DATE_TRUNC('month', o.order_date)::date or GROUP BY 1 instead to avoid an error. |
The query computes total revenue per calendar month for orders placed in 2023 by summing quantity * price_at_purchase for each order item, grouping results by the month (first day of each month) and returning months in ascending order.
| sales_month | monthly_revenue |
|---|---|
| 2023-01-01 | 12500.75 |
| 2023-02-01 | 9780.50 |
| 2023-03-01 | 14320.00 |
| sales_month | monthly_revenue |
|---|---|
| date | numeric |
Original SQL query
SELECT
p.name AS product_name,
u.username,
r.comment,
tag_element.tag
FROM
reviews r
JOIN
products p ON r.product_id = p.product_id
JOIN
users u ON r.user_id = u.user_id
CROSS JOIN LATERAL
jsonb_array_elements_text(r.metadata -> 'tags') AS tag_element(tag)
WHERE
r.rating = 5
AND r.metadata ->> 'verified_purchase' = 'true'; Explainer output
-- 1. Selected output columns: product name, reviewer username, the review text, and a single tag value (tag_element.tag):
SELECT
p.name AS product_name,
u.username,
r.comment,
tag_element.tag
-- 2. Base table is reviews (aliased r). It is joined to products and users to retrieve product name and reviewer username via the foreign keys r.product_id -> products.product_id and r.user_id -> users.user_id:
FROM
reviews r
JOIN
products p ON r.product_id = p.product_id
JOIN
users u ON r.user_id = u.user_id
-- 3. CROSS JOIN LATERAL with jsonb_array_elements_text(r.metadata -> 'tags') expands the JSONB array stored at metadata->'tags' into one text row per tag. LATERAL lets the function reference the current review row (r), so each tag becomes its own output row:
CROSS JOIN LATERAL
jsonb_array_elements_text(r.metadata -> 'tags') AS tag_element(tag)
-- 4. Filters to include only 5-star reviews that are marked as a verified purchase. Uses ->> to extract the JSON text value of metadata['verified_purchase'] and compares it to the string 'true':
WHERE
r.rating = 5
AND r.metadata ->> 'verified_purchase' = 'true';
| # | Explanation |
|---|---|
| 1 | SELECT picks four output columns: p.name AS product_name returns the product's name with the alias product_name; u.username returns the reviewer's username; r.comment returns the review text; tag_element.tag returns a single tag value produced by the lateral JSON expansion. |
| 2 | FROM reviews r is the base table. Two inner JOINs pull related data: JOIN products p ON r.product_id = p.product_id uses the review's product_id to get the product row; JOIN users u ON r.user_id = u.user_id uses the review's user_id to get the reviewer row. |
| 3 | CROSS JOIN LATERAL jsonb_array_elements_text(r.metadata -> 'tags') AS tag_element(tag) calls a set-returning function on the current review's metadata->'tags' JSONB array. jsonb_array_elements_text returns each array element as text; LATERAL allows the function to reference the current r row, and CROSS JOIN produces one output row per tag for that review. The alias tag_element(tag) names the resulting column tag. |
| 4 | WHERE r.rating = 5 AND r.metadata ->> 'verified_purchase' = 'true' filters to only 5-star reviews that are marked as verified purchases. The ->> operator extracts the JSON value as text, so the comparison checks the text string 'true'. |
Returns one row per tag for reviews that are 5 stars and marked verified, showing the product name, reviewer username, the review comment, and a single tag.
| product_name | username | comment | tag |
|---|---|---|---|
| Wireless Mouse X | jdoe | Excellent battery life and comfort. | ergonomic |
| product_name | username | comment | tag |
|---|---|---|---|
| text | text | text | text |
Use this checklist when documenting SQL query behavior so explanations stay accurate, readable, and review-ready.
For a full workflow, pair this with the SQL validator, SQL formatter, and SQL optimizer.
An SQL explainer translates a query into plain language, including clause-level behavior, expected output, and common edge cases like join cardinality or grouping scope.
Yes. A clear explanation helps reviewers verify intent, catch logic mismatches, and confirm that filters, joins, and aggregates align with the product requirement.
Yes. Pair explanations with syntax validation, formatting, and execution-plan checks so correctness, readability, and performance are all covered before release.
Get started for free
Generate, validate, optimize, and explain queries in seconds. Bring your own schema, keep full control, and start free with no credit card required.