Back to Devoured - August 31, 2026
DEVOURED
Speeding up some Postgres queries by up to 1000x with a single typecast

Speeding up some Postgres queries by up to 1000x with a single typecast

Data Linkedin
A simple Postgres typecast resolved a massive performance bottleneck caused by cross-type comparisons that rendered indexes unusable under Row Level Security.
What: Karla Burnett discovered that comparing a 'timestamp' column to a 'timestamptz' value (returned by NOW()) prevented Postgres from using btree indexes because the non-leakproof operator is prohibited under Row Level Security (RLS). Casting the comparison to '::timestamp' allowed index usage and improved query speed by up to 1,000x at Lorikeet.
Why it matters: Database performance issues are often rooted in subtle type mismatches that can silently disable query planners, especially when security features like RLS impose strict constraints on operator classification.
Takeaway: If you use Postgres RLS and observe unexpected index scans on date-based queries, explicitly typecast your comparisons to match your schema types or move entirely to 'timestamptz'.
Deep dive
  • Postgres 'timestamptz' does not store timezone information; it is simply a 'timestamp' that converts based on session settings.
  • Row Level Security (RLS) in Postgres forbids 'non-leakproof' operators to prevent sensitive data exposure.
  • Comparing a 'timestamp' to a 'timestamptz' is classified as non-leakproof, preventing the query planner from using btree indexes.
  • The issue occurred because Prisma defaulted to 'timestamp' while 'NOW()' returns 'timestamptz'.
  • Casting the comparison explicitly matches the types, satisfying the leakproof requirement and restoring index performance.
Decoder
  • Row Level Security (RLS): A Postgres feature that restricts which rows a user can access based on security policies, often adding hidden 'WHERE' clauses to queries.
  • Leakproof: A classification for functions/operators in Postgres that guarantees they will not reveal any data about hidden rows, even through error messages.
  • Btree Index: The default index type in Postgres, efficient for equality and range queries on ordered data.
Original article

People like to talk about how AI is making software engineers think less, but I’ve been finding exactly the opposite at Lorikeet. When I find an unusual problem, AI often teaches me something I would never have had time to learn otherwise. This week’s example: speeding up some Postgres queries by up to 1000x with a single typecast.

We use Prisma, which by default turns JS DateTimes into Postgres `timestamp` fields, though they can be annotated to `timestamptz` instead. Despite the name, `timestamptz` doesn’t actually store a timezone: both types use eight bytes, but `timestamptz` converts between your session timezone and UTC.

While speeding up our ticket processing, I found some very simple queries taking seconds: `owner = 'foo' AND createdAt > NOW() - INTERVAL '7 days'`

We had a btree index on `(owner, createdAt)`, so this should have been almost entirely answerable from the index. Postgres was even choosing that index, which was super strange. But it was a lie: the database was using just the owner prefix, then walking every row belonging to that owner to later filter on createdAt.

Here’s where I learned something fun: `NOW()` returns a timestamptz. Because `createdAt` is a timestamp, Postgres had to perform a cross-type comparison. In combination with Row Level Security, that meant it couldn’t use the `createdAt` portion of the index. Why would RLS affect a timestamp comparison? At Lorikeet, we use RLS as a second line of defence against data leaking between our users.

Postgres marks every function and operator as either "leakproof" or "not leakproof". A non-leakproof function might reveal something about a hidden row by throwing an error, so Postgres won’t let it be used when enforcing RLS. Comparing a `timestamp` with another `timestamp` is leakproof, but comparing a `timestamp` with a `timestamptz` isn't. Why? Historically, converting a `timestamp` near its minimum value into a `timestamptz` could underflow in certain UTC offsets. The error thrown leaks that the hidden value was near the type’s bound. That was fixed in 2020, but the function’s classification was never updated. Which was lucky! Just last month, someone found another bug where this comparison could produce incorrect index results during DST spring-forward gaps.

Our fix was tiny: `owner = 'foo' AND createdAt > NOW()::timestamp - INTERVAL '7 days'`

Our Postgres sessions use UTC, so this query is exactly the same as the original for us (we also had similar problems with comparisons to `${date}`, which Prisma would silently convert to a `timestamp`). That one cast made some queries up to 1000x faster, reduced database load, and sped up ticket processing for our users. The answer going forward for us is to use `timestamptz`, which avoids the problem entirely. I don’t usually get to go this far into the weeds of database performance, but this was a fun little adventure to share. If you’d like to work on this sort of thing with me, Lorikeet is hiring. Drop me a line!

Explore all 65 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi