Why Mastering Three Simple Moves Makes You a Better SQL Thinker
Hatched by Kai Nguyen
Apr 15, 2026
9 min read
9 views
88%
Can a handful of moves change how you think about data?
Most people learn SQL as a laundry list of commands: SELECT, JOIN, GROUP BY, ORDER BY, and a few functions. That approach teaches you to get answers. It rarely teaches you to ask smarter questions. But once you stop memorizing syntax and start noticing the patterns behind it, SQL becomes less like a toolset and more like a way of thinking about information.
This essay argues that almost every useful SQL query is built from three conceptual moves. Learn to recognize and combine these moves deliberately, and queries that once felt opaque become predictable, composable, and elegant. You will stop writing ad hoc queries and start designing readable, reusable logic that answers harder questions.
The three moves are: evaluate the local value, group and reduce, and contextualize across rows. Each map to familiar SQL constructs: expressions and operators, aggregates and GROUP BY, and window functions with PARTITION BY. When you chain these moves with named steps using CTEs, you get clarity, reuse, and the power to ask questions that are otherwise tricky.
The basic tension: local truth versus global story
At the heart of data work lies a tension between two kinds of truths. On one hand there is the single row truth: the value stored in a column for a particular record. On the other hand there is the aggregate truth: the pattern, summary, or ranking that shows how that row fits with others. SQL gives you tools for both. Learning to switch perspective is the skill that separates casual querying from strategic analysis.
An expression, in SQL terms, is anything that evaluates to a value. It is the atomic unit of local truth. Expressions include simple things like numeric literals and character strings, column references that yield the value stored in the current row, arithmetic, function calls, and operators for comparison and concatenation. You use expressions to answer a question such as: what is the price in this row, or what is the full name when I join first_name and last_name with string concatenation using the operator ||.
Aggregates collapse many rows into a smaller set of values. SUM, COUNT, AVG, MIN, and MAX are the familiar ones. They answer global questions such as: how much total revenue did this product generate, or what is the average session length for all users. GROUP BY tells SQL which rows belong to each aggregate bucket. But aggregates alone are blunt. They erase the row-level context that often matters for interpretation.
Window functions are the bridge. They compute values across a group of rows while keeping the original row intact. With PARTITION BY you define the group, and ordering lets you compute running totals, ranks, gaps, and positional metrics. Window functions let you ask: given this row, how does it compare to others in the same category, what is the cumulative value up to this row, and which rows are the top performers in their group?
Think of a spreadsheet: expressions are the formulas you write in a cell, aggregates are the summary sheet you create that collapses many rows, and window functions are like invisibly copying a summary back into each row so you can see the cell and its context simultaneously.
The Three Moves: a mental model for composition
Here is the framework I use when facing a data question. It is intentionally small and composable. Each move has clear syntax patterns and common pitfalls.
-
Evaluate the local value
- What expression transforms the raw column into the value I need? This includes casting, concatenation, arithmetic, and boolean logic.
- Key operators and ideas: equality and inequality, <, >, <=, >=, the BETWEEN shorthand for inclusive ranges, and wildcards in pattern matching where % matches zero or more characters and _ matches exactly one character.
- Use AS to rename expressions so the output reads like a sentence. Good names make the rest of the query easier to reason about.
Example: create a readable description for each sale.
SELECT order_id, product_id, quantity * unit_price AS line_total, customer_first_name || ' ' || customer_last_name AS customer_name FROM orders WHERE status = 'shipped'Local evaluation is fast and direct. It tells you the fact that lives in the row.
-
Group and reduce
- When the question asks for totals, averages, counts, minima, or maxima, you move from single rows to groups. GROUP BY is explicit about the bucketing. Aggregates look at many rows and return a single number per bucket.
- Common mistake: trying to select nonaggregated columns without grouping. The rule is simple: if you reference a column not in GROUP BY, it must appear inside an aggregate.
Example: total revenue per product.
SELECT product_id, SUM(quantity * unit_price) AS total_revenue FROM orders GROUP BY product_idAggregates tell you the global shape of the data. They are great for summaries, but they lose the per-row story.
-
Contextualize across rows
- Window functions put the summary back into the row context. Use PARTITION BY to define the group and ORDER BY to impose sequence. The function RANK, DENSE_RANK, ROW_NUMBER, SUM OVER, and others operate across the partition and return a value for each row.
- Window functions are ideal for ranking top performers in each category, computing running totals, or producing percentiles without collapsing rows.
Example: top 3 products by revenue within each category.
WITH product_revenue AS ( SELECT product_id, category_id, SUM(quantity * unit_price) AS revenue FROM orders GROUP BY product_id, category_id ) SELECT product_id, category_id, revenue, RANK() OVER (PARTITION BY category_id ORDER BY revenue DESC) AS revenue_rank FROM product_revenue WHERE revenue_rank <= 3Window functions give you the best of both worlds: the per-row detail and the aggregated context.
How to compose the moves: recipes that scale
Composing these moves is the practical skill. When you assemble them with names and steps, your queries become readable and debuggable. The common composition pattern is: compute local expressions, aggregate where needed, then bring contextual metrics back with window functions. Common table expressions, written with WITH, are the naming mechanism that allows this composition without nesting unreadable subqueries.
Concrete problem: find each salesperson's monthly revenue, their rank in the region, and whether they are above the regional median.
Step by step, using the three moves:
- Local evaluation: compute line totals and truncate the order date to the month.
- Group and reduce: sum the monthly revenue per salesperson per region.
- Contextualize: for each region and month compute rank and median, keeping per-salesperson rows.
SQL implementation:
WITH line_values AS (
SELECT
salesperson_id,
region_id,
DATE_TRUNC('month', order_date) AS month,
quantity * unit_price AS line_total
FROM orders
WHERE status = 'closed'
),
monthly_sales AS (
SELECT
salesperson_id,
region_id,
month,
SUM(line_total) AS month_revenue
FROM line_values
GROUP BY salesperson_id, region_id, month
)
SELECT
salesperson_id,
region_id,
month,
month_revenue,
RANK() OVER (PARTITION BY region_id, month ORDER BY month_revenue DESC) AS revenue_rank,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY month_revenue) OVER (PARTITION BY region_id, month) AS regional_median
FROM monthly_sales
ORDER BY region_id, month, revenue_rank
Notes on the recipe:
- The first CTE is a local transformation: it does not change the number of rows, it only computes an expression for each row. That keeps the logic easy to test.
- The second CTE is the group and reduce step. Aggregation is explicit and isolated. If the numbers look wrong, you can run just that CTE to inspect it.
- The final SELECT is the contextualization step where window functions annotate each row with rank and median values that refer to the row's partition.
This pattern makes complex questions tractable. Each step answers a smaller question that is easy to verify.
Common traps and how the three move model prevents them
Trap 1: trying to compute a running total with aggregation alone
If you attempt to use GROUP BY to compute a cumulative sum per salesperson over time, you will lose row-level identity and ordering. Window functions are the right tool for running totals because they maintain row identity while exposing the cumulative calculation across an ordered partition.
Trap 2: unreadable nested subqueries
Without naming steps, queries become nested mazes. Use WITH to name intermediate results. Name expressions with AS so column names describe the data rather than the expression. This makes reviews and debugging faster.
Trap 3: misunderstanding BETWEEN and wildcards in filtering
BETWEEN is inclusive, which matters for date ranges. Pattern matching uses % and _ not regex. If you need more flexible matching use appropriate string functions or full text search, but for many tasks LIKE with % is both efficient and expressive.
Trap 4: using ranking functions without an order clause
Ranking is meaningless without a deterministic ORDER BY. If you need ties to be handled specifically, choose between RANK, DENSE_RANK, and ROW_NUMBER depending on whether you need gaps in ranking or strict uniqueness.
Key Takeaways
- Learn to see queries as a composition of three moves: evaluate locally, group and reduce, and contextualize with windows. Naming each move clarifies design and debugging.
- Use CTEs to name intermediate steps. Make the first step the local expressions, then aggregate, then apply window functions to add context back to rows.
- Use AS to give expressions readable names. Clear names convert a query into documentation and make intent obvious.
- For per-group rankings, running totals, and percentiles, prefer window functions with PARTITION BY and ORDER BY. Choose RANK, DENSE_RANK, or ROW_NUMBER intentionally.
- Remember operator details: BETWEEN is inclusive; LIKE uses % and _; concatenation often uses ||; inequality uses <>.
Closing: SQL as a way of seeing
Many people treat SQL as a set of tools to be learned, rather than a way of thinking to be practiced. The three moves are not a cheat to avoid study. They are a lens that helps you recognize the recurring structure behind problems so that learning scales.
When you approach a new question, ask yourself three short questions: what value do I need for the single row, what summaries do I need across groups, and how should each row see the group that surrounds it? Answering these will guide you to the right combination of expressions, aggregates, and window functions. Name each step with a CTE and a clear alias, and you will write queries that are easier to reason about, easier to test, and easier to share.
Once you can move gracefully between a single cell and the story the table tells, you stop being a database consumer and become a database designer of thought.
Choose clarity over cleverness. When you do, the language of SQL stops being a barrier and becomes an amplifier of insight.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