Performance Optimization — Learn NOT IN/NULL, Anti Joins, FILTER, and Sorting from the Basics
BasicNOT IN and NULLNOT EXISTS / Anti JoinConditional aggregation with FILTERORDER BY and indexesPostgreSQL-ready5 questions
QUESTION 6
The NOT IN and NULL Trap — Express Exclusions as NOT EXISTS Anti Joins
NOT EXISTSThree-valued logicNULLHash Anti Join
Background

NOT IN (subquery) has two traps when you need rows that do not exist elsewhere. The correctness trap comes first: if the subquery returns even one NULL, SQL's three-valued logic makes x NOT IN (...) UNKNOWN for every candidate row, producing an empty result. The performance trap follows: because the planner must preserve that NULL-sensitive behavior, it has less freedom to choose an efficient Anti Join plan.

-- ✗ A NULL in the subquery empties the result and can produce an inefficient plan
WHERE member_id NOT IN (SELECT member_id FROM event_entries)

-- ✓ NULL-safe and optimizable as a Hash Anti Join
WHERE NOT EXISTS (SELECT 1 FROM event_entries e
                  WHERE e.member_id = m.member_id)
The core of three-valued logic: x NOT IN (2, 4, NULL) expands to x <> 2 AND x <> 4 AND x <> NULL. The last comparison is always UNKNOWN, so the conjunction can never be true and no row passes WHERE. This is a classic cause of a query that “suddenly returns nothing” after new data arrives.
Problem

The members table contains 100,000 rows in production, while event_entries contains 3 million. Its nullable member_id represents guest attendance. Return the members who have never attended an event in a form that is safe from NULL and can be optimized as an Anti Join. Return member_id, name in ascending member_id order.

Tables used
- members
member_idname
1Aoki
2Baba
3Chiba
4Doi
5Endo
6Fujii
- event_entries (member_id is nullable)
entry_idmember_id
12
24
3NULL
42

entry_id=3 is a guest entry whose member_id is NULL. Also consider how this single row breaks NOT IN.

Expected Output

Expected output (member_id ascending):

member_idname
1Aoki
3Chiba
5Endo
6Fujii

NOT IN returns zero rows because of the NULL entry. NOT EXISTS returns the correct four rows with a Hash Anti Join.

Model Answer
SELECT   m.member_id, m.name
FROM     members m
WHERE    NOT EXISTS (              -- directly express that no matching row exists
           SELECT 1
           FROM   event_entries e
           WHERE  e.member_id = m.member_id  -- NULL never matches with =, so it is harmless
         )
ORDER BY m.member_id;

/*
  Execution order:
  1. Build a hash table from event_entries member_id values
  2. Scan members and probe the hash table → Hash Anti Join
  3. Pass only rows with no match → WHERE
  4. Sort by member_id ascending → return the result
  */
Explanation (table transitions & key points)
SELECT m.member_id, m.name FROM members m WHERE NOT EXISTS ( SELECT 1 FROM event_entries e WHERE e.member_id = m.member_id ) ORDER BY m.member_id;
LEGEND
Rows read / loaded
1. Target table — members (outer side)
FROM members mThis is the population in which we look for nonparticipants: 100,000 rows in production. The ideal plan reads the other side once to build a lookup structure instead of rerunning the subquery for every member.
1 / 5
member_idname
1Aoki
2Baba
3Chiba
4Doi
5Endo
6Fujii
6 rows (100,000 in production)
LEARNING POINTS
NOT IN is a chain of comparisons; NOT EXISTS is an existence test: NOT IN joins inequality comparisons with AND, so a single NULL makes the whole expression UNKNOWN. NOT EXISTS asks only whether a matching row was found and is unaffected by NULL. The latter is the intended meaning of an exclusion predicate, so writing exclusions as NOT EXISTS prevents this entire class of failure.
NOT EXISTS is preferable for performance too: PostgreSQL can transform it into a Hash Anti Join or Merge Anti Join that reads each table once. NOT IN must preserve the semantic possibility that a NULL changes the answer, which restricts optimization; on a nullable column it can become a large materialized subplan evaluated against every row.
A third form is LEFT JOIN … IS NULL: LEFT JOIN event_entries e ON … WHERE e.entry_id IS NULL can produce the same answer and a similar Anti Join plan. However, duplicate join keys can multiply rows before a later DISTINCT, so NOT EXISTS is the clearest and safest first choice. Use the LEFT JOIN form when you also need columns from the other side.
ANTI-PATTERNS
Leaving NOT IN because “there are no NULLs today”: the data, not the code, determines whether NOT IN works. One NULL from tomorrow's import can change the answer to zero rows. As long as the column is nullable, NOT IN is a time bomb. If it is unavoidable, explicitly filter WHERE member_id IS NOT NULL inside the subquery.
Sending the exclusion list as an application array: embedding 300,000 IDs in NOT IN (1, 2, 3, …) makes parsing and planning expensive. If the exclusions are stored in a table, match them inside the database with a subquery or join. For application-provided values, consider a temporary table or NOT EXISTS over a VALUES relation rather than negating = ANY(array).
Field Notes: NULL means unknown, not a value
SQL NULL is neither an empty string nor zero; it marks an unknown value. Therefore both NULL = NULL and NULL <> 1 evaluate to UNKNOWN. WHERE passes only true rows, so UNKNOWN is filtered just like false. The same three-valued logic explains why col <> 'x' omits NULL rows and why COUNT(col) ignores NULL. Whenever you write a negation, aggregate, or comparison, spend a second asking what NULL would do. That habit prevents a broad class of production failures. Q7 next reduces repeated aggregation scans to one pass.
QUESTION 7
Conditional Aggregation — Combine Multiple Full Scans into One with FILTER or CASE
FILTER clauseConditional aggregationCASE expressionOne scan
Background

Suppose one report needs the completed count and sales total, the pending count, and the canceled count from the same table. Separate subqueries or queries scan the table once per aggregate. Conditional aggregation instead reads the table once and routes each row to the appropriate counters as it goes.

-- ✗ One full scan per aggregate (5 million rows × 4)
SELECT
  (SELECT COUNT(*) FROM orders WHERE status = 'completed'),
  (SELECT SUM(amount) FROM orders WHERE status = 'completed'), ...

-- ✓ Every aggregate in one scan with PostgreSQL's FILTER clause
SELECT COUNT(*) FILTER (WHERE status = 'completed'), ...
FROM orders;
Portability: FILTER is standard SQL, but some databases do not support it. The exact equivalents are SUM(CASE WHEN status = 'completed' THEN amount END) and COUNT(CASE WHEN … THEN 1 END). Omitting ELSE makes unmatched rows NULL, which SUM and COUNT ignore.
Problem

From the 5-million-row production orders table, return these four values in one row with only one scan: completed_cnt, completed_amt, pending_cnt, and cancelled_cnt.

Table used
- orders
order_idstatusamount
1completed1200
2pending800
3completed2000
4cancelled500
5completed700
6pending1500

Imagine replacing four KPI queries on a dashboard with a single query.

Expected Output

Expected output (one row):

completed_cntcompleted_amtpending_cntcancelled_cnt
3390021

Four subqueries scan the equivalent of 5 million × 4 = 20 million rows. Conditional aggregation scans 5 million rows once.

Model Answer
SELECT
  COUNT(*)    FILTER (WHERE status = 'completed') AS completed_cnt,
  SUM(amount) FILTER (WHERE status = 'completed') AS completed_amt,
  COUNT(*)    FILTER (WHERE status = 'pending')   AS pending_cnt,
  COUNT(*)    FILTER (WHERE status = 'cancelled') AS cancelled_cnt
FROM orders;   -- this is the only table scan

