# SQL Query Explanation ## 1. Plain-English Summary This query returns the **top 10 customers who spent more than $500 in the last 30 days**, showing their names and total spending, sorted from highest to lowest spender. ## 2. Step-by-Step Walkthrough 1. **CTE (`WITH recent_orders AS ...`)**: Creates a temporary, named result set called `recent_orders`. Think of it as a mini-table built on the fly to be used later in the main query. 2. **Inside the CTE — `SELECT customer_id, SUM(total) AS spend FROM orders`**: Pulls each customer's ID and adds up their order totals into a column named `spend`. 3. **`WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'`**: Filters orders to only those placed within the last 30 days from today. 4. **`GROUP BY customer_id`**: Collapses all orders per customer into a single row so `SUM(total)` produces one spend value per customer. 5. **Main `SELECT c.name, r.spend`**: Chooses the customer's name and their 30-day spend as the final output columns. 6. **`FROM customers c JOIN recent_orders r ON c.id = r.customer_id`**: Links the customers table to the CTE so each spend value gets paired with the correct customer name. Customers with no recent orders are excluded (inner join). 7. **`WHERE r.spend > 500`**: Keeps only customers whose recent spending exceeds $500. 8. **`ORDER BY r.spend DESC`**: Sorts results so the biggest spenders appear first. 9. **`LIMIT 10`**: Returns only the top 10 rows. ## 3. Tables & Columns - **`orders`**: `customer_id`, `total`, `order_date` — used to calculate per-customer spending over the last 30 days. - **`customers`**: `id`, `name` — used to look up the human-readable customer name. - **Output**: `name` (customer's name), `spend` (total spent in last 30 days). ## 4. Watch Outs - **"Last 30 days" is rolling**: `CURRENT_DATE - INTERVAL '30 days'` changes daily, so results are not reproducible on a later date. - **Time zones**: `CURRENT_DATE` depends on the database server's time zone, which may not match your users'. - **Inner join excludes inactive customers**: Customers with zero recent orders won't appear — intentional here, but worth noting. - **Ties at rank 10**: If multiple customers have identical spend values near the cutoff, `LIMIT 10` picks arbitrarily; add a tiebreaker (e.g., `ORDER BY r.spend DESC, c.name`) for determinism. - **Performance**: An index on `orders(order_date)` or `orders(customer_id, order_date)` will greatly speed up the CTE on large datasets. - **NULL totals**: If `total` can be NULL, `SUM` ignores them — usually fine, but verify this matches business expectations.
AI Tools to Explain Complex SQL Queries Step by Step
Tested prompts for explain sql query with ai compared across 5 leading AI models.
You have a SQL query in front of you and you are not sure what it actually does. Maybe it was written by someone else, inherited from legacy code, or pulled from a database you just started working with. You need to understand it before you run it, modify it, or explain it to a teammate. That is exactly the problem AI tools solve here.
Pasting a SQL query into an AI model and asking for a plain-English explanation gives you a line-by-line breakdown in seconds. Instead of reverse-engineering JOIN logic or tracing nested subqueries manually, the AI walks you through each clause, explains what data is being filtered or aggregated, and flags anything that looks unusual or potentially slow.
This page tests that workflow directly. We used a real complex query as the input, ran it through four leading AI models, and compared the quality of their explanations. If you are trying to understand a SQL query faster, you are in the right place. The results and a prompt you can reuse are below.
When to use this
Using AI to explain SQL is the right move when the query is long, unfamiliar, or dense with joins, subqueries, window functions, or CTEs. It is also useful when you need to communicate what a query does to a non-technical stakeholder or when you are onboarding to a codebase and need to get up to speed quickly.
- You inherited a stored procedure or view with no documentation and need to understand its logic before modifying it
- You are onboarding to a new data warehouse and need to understand dozens of existing queries quickly
- A query is returning unexpected results and you want a plain-English walkthrough to spot the logic error
- You need to explain what a report-generating query does to a product manager or business stakeholder
- You are learning SQL and want a senior-level explanation of why a query is written the way it is
When this format breaks down
- The query references proprietary table or column names that contain sensitive business logic or PII you cannot paste into a third-party AI tool
- You need a performance audit rather than a logic explanation. AI can describe what a query does but it cannot replace EXPLAIN ANALYZE output from your actual database engine
- The query is simple enough that reading it takes less time than writing a prompt. A three-line SELECT with one WHERE clause does not need AI help
- Your organization has a strict data governance policy that prohibits sending schema details or query structures to external services
The prompt we tested
You are an expert SQL tutor and database engineer. Explain the following SQL query in plain English so that a non-expert can understand what it does, how it works, and why each part matters. Follow these rules strictly: Structure the output as: (1) a one-sentence plain-English summary of what the query returns, (2) a numbered step-by-step walkthrough of each clause (SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, subqueries, CTEs) explaining its purpose, (3) a short 'Tables & Columns' section listing what data is being pulled, and (4) a 'Watch Outs' section noting performance concerns, edge cases, or ambiguous logic. Keep language simple, avoid unexplained jargon, and stay under 400 words. SQL query to explain: WITH recent_orders AS (SELECT customer_id, SUM(total) AS spend FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY customer_id) SELECT c.name, r.spend FROM customers c JOIN recent_orders r ON c.id = r.customer_id WHERE r.spend > 500 ORDER BY r.spend DESC LIMIT 10; Produce the explanation now.
Model comparison
| Model | Quality | Cost | Latency | Tokens |
|---|---|---|---|---|
| Claude Opus 4.7 | 9 | $0.0839 | 14.5s | 450 / 1029 |
| Claude Haiku 4.5 | 9 | $0.0031 | 5.4s | 292 / 555 |
| GPT-5.4 | 9 | $0.0099 | 9.6s | 252 / 621 |
| Gemini 2.5 Pro | 9 | $0.0030 | 16.9s | 267 / 534 |
| Grok 4.1 Fast Reasoning | 9 | $0.0003 | 7.8s | 399 / 443 |
Outputs
# SQL Query Explanation ## One-Sentence Summary This query finds the 10 customers who spent the most money in the last 30 days, but only those who spent more than $500, showing their names and total spending. --- ## Step-by-Step Walkthrough **1. WITH recent_orders AS (...)** — *Common Table Expression (CTE)* This creates a temporary named dataset called "recent_orders" that you'll use later. Think of it as a helper table built just for this query. **2. SELECT customer_id, SUM(total) AS spend** Inside the CTE, grab each customer's ID and add up all their order amounts, naming that sum "spend." **3. FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'** Look only at orders from the last 30 days. Orders older than that are ignored. **4. GROUP BY customer_id** Bundle all orders together by customer, so each customer appears once with their total spending. **5. SELECT c.name, r.spend** — *Main query's SELECT* From the final result, show only the customer's name and their spending total. **6. FROM customers c** Start with the customers table (nicknamed "c"). **7. JOIN recent_orders r ON c.id = r.customer_id** Connect each customer to their spending data by matching customer IDs. Only customers with recent orders appear. **8. WHERE r.spend > 500** Filter to keep only customers whose 30-day spending exceeds $500. **9. ORDER BY r.spend DESC** Sort results highest to lowest spending (DESC = descending). **10. LIMIT 10** Show only the top 10 rows. --- ## Tables & Columns - **orders**: order_date, total, customer_id - **customers**: id, name --- ## Watch Outs - **No results?** If no customers spent >$500 in 30 days, the query returns nothing. - **Ties at rank 10**: If multiple customers tied for 10th place, only one appears due to LIMIT. - **NULL names**: If a customer record has no name, it still appears in results (unless you add `WHERE c.name IS NOT NULL`). - **Performance**: Large orders tables may slow down; consider adding an index on `order_date` and `customer_id`.
**1) One-sentence summary**
This query finds the **top 10 customers who spent more than 500 in the last 30 days**, showing each customer’s name and their total spending, sorted from highest spender to lowest.
**2) Step-by-step walkthrough**
1. **CTE (`WITH recent_orders AS (...)`)**
This creates a temporary named result called `recent_orders` that the main query can use.
Think of it as: “First, make a small summary table of recent customer spending.”
2. **Inside the CTE: `SELECT customer_id, SUM(total) AS spend`**
For each customer, it calculates the **total amount spent** by adding up (`SUM`) their order totals.
The result is labeled `spend`.
3. **Inside the CTE: `FROM orders`**
The data comes from the `orders` table, which stores customer purchases.
4. **Inside the CTE: `WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'`**
Only orders from the **last 30 days** are included.
This filters out older purchases.
5. **Inside the CTE: `GROUP BY customer_id`**
Orders are grouped by customer so the query can calculate one total spending number per customer.
6. **Main query: `SELECT c.name, r.spend`**
The final output shows:
- the customer’s name (`c.name`)
- their recent total spending (`r.spend`)
7. **Main query: `FROM customers c`**
Starts with the `customers` table, using `c` as a short alias.
8. **Main query: `JOIN recent_orders r ON c.id = r.customer_id`**
Combines customer names with the spending totals from the CTE.
This matches each customer record to their recent spending summary.
9. **Main query: `WHERE r.spend > 500`**
Keeps only customers whose total spending in the last 30 days is **more than 500**.
10. **Main query: `ORDER BY r.spend DESC`**
Sorts the results by spending, from **highest to lowest**.
11. **Main query: `LIMIT 10`**
Returns only the **top 10** matching customers.
**3) Tables & Columns**
- **`orders`**: `customer_id`, `total`, `order_date`
- **`customers`**: `id`, `name`
**4) Watch Outs**
- Customers with **no orders in the last 30 days** will not appear.
- If two customers have the same spending, the order between them is not guaranteed unless another sort rule is added.
- Performance may depend on indexes, especially on `orders.order_date` and `orders.customer_id`.
- `CURRENT_DATE` uses the database’s date/time settings, which may affect the exact 30-day window.
This query finds the names and total spending of the top 10 customers who have spent over $500 in the last 30 days. ### Step-by-Step Walkthrough 1. **`WITH recent_orders AS (...)`**: First, the query creates a temporary, virtual table named `recent_orders`. This is done by looking at the `orders` table, filtering for orders placed in the last 30 days, and then calculating the total amount spent (`spend`) for each unique customer. 2. **`FROM customers c JOIN recent_orders r ON c.id = r.customer_id`**: The main query then takes the `customers` table (nicknamed `c`) and connects it to our temporary `recent_orders` table (nicknamed `r`). It matches rows between the two where the customer ID is the same, effectively linking each customer's name to their recent spending amount. 3. **`WHERE r.spend > 500`**: After joining, it filters the results to only include customers whose total recent `spend` is greater than $500. 4. **`SELECT c.name, r.spend`**: It specifies the final columns to display: the customer's `name` from the `customers` table and their calculated `spend` from our temporary table. 5. **`ORDER BY r.spend DESC`**: It sorts the filtered list of customers, placing the highest spenders at the top (`DESC` means descending order). 6. **`LIMIT 10`**: Finally, it restricts the output to only the top 10 rows from that sorted list. ### Tables & Columns * **`customers` table**: Uses the `id` and `name` columns. * **`orders` table**: Uses the `customer_id`, `total`, and `order_date` columns. ### Watch Outs * **Performance**: If the `orders` table is massive, calculating the sum for recent orders can be slow. An index on the `order_date` column would significantly speed this up. * **Timezones**: `CURRENT_DATE` uses the database server's timezone. This might lead to an inaccurate 30-day window if your users and server are in different timezones. * **Logic**: Only customers with orders in the last 30 days are considered. A loyal customer who spent thousands but hasn't ordered recently won't appear in the results.
1. This query returns the names and total spending amounts of the top 10 customers who spent more than $500 on orders in the last 30 days, sorted from highest to lowest spend. 2. 1. **CTE (WITH recent_orders AS ...)**: Creates a temporary "helper table" named recent_orders that calculates total spending per customer from the last 30 days; it's like pre-computing data to make the main query simpler. 2. **Inside CTE - SELECT customer_id, SUM(total) AS spend**: Picks customer IDs and adds up all order totals for each, naming the sum "spend". 3. **Inside CTE - FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'**: Pulls data only from the orders table for orders placed 30 days ago or more recently (CURRENT_DATE is today). 4. **Inside CTE - GROUP BY customer_id**: Groups rows by customer to sum spends correctly per person. 5. **SELECT c.name, r.spend**: Chooses customer names and their spend totals for the final output. 6. **FROM customers c JOIN recent_orders r ON c.id = r.customer_id**: Starts with the customers table (aliased "c"), links it to the CTE (aliased "r") using matching customer IDs. 7. **WHERE r.spend > 500**: Filters to show only customers whose 30-day spend exceeds $500. 8. **ORDER BY r.spend DESC**: Sorts results by spend from highest to lowest. 9. **LIMIT 10**: Keeps only the top 10 rows. 3. **Tables & Columns**: - customers: id, name - orders: customer_id, total, order_date - Pulls: customer names and summed 30-day totals. 4. **Watch Outs**: Needs indexes on orders.order_date and orders.customer_id for speed on large tables. Customers with no recent orders vanish (no JOIN match). "30 days" excludes partial days if run at midnight; test date ranges. Assumes PostgreSQL (INTERVAL syntax). (248 words)
What makes these work
-
01Specify the explanation format you need
Asking the AI to explain a query without direction gives you a generic paragraph. Instead, ask for a clause-by-clause breakdown, a plain-English summary for a non-technical audience, or a numbered walkthrough. The format you request directly controls how usable the output is for your actual situation.
-
02Ask about performance and edge cases explicitly
AI models will explain what a query does but they will not always volunteer that it has a performance problem or a subtle logical bug unless you ask. Add a line like 'flag any performance concerns or logic issues you notice' to your prompt. This turns a simple explainer into a lightweight code review.
-
03Provide table schema when available
If you can safely paste in the relevant CREATE TABLE statements or describe the column types, the AI explanation becomes significantly more accurate. Knowing that order_status is an ENUM versus a free-text VARCHAR, for example, changes how the AI interprets a WHERE clause filtering on it.
-
04Use follow-up prompts to go deeper
Start with a broad explanation request, then drill in. After the initial response, ask 'why is the LEFT JOIN used here instead of INNER JOIN' or 'what would happen if I removed the HAVING clause.' Iterative prompting extracts more value than trying to ask everything in one go.
More example scenarios
Explain this SQL query step by step. Tell me what data it pulls, what each JOIN does, and what the final result set looks like. SELECT c.customer_id, c.name, SUM(o.total_amount) AS lifetime_value, COUNT(o.order_id) AS order_count FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= '2023-01-01' GROUP BY c.customer_id, c.name HAVING SUM(o.total_amount) > 500 ORDER BY lifetime_value DESC;
This query calculates the lifetime value and order count for customers who placed orders after January 1, 2023 and spent more than $500 total. It LEFT JOINs customers to orders, groups by customer, filters post-aggregation with HAVING, and returns results ranked from highest to lowest spender. Note: the LEFT JOIN combined with a WHERE on order_date effectively becomes an INNER JOIN, potentially excluding customers with no orders.
Explain what this SQL does, especially the window function part. I am not sure what PARTITION BY and ORDER BY are doing inside OVER(). SELECT user_id, event_date, revenue, SUM(revenue) OVER (PARTITION BY user_id ORDER BY event_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total FROM user_events;
The SUM() OVER() is a window function that calculates a running total of revenue per user. PARTITION BY user_id resets the running total for each new user. ORDER BY event_date ensures the sum accumulates in chronological order. The ROWS clause explicitly defines the frame as all rows from the start up to the current row. The result is each event row plus a cumulative revenue column for that user up to that date.
Can you explain this recursive CTE query in plain English? What is the base case, what does the recursive part do, and what does the final SELECT return? WITH RECURSIVE org_tree AS (SELECT id, name, manager_id, 0 AS depth FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, ot.depth + 1 FROM employees e JOIN org_tree ot ON e.manager_id = ot.id) SELECT * FROM org_tree ORDER BY depth, name;
This CTE builds a full organizational hierarchy. The base case selects the top-level employees with no manager. The recursive part repeatedly joins employees to the already-found rows, finding each person's direct reports and incrementing the depth counter. The final SELECT returns the entire org chart sorted by level then alphabetically. The depth column tells you how many levels down from the CEO each employee sits.
I need to explain this SQL to a non-technical product manager. Please describe what data this query returns in plain business terms, not technical jargon. SELECT p.product_name, cat.category_name, SUM(oi.quantity) AS units_sold, SUM(oi.quantity * oi.unit_price) AS gross_revenue FROM order_items oi JOIN products p ON oi.product_id = p.product_id JOIN categories cat ON p.category_id = cat.category_id GROUP BY p.product_name, cat.category_name ORDER BY gross_revenue DESC;
This query produces a sales performance report ranked by revenue. For every product, it shows which category it belongs to, how many units were sold, and total gross revenue. The data comes from combining order line items with product and category information. The result is sorted so the highest-earning products appear first. A product manager can use this to identify top performers and category trends.
Explain this SQL query and tell me if there are any performance concerns I should know about. SELECT e.name, e.salary FROM employees e WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE department_id = e.department_id);
This query finds employees earning above their own department's average salary. The subquery is correlated, meaning it re-runs once for every row in the outer query, comparing each employee to their specific department average. It is logically correct but can be slow on large tables because of repeated subquery execution. A more performant rewrite would use a CTE or window function to calculate department averages once, then join or filter against that result.
Common mistakes to avoid
-
Pasting the query with no context
Dropping a raw query into the chat with no instruction produces a response calibrated for a generic audience. The AI does not know if you are a beginner who needs every keyword defined or a senior engineer who just wants the business logic summarized. Always include your experience level and what you actually need from the explanation.
-
Trusting the explanation without verifying
AI models can misread complex queries, especially ones with multiple layers of nesting or dialect-specific syntax like SQL Server's TOP or Oracle's ROWNUM. If the query is critical, cross-check the AI explanation against the actual query output or run it against a test dataset. An explanation can sound confident and still be subtly wrong.
-
Ignoring dialect differences
SQL has meaningful syntax differences across MySQL, PostgreSQL, SQL Server, BigQuery, and others. If you do not tell the AI which dialect the query is written in, it may explain syntax as standard SQL when it is actually vendor-specific, leading to confusion if you try to adapt the query for a different platform.
-
Sending sensitive data to public AI tools
Real table and column names can reveal business logic, and in some cases the query itself contains or implies sensitive information about customers or financial data. Before pasting any query into a public AI model, check your organization's data handling policies and consider anonymizing table and column names if needed.
Related queries
Frequently asked questions
Can AI explain SQL queries accurately for all databases like MySQL, PostgreSQL, and SQL Server?
Most leading AI models have strong coverage of the major SQL dialects. Accuracy is highest for standard ANSI SQL and common dialects like PostgreSQL and MySQL. For less common or heavily vendor-specific syntax, explicitly telling the AI which database you are using improves the explanation significantly. Always name the dialect in your prompt.
Is it safe to paste my SQL queries into an AI tool?
It depends on what the query contains and which tool you are using. Queries that include schema structure, table names, or business logic may expose proprietary information. For enterprise use, check whether the AI provider offers a no-training data policy or use a self-hosted model. Anonymizing column and table names before pasting is a practical middle ground.
What is the best prompt to explain a SQL query with AI?
A high-performing prompt includes the query itself, your experience level, the SQL dialect, and what you want from the explanation. For example: 'I am a mid-level analyst. This is a PostgreSQL query. Explain it clause by clause in plain English and flag any performance issues.' Specificity consistently produces better output than a bare paste.
Can AI help me understand why a SQL query is slow?
AI can identify common patterns that cause slow queries, such as correlated subqueries, missing index hints, SELECT *, or inefficient JOIN ordering. However, it cannot replace actual execution plan analysis from your database engine. Use AI to get a fast first pass on likely issues, then confirm with EXPLAIN or EXPLAIN ANALYZE in your database.
Can AI explain SQL window functions and CTEs, or just simple queries?
Modern AI models handle advanced SQL constructs well, including window functions, recursive CTEs, lateral joins, and complex aggregations. These are actually areas where AI explanation adds the most value because the logic is hardest to read at a glance. Just include the full query and ask for the specific construct to be explained in detail.
Which AI model is best for explaining SQL queries?
GPT-4 class models and Claude perform well on complex SQL explanation tasks, with strong reasoning about query logic and clear prose output. The comparison table on this page shows side-by-side results from the leading models on an identical test query. The differences come down to explanation depth, how well they flag issues, and how readable the output is for non-technical audiences.
Try it with a real tool
Run this prompt in one of these tools. Affiliate links help keep Gridlyx free.