Code, art & human experience · Sabrina Danielle
← All field notes

Engineering in practice

PEMDAS, Panic, and a Production Report

Operator precedence, SQL surprises, and the day I mistook a reporting problem for a verdict on myself.

The conversation ↓
A pencil-drawn branching tree beside a brass magnifying glass and wooden blocks.
AI-generated editorial illustration; the sketch is conceptual.

A report was wrong.

That was the technical problem.

The problem I created inside my own head was considerably larger.

I remember a change that affected a calculation, numbers that were no longer right, and the awful feeling that I had somehow forgotten elementary math in front of people who expected me to know what I was doing.

PEMDAS.

How could I possibly have a problem with PEMDAS?

Then came the thought beneath the thought.

I had worked with people for whom I was the first Black woman they had meaningfully interacted with. One colleague had told me that. In that moment, I wasn’t only worried about my calculation. I was afraid my mistake would become somebody’s conclusion about all of us.

That is an unreasonable amount of responsibility to put inside one SQL query.

But knowing that now does not change how heavy it felt then.

I want to separate those two things: the technical lesson worth learning and the personal punishment that helped me learn nothing.

First, let’s not put the database on trial without evidence

I am not reconstructing that incident here. Without the original query, database version, input types, and before-and-after behavior, I cannot honestly identify its precise cause.

The examples below are independent demonstrations, not a diagnosis of that report.

What they demonstrate is important, though: a school mnemonic is not a complete specification for a programming language.

SQL Server, for example, puts multiplication and division at the same precedence level, above addition and subtraction. That is familiar arithmetic territory—not evidence that SQL has decided to disregard mathematics. 1

The surprises appear when we assume that familiar-looking expressions mean exactly the same thing everywhere.

Precedence determines how tightly an operator binds. Associativity determines how operators at the same precedence level group together. These are related questions, but not interchangeable ones. 2

Then there is another question: what does the operator actually mean in this language?

A familiar symbol can have a different job

Consider the caret: ^.

In PostgreSQL, it performs exponentiation. In Python, it performs bitwise exclusive OR on integers; Python uses ** for exponentiation. The same-looking expression can therefore produce a different result before precedence even enters the conversation. 3

EnvironmentExpressionResultMeaning
PostgreSQL2 ^ 38Exponentiation
Python2 ^ 31Bitwise exclusive OR
Python2 ** 38Exponentiation

The machine is not confused about two cubed.

We have asked two different languages to do two different things.

Even exponents have local customs

Now consider a chain of exponents.

PostgreSQL groups repeated ^ operators from the left. Python groups repeated ** operators from the right. That difference is associativity. 4

In PostgreSQL:

SELECT 2 ^ 3 ^ 2;     -- 64: (2 ^ 3) ^ 2
SELECT 2 ^ (3 ^ 2);   -- 512

In Python:

2 ** 3 ** 2      # 512: 2 ** (3 ** 2)
(2 ** 3) ** 2    # 64

There is also a genuine precedence difference around negative numbers. PostgreSQL places unary minus above exponentiation. Python places exponentiation above unary minus on its left. 5

-- PostgreSQL
SELECT -2 ^ 2;      -- 4
SELECT -(2 ^ 2);    -- -4
# Python
-2 ** 2       # -4
(-2) ** 2     # 4

This is why I want to distinguish remembering mathematics from knowing the rules of a particular language.

They overlap.

They are not the same task.

Sometimes the order is fine and the types are the trouble

Here is another way to get a result that looks wrong without any precedence rule being broken.

In SQL Server, dividing an integer by an integer produces an integer result, with the fractional part truncated. 6

-- SQL Server
SELECT 5 / 2;  -- 2

Putting parentheses around the division does not turn its operands into decimals.

Casting after the division does not recover the fraction either:

SELECT CAST(5 / 2 AS decimal(10, 2));  -- 2.00

To retain the fractional result in this example, make an operand decimal before division:

SELECT CAST(5 AS decimal(10, 2)) / 2;  -- Numerical value: 2.5

The placement of the conversion matters because the integer division has already discarded the fractional part in the earlier expression. This follows SQL Server’s documented division and result-type rules. 7

That is a type issue.

Calling every unexpected result “PEMDAS” can send the investigation in the wrong direction.

The report may be counting the wrong rows

Arithmetic is not the only place where grouping matters.

In SQL Server, AND has higher precedence than OR. Consider these illustrative query fragments against an orders table: 8

WHERE status = 'paid'
   OR status = 'refunded' AND archived = 0

This groups as:

WHERE status = 'paid'
   OR (status = 'refunded' AND archived = 0)

The archive condition applies to the refunded branch. A paid order can satisfy the condition regardless of its archive flag.

To apply the archive condition to both statuses, write:

WHERE (status = 'paid' OR status = 'refunded')
  AND archived = 0

Those parentheses express a different rule. A report can consequently contain different rows even when every arithmetic calculation is correct. 9

Before accusing the sum, I would check what we invited into it.

Yes, precedence rules have changed between versions

This is not an imaginary possibility.

PostgreSQL documents changes introduced in version 9.5 involving certain comparisons, IS tests, and constructs such as NOT BETWEEN. Its documentation also notes that some corner cases could change behavior without producing a parsing error. These were specific changes—not a general reversal of multiplication and addition. 10

PHP provides another historical example. In PHP 8.0, string concatenation with . moved below arithmetic addition and subtraction in precedence; previously, they shared a level. 11

Neither example proves what happened to my report.

They do demonstrate why “Which language?” sometimes needs to be followed by “Which version?”

Parentheses help. They are not the whole investigation.

I like explicit grouping where it makes the intended rule easier to see.

But parentheses are not a universal repair tool. They do not fix incorrect business logic or restore information already lost through an operation.

They also should not be confused with a guarantee about runtime execution order. PostgreSQL explicitly states that the evaluation order of subexpressions is not fixed, and that Boolean expressions can be reorganized during query planning. Grouping and execution order are different concerns. 12

For a calculation that matters, this is the approach I would take:

  1. Identify the environment. Record the language or SQL dialect, its version, and the relevant data types.
  2. Reduce the problem. Build the smallest example that reproduces the unexpected result.
  3. Make the intention visible. Use clear grouping, deliberate conversions, and names that explain the business rule.
  4. Test against known answers. Include meaningful edge cases and rerun the checks when the implementation or environment changes.

That is an investigation I can work with.

“Maybe I’m an idiot” does not belong on the checklist.

A bug is not a biography

What I wish I had understood in that moment is that taking a mistake seriously does not require turning it into a judgment about my entire ability.

I could investigate the query without putting myself on trial.

I could acknowledge an error without volunteering it as evidence against every Black woman who might come after me.

The work deserves care. The numbers deserve verification. The people using the report deserve an accurate result.

None of that requires me to be less human than the other engineers in the room.

So remember PEMDAS.

Read the documentation, too.

And when a calculation goes wrong, go looking for the cause—not a reason you never belonged.

Footnotes

  1. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  2. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  3. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  4. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  5. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  6. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  7. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  8. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  9. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  10. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  11. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

  12. Source link was not included in the supplied draft. Verify and add the original source before publication. ↩

Think it through with me

The conversation

What helped you separate investigating a mistake from judging yourself?

Be generous. Be curious. Respect each other’s privacy. Our house rules ↗

Comments are hosted by Hyvor Talk. Opening the conversation connects to that service.

The Code Conjurer

Notes from
the workbench.

New writing on software, AI, and what I’m learning as I build.