/*
  Execution order:
  1. Scan orders from beginning to end once (Seq Scan)
  2. Evaluate every aggregate's FILTER condition for each row
  3. Update only the counters or sum whose conditions match
  4. Return all four aggregate values as one row after the scan finishes
  */
Explanation (table transitions & key points)
SELECT COUNT(*) FILTER (WHERE status = 'completed'), SUM(amount) FILTER (WHERE status = 'completed'), COUNT(*) FILTER (WHERE status = 'pending'), COUNT(*) FILTER (WHERE status = 'cancelled') FROM orders;
LEGEND
Grouping keys / aggregation targets
Group classification
1. Target table — orders
FROM ordersThe status column divides the data into three groups, shown by the row colors. The performance question is how many times these six sample rows—or 5 million production rows—must be read.
1 / 5
order_idstatusamount
1completed1200
2pending800
3completed2000
4cancelled500
5completed700
6pending1500
6 rows (5 million in production)
LEARNING POINTS
I/O is scan count × table size: separate or combined aggregates perform the same calculations. The difference is how often data is read from disk or cache. FILTER and CASE predicates are CPU work on rows already in memory, so the pattern trades repeated I/O for cheap CPU checks. Its benefit grows dramatically with table size.
FILTER and CASE are equivalent; choose for readability and portability: COUNT(*) FILTER (WHERE c) equals COUNT(CASE WHEN c THEN 1 END), and SUM(x) FILTER (WHERE c) equals SUM(CASE WHEN c THEN x END). FILTER states the intent more clearly and PostgreSQL permits it on any aggregate, including AVG, MIN, MAX, and ARRAY_AGG. Use CASE when targeting a database without FILTER support.
Conditional aggregation is independent of GROUP BY: combine it with GROUP BY region to build a region-by-status cross-tab in one scan. This is common when producing BI summaries or daily KPI batches, and Q10 uses the combination directly.
ANTI-PATTERNS
Looping over queries in application code: fetching a list of statuses and issuing a COUNT query for each adds network round trips—the N+1 problem—to repeated scans. Even for dynamic categories, one GROUP BY status query returns every group for the application to arrange.
Stacking aggregates vertically with UNION: SELECT 'completed', COUNT(*) … UNION SELECT 'pending', COUNT(*) … still scans once per aggregate. Without ALL it also adds needless duplicate elimination, as Q9 explains. Use GROUP BY for vertical rows and FILTER for horizontal columns.
Field Notes: Ask whether the design can finish in one pass
Across data processing, rereading the same data is a losing design. Conditional aggregation is the smallest practical example. GROUPING SETS or ROLLUP can combine multiple granularities, such as daily and monthly totals, into one scan; window functions can return aggregates and details together. A single unreadable mega-query is not a victory, however. Track scan count while using CTEs to keep the logical structure understandable. Q8 next removes the post-scan sorting stage with an index.
QUESTION 8
ORDER BY and Indexes — Remove Sorting, Understand Top-N Heapsort and work_mem
ORDER BYwork_memTop-N heapsortNo Sort node
Background

ORDER BY may look like a small final step, but it is a blocking operation that cannot begin producing ordered output until it has received the necessary input. Large sorts can exceed work_mem and fall back to an expensive disk-based external merge. A B-tree index, however, is always ordered. When its columns and order match the query, the database can remove the sorting stage entirely.

-- ✗ No index: read every row → sort → take the first three
Limit → Sort (Sort Method: top-N heapsort) → Seq Scan

-- ✓ published_at index: the Sort stage disappears
Limit → Index Scan Backward using idx_articles_pub
       -- The index guarantees order; stop after three rows
Read the Sort Method: EXPLAIN ANALYZE identifies quicksort when the full sort fits in memory, top-N heapsort when LIMIT retains only the best N rows, and external merge when work_mem is exceeded and disk is used. Seeing Disk: nnnn kB is a signal to remove the sort with an index or review work_mem.
Problem

