Skip to content

Conversation

@subpath
Copy link
Collaborator

@subpath subpath commented Dec 29, 2025

What's new:

Prepared statements pre-parse and cache query execution plans on the database server, eliminating parsing overhead on repeated queries.

Instead of parsing the SQL string on every execution, the database reuses the prepared plan.

Benchmarks:

I've tested it locally with 100k random users

Implementation Mean (ms) Median (ms) P95 (ms) P99 (ms)
Raw (no prepared) 6.534 5.596 11.001 76.491
Prepared (cached) 3.629 3.352 8.245 11.449

methods were used in the benchmark:

get_challenge
get_key
store_challenge
delete_challenge
update_key_counter
store_key
delete_key

Performance improvement:

  • 1.8x faster in P50 (6.5 ms -> 3.6 ms)
  • 6.7x faster in P99 (76ms -> 11ms) - less outliers

def __init__(self, db_name: str):
self.db_name = db_name
self.db_url = os.path.join(env.PG_DB_URL, db_name)
self.db_url = f"{env.PG_DB_URL.rstrip('/')}/{db_name}"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: os.path.join technically works on Unix OS, but it is not the right way to use it, better use simple f strings

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? That's surprising, why are f strings preferred to using os.path.join?

conn._mlpa_stmt_cache = stmt_cache
if query not in stmt_cache:
stmt_cache[query] = await conn.prepare(query)
return stmt_cache[query]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this blow up the memory of our pods? Each request is stored until pod restart right? Is there a max_store variable we could add? Maybe store for up to 1 day? What do you think?

counter,
)
stmt = await self._get_prepared_statement(conn, query)
await stmt.execute(key_id_b64, counter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one example of SQL where I don't think it makes sense to cache, this is a very unique query that really ever only runs once given the key_id_b64 and counter unique pair

await self.pg.execute(
"DELETE FROM challenges WHERE key_id_b64 = $1", key_id_b64
)
async with self.pg.acquire() as conn:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also recommend maybe leave caching off of this one? Deleting a challenge is sort of a 1-time operation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless the caching takes place before the key_id_b64 is added? Or is the value added to the string, and then it's cached?

key_id_b64,
challenge,
)
stmt = await self._get_prepared_statement(conn, query)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another I'd recommend no caching on given on how unique each request is, unless I'm misunderstanding how the caching works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants