---
title: "04 — DML and the SELECT Pipeline: FROM, WHERE, GROUP BY, HAVING, ORDER BY"
uid: dml-organizing-queries
tags: ["sql", "order-by", "roadmap:sql", "from", "dml", "where", "having", "group-by"]
excerpt: "A query flows through a fixed logical pipeline — FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — and each clause is a stage that shapes the rows passing through."
date: 2026-08-13T03:27:38+0000
source: https://www.aveshina.my.id/en/blog/dml-organizing-queries
---

A pile of clauses I'd copy-paste and shuffle until they worked was my SELECT-building method. Writing them down collapsed the mess into one model: **a query flows through a fixed logical pipeline — FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — and each clause is just a stage that shapes the rows passing through.** [1][2][3]

The framing that clicked is that the order I *write* a query differs from the order the engine *evaluates* it. I write SELECT first, but the engine reads FROM first. Once I knew the evaluation order, every clause had an obvious job: FROM picks the source, WHERE filters rows *before* grouping, GROUP BY buckets them, HAVING filters the *buckets*, and ORDER BY sorts the final output.

```figure
<svg viewBox="0 0 720 220" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The SELECT evaluation pipeline as five left-to-right stages. FROM picks the source table, WHERE filters individual rows, GROUP BY buckets rows into groups, HAVING filters the groups, ORDER BY sorts the final output. Rows visibly narrow and reshape at each stage.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- FROM -->
    <rect x="20" y="60" width="110" height="100" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="75" y="84" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">FROM</text>
    <text x="75" y="100" font-size="9" fill="#475569" text-anchor="middle">pick the source</text>
    <g fill="#a5b4fc">
      <rect x="34" y="112" width="82" height="8" rx="2"/>
      <rect x="34" y="124" width="82" height="8" rx="2"/>
      <rect x="34" y="136" width="82" height="8" rx="2"/>
      <rect x="34" y="148" width="82" height="8" rx="2"/>
    </g>

    <!-- WHERE -->
    <rect x="155" y="60" width="110" height="100" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="210" y="84" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">WHERE</text>
    <text x="210" y="100" font-size="9" fill="#475569" text-anchor="middle">filter rows</text>
    <g fill="#fde047">
      <rect x="169" y="116" width="82" height="8" rx="2"/>
      <rect x="169" y="140" width="82" height="8" rx="2"/>
    </g>

    <!-- GROUP BY -->
    <rect x="290" y="60" width="110" height="100" rx="8" fill="#ccfbf1" stroke="#0d9488" stroke-width="1.5"/>
    <text x="345" y="84" font-size="12" font-weight="700" fill="#134e4a" text-anchor="middle">GROUP BY</text>
    <text x="345" y="100" font-size="9" fill="#475569" text-anchor="middle">bucket rows</text>
    <g fill="#5eead4">
      <rect x="304" y="116" width="36" height="20" rx="3"/>
      <rect x="346" y="116" width="36" height="20" rx="3"/>
      <rect x="304" y="140" width="36" height="14" rx="3"/>
      <rect x="346" y="140" width="36" height="14" rx="3"/>
    </g>

    <!-- HAVING -->
    <rect x="425" y="60" width="110" height="100" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="480" y="84" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">HAVING</text>
    <text x="480" y="100" font-size="9" fill="#475569" text-anchor="middle">filter groups</text>
    <g fill="#f9a8d4">
      <rect x="439" y="120" width="82" height="14" rx="3"/>
      <rect x="439" y="142" width="82" height="14" rx="3"/>
    </g>

    <!-- ORDER BY -->
    <rect x="560" y="60" width="140" height="100" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="630" y="84" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">ORDER BY</text>
    <text x="630" y="100" font-size="9" fill="#475569" text-anchor="middle">sort output</text>
    <g fill="#86efac">
      <rect x="580" y="116" width="100" height="10" rx="2"/>
      <rect x="580" y="130" width="100" height="10" rx="2"/>
      <rect x="580" y="144" width="100" height="10" rx="2"/>
    </g>

    <!-- arrows -->
    <g stroke="#64748b" stroke-width="1.5" fill="none">
      <line x1="132" y1="110" x2="153" y2="110" marker-end="url(#dmlarrow)"/>
      <line x1="267" y1="110" x2="288" y2="110" marker-end="url(#dmlarrow)"/>
      <line x1="402" y1="110" x2="423" y2="110" marker-end="url(#dmlarrow)"/>
      <line x1="537" y1="110" x2="558" y2="110" marker-end="url(#dmlarrow)"/>
    </g>

    <text x="360" y="195" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">evaluation order differs from the order you write — FROM is read first, SELECT projected late</text>

    <defs>
      <marker id="dmlarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
        <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
      </marker>
    </defs>
  </g>
</svg>
```

## DML — the data side

DML (Data Manipulation Language) is the subset that operates on data within an existing structure: SELECT, INSERT, UPDATE, DELETE [1][2]. Where DDL shapes the stage, DML is the actors. I covered INSERT/UPDATE/DELETE in the CRUD notes; here the focus is the SELECT pipeline itself, because the same clause ordering governs every reporting query I write.

## FROM — name the source

FROM declares where the data comes from — a single table, several joined tables, or a subquery [3]. Without it, the engine has nothing to read. It's the first stage evaluated, even though I write it second.

```
SELECT name, total FROM orders;
```

## WHERE — filter rows, before grouping

WHERE keeps individual rows that satisfy a condition and discards the rest [4]:

```
SELECT name, total FROM orders WHERE total > 50;
```

The key point: WHERE runs *before* any grouping, so it can't reference aggregate functions. WHERE SUM(total) > 50 is illegal — that's HAVING's job. WHERE is for filtering raw rows; aggregates don't exist yet at this stage.

## GROUP BY — bucket the rows

GROUP BY collapses rows that share the same value in the named column(s) into one bucket per distinct value [5][6]. Pair it with an aggregate (COUNT, SUM, AVG) and each bucket produces one summary row:

```
SELECT customer_id, SUM(total) AS spent
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
```

This is how I turn row-level data into per-customer, per-month, per-anything summaries. The non-aggregated columns in the SELECT must appear in the GROUP BY — otherwise the engine doesn't know which value to show for the bucket.

## HAVING — filter the buckets

HAVING is WHERE for grouped results [7][8]. It runs *after* GROUP BY, so it can reference aggregates — the one thing WHERE cannot do:

```
SELECT customer_id, SUM(total) AS spent
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(total) > 1000;
```

The division of labor is the whole point: WHERE filters raw rows cheaply, early; HAVING filters computed groups after the aggregation work is done. Reaching for HAVING when WHERE would do means computing aggregates over rows I was going to throw away anyway.

## ORDER BY — sort the output

ORDER BY sorts the final result by one or more columns, ascending by default, DESC for descending [9]:

```
ORDER BY spent DESC, customer_id ASC
```

It's the last stage, operating on the already-projected result set, which is why it can reference column aliases defined in the SELECT — by the time ORDER BY runs, those aliases exist.

## How I use this

The habit I keep from this pipeline is writing queries in evaluation order, not keyword order. I draft FROM and WHERE first (what rows am I working with?), then GROUP BY/HAVING (how do I summarize and filter the summary?), and only then the SELECT list (which columns and aggregates do I show?). Drafting in that order surfaces mistakes early — a WHERE clause referencing an aggregate is immediately obvious as illegal, and a missing column in GROUP BY shows up before I've built a result on top of it.

## References

[1] Satori Cyber, "What is DML?," satoricyber.com, 2024. [Online]. Available: [https://satoricyber.com/glossary/dml-data-manipulation-language](https://satoricyber.com/glossary/dml-data-manipulation-language)

[2] Wikipedia, "Data manipulation language," en.wikipedia.org, 2024. [Online]. Available: [https://en.wikipedia.org/wiki/Data_manipulation_language](https://en.wikipedia.org/wiki/Data_manipulation_language)

[3] AppMaster, "Difference Between DDL and DML," appmaster.io, 2024. [Online]. Available: [https://appmaster.io/blog/difference-between-ddl-and-dml](https://appmaster.io/blog/difference-between-ddl-and-dml)

[4] W3Schools, "WHERE Clause," w3schools.com, 2024. [Online]. Available: [https://www.w3schools.com/sql/sql_where.asp](https://www.w3schools.com/sql/sql_where.asp)

[5] Programiz, "SQL GROUP BY," programiz.com, 2024. [Online]. Available: [https://www.programiz.com/sql/group-by](https://www.programiz.com/sql/group-by)

[6] Programiz, "SQL HAVING Clause," programiz.com, 2024. [Online]. Available: [https://www.programiz.com/sql/having](https://www.programiz.com/sql/having)

[7] Atlassian, "Efficient column updates in SQL," atlassian.com, 2024. [Online]. Available: [https://www.atlassian.com/data/sql/how-to-update-a-column-based-on-a-filter-of-another-column](https://www.atlassian.com/data/sql/how-to-update-a-column-based-on-a-filter-of-another-column)

[8] YouTube, "How to write basic SQL — FROM clause," 2023. [Online]. Available: [https://www.youtube.com/watch?v=YfTDBA45PHk](https://www.youtube.com/watch?v=YfTDBA45PHk)

[9] YouTube, "SQL ORDER BY Sorting Clause," 2023. [Online]. Available: [https://www.youtube.com/watch?v=h_HHTNjAgS8](https://www.youtube.com/watch?v=h_HHTNjAgS8)

```quiz
Q: In what order does the database engine logically evaluate a SELECT query?
- SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY
- FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY
correct: 1
explain: The engine reads FROM first, filters rows with WHERE, buckets with GROUP BY, filters groups with HAVING, projects the SELECT list, then sorts with ORDER BY — regardless of the order you write.

Q: Why can't WHERE reference aggregate functions like SUM(total)?
- Because aggregates are too slow for WHERE
- Because WHERE runs before grouping, so aggregates don't exist yet
correct: 1
explain: WHERE filters raw rows before GROUP BY. Aggregates are computed during grouping, so filtering on them is HAVING's job.

Q: You want only customers whose total paid spend exceeds 1000. Which clause expresses that?
- WHERE SUM(total) > 1000
- HAVING SUM(total) > 1000
correct: 1
explain: HAVING runs after GROUP BY and can reference aggregates. WHERE cannot.

Q: Every non-aggregated column in the SELECT list must also appear in…
- ORDER BY
- GROUP BY
correct: 1
explain: When grouping, the engine needs to know which value to show for each bucket, so any non-aggregated selected column must be a grouping column.

Q: ORDER BY can reference column aliases defined in the SELECT because…
- aliases are just comments
- ORDER BY runs after the SELECT list is projected, so the aliases already exist
correct: 1
explain: ORDER BY is the last stage, evaluated after the SELECT projection, so aliases defined there are available to it.
```