Return the three newest articles from the 2-million-row production articles table in descending published_at order. Also define an index that eliminates the sorting stage. Return article_id, title, published_at.

Table used
- articles (heap order is not publication order)
article_idtitlepublished_at
1PostgreSQL Basics2026-05-01
2Index Design2026-06-09
3Reading Execution Plans2026-04-12
4JOIN Basics2026-06-11
5Understanding NULL2026-03-30
6Sorting and Memory2026-06-02
7Using CTEs2026-05-20

Think of a news site's “latest articles” panel: sorting 2 million rows on every request versus reading only three entries.

Expected Output

Expected output (published_at descending, 3 rows):

article_idtitlepublished_at
4JOIN Basics2026-06-11
2Index Design2026-06-09
6Sorting and Memory2026-06-02

With the index, the Sort node disappears and the plan reads only the first three index entries plus three heap rows.

Model Answer
CREATE INDEX idx_articles_pub
  ON articles (published_at DESC);  -- create an index in the ORDER BY sequence

SELECT   article_id, title, published_at
FROM     articles
ORDER BY published_at DESC   -- matches index order, so no Sort node is needed
LIMIT    3;                    -- stop the scan after three rows

/*
  Execution order:
  1. Position at the leading edge of idx_articles_pub (largest published_at)
  2. Read index entries in order and fetch the three columns from the heap
  3. LIMIT stops the scan after three rows; no Sort stage exists
  */
Explanation (table transitions & key points)
CREATE INDEX idx_articles_pub ON articles (published_at DESC); SELECT article_id, title, published_at FROM articles ORDER BY published_at DESC LIMIT 3;
LEGEND
Rows read / loaded
1. Target table — heap rows are not date-ordered
FROM articles (physical order follows inserts and updates)The table heap is not arranged by published_at. Someone must create the required order: either a Sort at query time or an index that maintains it in advance.
1 / 5
article_idtitlepublished_at
1PostgreSQL Basics2026-05-01
2Index Design2026-06-09
3Reading Execution Plans2026-04-12
4JOIN Basics2026-06-11
5Understanding NULL2026-03-30
6Sorting and Memory2026-06-02
7Using CTEs2026-05-20
7 rows (2 million in production)
LEARNING POINTS
Sorting is both blocking and memory-hungry: a Sort node cannot emit a row until it has enough input to establish order, and its memory use grows with the row count. Exceeding work_mem, commonly 4 MB by default, writes temporary files for an external merge and can multiply I/O. Check Sort Method and Disk: in EXPLAIN ANALYZE.
An index prepays sorting: a B-tree keeps values ordered by placing each inserted value correctly. When ORDER BY columns, direction, and NULL ordering match the index definition, the planner can remove Sort entirely. A B-tree can also be scanned backward, so a single-column ascending index can support both ASC and DESC queries.
One index can serve WHERE and ORDER BY: WHERE category = 'tech' ORDER BY published_at DESC LIMIT 3 is well served by (category, published_at DESC): find the equality slice, then read its first three ordered entries. This extends the “equality before range” rule to “equality before sort column.”
ANTI-PATTERNS
Raising global work_mem dramatically: setting it to 1 GB after seeing an external merge is dangerous because every session and every sort node may consume its own allocation, causing out-of-memory failures under concurrency. First remove the sort with an index. For an unavoidable analytical sort, raise it only for that transaction with SET LOCAL work_mem.
Indexing every presentation-specific multi-column order: blindly adding indexes for ORDER BY a, b DESC, c creates index sprawl. Sorting becomes a concern when both row count and execution frequency are high. Leave a few hundred display rows to sort normally and reserve indexes for genuinely expensive latest-item and ranking queries.
Field Notes: Sort nodes do not come only from ORDER BY
Sort may support a Merge Join, DISTINCT, GROUP BY when hashing is not chosen, a window function's PARTITION BY or ORDER BY, or UNION duplicate elimination as Q9 shows. When reading EXPLAIN, first identify which operation required each Sort. Then you can choose among removing it with an index, enabling a hash strategy, or eliminating the need for duplicate removal. Finding the quiet Sort behind a slow query is a key step toward intermediate tuning.
QUESTION 9
UNION vs UNION ALL — Do Not Pay for Unneeded Duplicate Elimination
UNION ALLDuplicate eliminationAppendAvoid Sort/Hash
Background

UNION removes duplicates while UNION ALL preserves them, but their performance is fundamentally different. UNION must sort or hash every selected column of every combined row to eliminate duplicates. UNION ALL merely appends one result to another, adding almost no work. When the data model makes overlap impossible, duplicate elimination is a tax with no benefit.

-- UNION: eliminate duplicates after Append; every row enters Sort or HashAggregate
HashAggregate  -- a hash table or sort for 5 million rows
  └─ Append → Seq Scan ×2

-- UNION ALL: only append the streams
Append → Seq Scan ×2   -- no duplicate-elimination stage exists
Let the data model decide: sources such as domestic versus overseas orders, or current versus archived months, are mutually exclusive by design. Writing UNION when there are no duplicates to remove is equivalent to volunteering for a full-result sort. Use UNION only when duplicates can occur and the requirement is to remove them.
Problem

The 3-million-row domestic_orders table and 2-million-row overseas_orders table use separate order-ID ranges, so the same order can never occur in both. Return one combined list of order_id, customer, amount without incurring an unnecessary duplicate-elimination stage.

Tables used
- domestic_orders (1000-range order IDs)
order_idcustomeramount
1001Sato5000
1002Suzuki3200
1003Tanaka7800
- overseas_orders (9000-range order IDs)
order_idcustomeramount
9001Smith12000
9002Lee4500

Is “use UNION to be safe” really safe for performance? Consider what happens across 5 million rows.

Expected Output

Expected output (5 rows, no duplicate-elimination stage):

order_idcustomeramount
1001Sato5000
1002Suzuki3200
1003Tanaka7800
9001Smith12000
9002Lee4500

UNION ALL needs only Append. UNION performs duplicate checks across 5 million rows to produce the same answer.

Model Answer
SELECT order_id, customer, amount
FROM   domestic_orders
UNION ALL                       -- mutually exclusive sources need no duplicate elimination
SELECT order_id, customer, amount
FROM   overseas_orders;

/*
  Execution order:
  1. Scan domestic_orders and stream each row directly to the output
  2. Scan overseas_orders next and append its rows
  3. Perform no sorting, hashing, or duplicate comparisons
  */
