October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk6 min

5 SQL Patterns That Run Fine but Return the Wrong Answer

Five SQL patterns can execute without errors yet produce misleading results. Learn the hidden rules behind NULLs, joins, sums, window frames, and timestamp ranges.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A SQL query can execute successfully and still give a plausible but incorrect result. The cause is often a mismatch between what the query appears to say and how SQL handles NULLs, joins, aggregation, window frames, or timestamp boundaries. These examples follow PostgreSQL behavior; confirm relevant defaults and type handling in your database and version.

Why does NOT IN return no rows when the subquery has a NULL?

NOT IN looks like a direct way to find records with no match. But if its subquery returns a NULL, a comparison that does not match a non-NULL value can evaluate to unknown—not true. A WHERE clause keeps only rows for which its condition is true, so the anti-match may unexpectedly return no rows. PostgreSQL’s documentation wiki explains this NULL behavior.

As an Amazon Associate I earn from qualifying purchases.

SELECT c.id
FROM customers AS c
WHERE c.id NOT IN (SELECT o.customer_id FROM orders AS o);

Use NOT EXISTS for an absence test, and decide explicitly whether an outer row with a NULL key should count as unmatched:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
SELECT c.id
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders AS o
  WHERE o.customer_id = c.id
);

With this equality predicate, a NULL customer ID does not match an order ID; NOT EXISTS will therefore include that customer. If that is not the intended business rule, add an explicit condition to handle outer NULL keys. Alternatively, exclude NULLs from the subquery when that matches the rule, but do not assume that filtering only the subquery settles how outer NULLs should be treated.

What to check

  • Check whether the subquery key can contain NULL, for example with WHERE customer_id IS NULL.
  • Test the anti-match with a known matching key, a known non-matching key, and a NULL key on each side where applicable.

Why did my LEFT JOIN turn into an inner join?

A LEFT JOIN retains unmatched rows from its left input by filling right-side columns with NULL. A later WHERE condition on one of those right-side columns can remove those rows: the condition is not true for NULL. The result then contains only rows with a qualifying match, as PostgreSQL’s table-expression documentation describes for join inputs and conditions.

SELECT a.id, b.status
FROM accounts AS a
LEFT JOIN events AS b ON b.account_id = a.id
WHERE b.status = 'open';

If the goal is to keep every account and attach only its open events, put the status condition in the join condition:

SELECT a.id, b.status
FROM accounts AS a
LEFT JOIN events AS b
  ON b.account_id = a.id
 AND b.status = 'open';

If the goal is to return only accounts with an open event, the original filtering approach expresses that requirement. PostgreSQL distinguishes row filtering with WHERE from group filtering with HAVING in its SELECT reference. For a complex query, verify that a known account without events survives when preservation is required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why is my SUM too high after joining two tables?

Aggregates operate on the rows produced by the joins. If one order has several matching items, its order total appears once per item in the joined rows; summing at customer level adds those repeated amounts. The query is summing the joined row set correctly, but that set has item-level rather than order-level grain. PostgreSQL documents how joins form input rows and how GROUP BY groups them before aggregation in its table-expression reference.

SELECT o.customer_id, SUM(o.order_total)
FROM orders AS o
JOIN order_items AS i ON i.order_id = o.id
GROUP BY o.customer_id;

Choose a repair based on the intended result:

  • Need totals at order grain: aggregate orders before joining item details, or aggregate the order and item data separately before combining results.
  • Need orders that have at least one item, but no item columns: use EXISTS as a match test instead of joining item rows into the aggregate input.
  • Need a customer total: establish the intended grain first, then sum one order-level value per order.

Compare row counts and distinct order IDs before and after each join to find where multiplicity changes. Avoid using SUM(DISTINCT o.order_total) as a general fix: two different orders may legitimately have the same total, and the distinct sum would collapse them.

Why does SUM() OVER (ORDER BY ...) give me a running total?

In PostgreSQL, an aggregate window with ORDER BY uses a default frame that runs from the partition start through the current row’s last peer. That produces a running aggregate; rows tied on the ordering value share the peer endpoint. The PostgreSQL 18 window-functions tutorial demonstrates the difference between an unordered whole-window sum and an ordered one. It also notes that tied rows in row_number are numbered in an unspecified order unless the ordering breaks the tie.

SELECT employee_id, salary,
       SUM(salary) OVER (ORDER BY salary) AS total_salary
FROM employees;

Choose the window definition that matches the question:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Whole result total on every row: SUM(salary) OVER ().
  • Department total on every employee row: SUM(salary) OVER (PARTITION BY department_id).
  • Running total row by row: specify a stable ordering and an explicit frame, such as ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Add a unique tie-breaker if tied salaries must have a defined row-by-row sequence.

Window functions use the virtual table formed after the query’s FROM, WHERE, GROUP BY, and HAVING processing, as the PostgreSQL 18 tutorial explains. A window total therefore reflects the rows that remain at that stage, not necessarily every row in the underlying table.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Why does BETWEEN miss rows on the end date?

BETWEEN includes both endpoints. In PostgreSQL, a date-like upper bound used with a timestamp can represent midnight at the start of that date, leaving later timestamps on that same date outside the range. The PostgreSQL wiki’s timestamp guidance illustrates the boundary problem.

WHERE created_at BETWEEN '2026-10-01' AND '2026-10-07'

For a period covering October 1 through October 7, use a half-open interval: inclusive at the start and exclusive at the next period boundary.

WHERE created_at >= '2026-10-01'
  AND created_at <  '2026-10-08'

When the values represent absolute instants, calculate those boundaries in the intended business time zone and use an appropriate timezone-aware timestamp type. Timestamp and time-zone behavior can vary by engine and type, so confirm the interpretation in the target database rather than treating this PostgreSQL guidance as universal.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Two other silent aggregate surprises in PostgreSQL

Does SUM return zero when no rows match?

No. PostgreSQL returns NULL for sum over no selected rows; count is an exception among built-in aggregates. Use COALESCE(SUM(amount), 0) only when the application’s meaning of “no rows” is genuinely zero rather than unknown or absent. See the PostgreSQL aggregate-function documentation.

Is the order of array_agg or string_agg guaranteed?

Not by default in PostgreSQL. If the order of elements or concatenated values is part of the result, put an ORDER BY inside the aggregate call, as described in the aggregate documentation. An outer query’s ordering does not define the order in which values were fed to the aggregate.

A quick way to diagnose plausible but wrong results

  • Unexpectedly empty anti-match: inspect NULLs in both compared keys and verify the intended treatment of outer NULLs.
  • Missing left-side records: test a known unmatched row and inspect right-side predicates in WHERE.
  • Inflated totals: compare row counts and distinct keys before and after joins; state the grain each aggregate should represent.
  • Unexpected window values: check partitioning, ordering, frame, and tie-breakers.
  • Missing boundary timestamps: inspect the actual start and end instants, timestamp types, and time zone used to compute the interval.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.