SQL Explainer Examples
Examples use PostgreSQL syntax. The same explanations apply across all 28 supported databases β MySQL, SQL Server, Oracle, BigQuery, Snowflake, and more.
1. Filter and sort a product list
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; π οΈ Breakdown
-- 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
| # | 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. |
π― Summary
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.
π Returned result
| product_id | name | price | stock_quantity |
|---|---|---|---|
| 101 | 4K OLED TV 65" | 2499.99 | 5 |
β Returned types
| product_id | name | price | stock_quantity |
|---|---|---|---|
| integer | text | numeric | integer |
2. Count orders per user with HAVING
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; π οΈ Breakdown
-- 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
| # | 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). |
π― Summary
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.
π Returned result
| column 1 | column 2 |
|---|---|
| alice | 12 |
| bob | 7 |
| carol | 4 |
β Returned types
| column 1 | column 2 |
|---|---|
| text | bigint |
3. Rank top products by revenue with a CTE
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; π οΈ Breakdown
-- 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
| # | 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). |
π― Summary
Return the top 5 products by total sales revenue, showing each product's name and aggregated revenue computed from order_items.
π Returned result
| name | total_revenue |
|---|---|
| SuperWidget | 12500.75 |
| MegaGadget | 9875.00 |
| BasicWidget | 5400.00 |
| ProThing | 4320.50 |
| MiniGizmo | 3100.00 |
β Returned types
| name | total_revenue |
|---|---|
| text | numeric |
4. Top-N per category with a window function
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; π οΈ Breakdown
-- 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
| # | 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. |
π― Summary
Returns products (name, category, price) that are ranked first or second by price within each category; ties at a rank are included.
π Returned result
| 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 |
β Returned types
| name | category | price |
|---|---|---|
| TEXT | TEXT | NUMERIC |
5. Monthly revenue with date bucketing
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; π οΈ Breakdown
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
| # | 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. |
π― Summary
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.
π Returned result
| sales_month | monthly_revenue |
|---|---|
| 2023-01-01 | 12500.75 |
| 2023-02-01 | 9780.50 |
| 2023-03-01 | 14320.00 |
β Returned types
| sales_month | monthly_revenue |
|---|---|
| date | numeric |
6. Expand JSON tags with CROSS JOIN LATERAL
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'; π οΈ Breakdown
-- 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
| # | 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'. |
π― Summary
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.
π Returned result
| product_name | username | comment | tag |
|---|---|---|---|
| Wireless Mouse X | jdoe | Excellent battery life and comfort. | ergonomic |
β Returned types
| product_name | username | comment | tag |
|---|---|---|---|
| text | text | text | text |
Try these on your own queries
Generate, optimize, validate, explain, and format SQL with the SQLAI.ai tools β across every supported database.