Explanation (table transitions & key points)
SELECT order_id, customer, amount FROM domestic_orders UNION ALL SELECT order_id, customer, amount FROM overseas_orders;
LEGEND
Grouping keys / aggregation targets
Group classification
1. Input — two mutually exclusive source tables
domestic_orders / overseas_ordersThe separate 1000 and 9000 ID ranges make it structurally impossible for one row to appear in both tables. Row colors show the source. This design guarantee is the basis for choosing UNION ALL.
1 / 5
sourceorder_idcustomeramount
domestic1001Sato5000
domestic1002Suzuki3200
domestic1003Tanaka7800
overseas9001Smith12000
overseas9002Lee4500
3 + 2 rows (3 million + 2 million in production)
LEARNING POINTS
UNION compares every selected column of every row: duplicate identity is based on the complete output tuple. More columns and wider rows enlarge hash entries or sort keys, making cost grow faster than row count alone suggests. The stage is also blocking, so it is especially poor for LIMIT queries and streaming consumers.
UNION ALL gives the planner more freedom: Append can optimize branches independently and push outer WHERE predicates into them for pruning. PostgreSQL table partitioning relies on the same mechanism. Splitting storage by month or another exclusive key and querying the parts through ALL is a natural pattern for the engine.
Even when duplicates are possible, find their source first: preventing overlap in each branch's WHERE clause—such as separating migration periods—is often cheaper than combining everything and removing duplicates afterward. Q3 used UNION to split an OR on one table because a row could match both branches; that is the contrasting case where duplicate handling may be required.
ANTI-PATTERNS
Making plain UNION the template “to be safe”: teams that always remove duplicates silently create set operations that slow as data grows. Reverse the default: use UNION ALL, and make duplicate elimination an explicit decision. A healthy review question is, “Which duplicates is this UNION intended to remove?”
Using UNION as a substitute for DISTINCT: duplicating a query in UNION, or wrapping UNION ALL with a later SELECT DISTINCT, pays for scans and sorting twice. Find the source of duplicates—often row multiplication from a JOIN—and fix it with EXISTS or a corrected join predicate.
Field Notes: ALL also declares meaning
Choosing UNION ALL is not only a performance decision; it states design knowledge that the sources are mutually exclusive. A reviewer can see that assumption, and if the ID systems later merge, the place to revisit is clear. An unexplained UNION leaves overlap ambiguous. Queries are part of the specification, and the faster form is often also the one that communicates intent most precisely. Q10 combines NOT EXISTS, FILTER, and UNION ALL in one report.
QUESTION 10
Capstone — Build a Report with UNION ALL, NOT EXISTS, and FILTER
CapstoneUNION ALLNOT EXISTSFILTER aggregation
Background

This final problem combines the second half's building blocks: 1. UNION ALL for mutually exclusive sources (Q9), 2. NOT EXISTS for exclusions (Q6: NULL-safe and Anti Join-friendly), and 3. FILTER for multiple conditional aggregates in one scan (Q7). The opposite pattern—UNION + NOT IN + one subquery per aggregate—costs several times more and may return a wrong result when NULL appears.

-- Composition pattern, read from the inside outward
SELECT aggregate FILTER (...)        -- 3. conditional metrics in one scan
FROM ( ... UNION ALL ... ) s    -- 1. combine exclusive sources
WHERE NOT EXISTS ( ... )        -- 2. NULL-safe exclusion
GROUP BY ...
How to read it: follow the data flow—FROM → WHERE → GROUP BY → SELECT. The processing story is “combine → exclude → aggregate,” which is also the logical execution order.
Problem

Build a shipment report. Combine domestic shipments_jp and international shipments_intl, whose ID systems are mutually exclusive and whose production tables each contain millions of rows. Exclude shipments from test accounts, then aggregate pending and shipped counts by region. test_accounts.customer_id is nullable because pending applications may appear. Return region, pending_cnt, shipped_cnt ordered by region ascending.

Tables used
- shipments_jp
ship_idstatuscustomer_id
1pending101
2shipped102
3pending103
4shipped101
- shipments_intl
ship_idstatuscustomer_id
51pending201
52shipped202
53shipped999
- test_accounts (customer_id is nullable)
account_idcustomer_id
1999
2NULL

Also identify where a UNION + NOT IN + status-specific subqueries version would become slow or incorrect.

Expected Output

Expected output (region ascending):

regionpending_cntshipped_cnt
INTL11
JP22

Only ship_id=53, the shipment for test account 999, is excluded. NOT IN would return zero rows because test_accounts contains NULL.

Model Answer
SELECT   s.region,
         COUNT(*) FILTER (WHERE s.status = 'pending') AS pending_cnt,
         COUNT(*) FILTER (WHERE s.status = 'shipped') AS shipped_cnt
FROM (
  SELECT 'JP'   AS region, status, customer_id FROM shipments_jp
  UNION ALL                       -- exclusive sources: ALL avoids duplicate-elimination cost
  SELECT 'INTL', status, customer_id FROM shipments_intl
) s
WHERE    NOT EXISTS (              -- NULL-safe exclusion through a Hash Anti Join
           SELECT 1 FROM test_accounts t
           WHERE  t.customer_id = s.customer_id
         )
GROUP BY s.region
ORDER BY s.region;

/*
  Execution order:
  1. Append shipments_jp and shipments_intl while adding region
  2. Build a hash table from test_accounts for matching
  3. Exclude matching rows with an Anti Join
  4. Group by region and update the FILTER counters
  5. Sort by region ascending and return the result
  */
Explanation (table transitions & key points)
SELECT s.region, COUNT(*) FILTER (WHERE s.status = 'pending'), COUNT(*) FILTER (WHERE s.status = 'shipped') FROM ( SELECT 'JP' AS region, status, customer_id FROM shipments_jp UNION ALL SELECT 'INTL', status, customer_id FROM shipments_intl ) s WHERE NOT EXISTS ( SELECT 1 FROM test_accounts t WHERE t.customer_id = s.customer_id ) GROUP BY s.region ORDER BY s.region;
LEGEND
Rows read / loaded
1. Input — two exclusive sources plus an exclusion list
shipments_jp / shipments_intl / test_accountsThe shipments are split across two tables with exclusive ID systems. test_accounts contains customer_id 999 and NULL. That NULL would make the entire report empty as soon as NOT IN was used.
1 / 5
sourceship_idstatuscustomer_id
JP1pending101
JP2shipped102
JP3pending103
JP4shipped101
INTL51pending201
INTL52shipped202
INTL53shipped999
JP 4 + INTL 3 rows / exclusion list {999, NULL}
LEARNING POINTS
Count whether each table is scanned only once: this plan stops at one scan each of shipments_jp, shipments_intl, and test_accounts—three scans total. A UNION + NOT IN + two status subqueries version reads the shipment tables at least four times and still pays full duplicate elimination. Counting table appearances in the execution plan is a fast first check for a complex query.
Correctness and speed arise from the same choices: NOT EXISTS is both NULL-safe and optimizable as an Anti Join. UNION ALL both declares that the sources are exclusive and needs only Append. In good SQL, the correctness argument and performance argument reinforce each other; that principle runs throughout this basic series.
Derived tables and predicate pushdown: adding an outer predicate such as WHERE s.region = 'JP' lets the planner push it into UNION ALL branches and skip the INTL scan entirely through Append pruning. Writing “combine, then filter” can execute as “filter, then combine,” which is why UNION ALL is foundational to partitioning.
ANTI-PATTERNS
Treating a working report as untouchable: reports are highly exposed to data growth, yet correct output often leaves them unreviewed. The three checks from this set—missing ALL on UNION, NOT IN, and repeated aggregate subqueries—can be applied directly. Run EXPLAIN ANALYZE periodically and inspect scan counts plus Sort and HashAggregate nodes.
Applying exclusions after aggregation: aggregating test-account rows and subtracting afterward creates unnecessary intermediate work and may be unable to reverse every FILTER breakdown accurately. Exclude and filter as far upstream as possible, in WHERE or before joins, for both correctness and speed.
Field Notes: Five principles for the second half of Performance Basics 002
Q6–Q10 give five checks: 6. Use NOT EXISTS for exclusions (NULL-safe Anti Join); 7. Use FILTER or CASE for conditional metrics in one scan; 8. Remove frequent ORDER BY + LIMIT sorts with an index and conserve work_mem; 9. Combine exclusive sources with UNION ALL and make duplicate removal explicit; and 10. Follow combine → exclude → aggregate while keeping every table to one scan. Together with the first half's type alignment, prefix LIKE, OR decomposition, WHERE versus HAVING, and covering-index rules—and the ten principles from Performance Basics 001—you now have a 20-item checklist. Most practical SELECT tuning becomes identifying which principle a query violates. The intermediate series moves on to cardinality estimates and JOIN strategy.