Nik: Hello, hello, this is Postgres.FM

It is Nik, as usual, PostgresAI,
and as usual, my co-host is

Michael pgMustard.

Hi, Michael.

And today we have a guest, which
I know for quite some time.

I think never met in person, but
somehow our paths crossed many

times when we write materials or
read other people's materials,

obviously, and also on social media.

It's Shaun Thomas from pgEdge.

Hello, Shaun.

Thank you for coming.

Shaun: Hi, Nik, Michael.

What's going on?

Nik: Yeah, and it took us some
time to choose the topic because

you obviously write about a lot
of various stuff.

Shaun: Yeah, it's a very eclectic
grab bag, but I like to make

sure that cover all the interesting
topics as they come up.

Nik: And I was tempted to choose
an AI-related topic, obviously,

because I know you also, since
the very beginning of GPT-4 and

so on, I know you're like very
actively use it, right?

Shaun: Yeah, a lot of that's your
fault, honestly.

You pulled me in, I was like, well,
that's really a lot more behind

the scenes than I would have realized.

Cool.

Nik: I'm glad that I influenced
a little bit.

That's great to hear.

Thank you.

But we chose very boring on 1 side,
like a very technical topic,

but I think what's important is
that this topic is very often

misunderstood, especially if you
don't spend every day in Postgres

internals a lot of hours, right?

So it's easy to forget how to tune
memory properly, how to deal

with work_mem, what to choose, and
pros and cons and trade-offs

about various choices.

So I think topic choice is great
here.

Do you remember when 1st time you
needed to tune work_mem?

It was long ago, right?

Shaun: Yeah, that's...

Some of these older settings, like,
there's something you fiddled

with sometime 20 years ago, and
I'm like, oh yeah, I did have

to fight with that a lot, but it
still comes up frequently, like

in the mailing lists and in message
boards and Discord, Slack,

wherever, there's always someone
that's wondering about how to

set it.

And I wouldn't say it's a black
art, but there's definitely some

ambiguity there.

Nik: I think It takes some significant
time to understand nuances

around it.

Because what people expect, obviously,
is that work_mem is some

limit for the whole thing.

It's like the number 1 problem.

And yeah, it's obviously not so.

So you published a blog post recently,
Let's Write Extension,

that will help tune work_mem.

I see it like 2 goals of this blog
post.

1 is let's write extension.

And honestly, my opinion is maybe
not popular, but I think we

should write extensions less because
they are not supported by

managed providers.

Unless it's pg_tle.

That's okay, because it will be
possible to run it anywhere.

And also, like yesterday, someone
posted how many vulnerabilities

various third-party extensions
bring.

Then platforms like Neon, Supabase
and others are badly affected.

So this is 1 more reason not to
write extension.

So We can spend some time there,
I think, to talk about writing

extensions, because it's still
useful.

Of course, my opinion is very practical.

Of course, we should write extensions,
but we should understand

that probably it will be very limited
in terms of use.

But obviously, the 2nd goal is
work_mem tuning.

And I'm very curious, and my goal
for myself today is to see

how our visions are aligned in
memory tuning.

In the beginning, I checked some
old EnterpriseDB posts and

I saw a formula, a very simple
formula.

And I'm very curious what you think
about this.

Let's take whole memory, divide
by 4, usually it's done with

shared_buffers, right?

And then divide by max_connections.

And this is our work_mem.

What do you think about this?

Shaun: So I actually did a blog
post like that while I was at

EDB, and I think my formula was
slightly different.

I said something like divide by
max_connections and divide by

5.

Because the assumption there was
every query could have some

amount of query nodes and since
each of those can create an instance

of work_mem, Assuming 1 is probably
a bad idea, because only the

most simple queries will only have
1 query node.

Nik: Yeah, so 5 is quite, like
I would say, conservative.

Shaun: Yeah, it's aggressive.

It's aggressively conservative
because I know that's, for 1,

that's how the Postgres community
tends to operate.

They use what they call sensible
defaults.

And it's an easy way to avoid out
of memorying yourself until

you can have more time to tweak
it.

Nik: That's interesting.

Also, if we think about both of
these approaches, and you're

obviously more advanced than that
simple 1.

Like we understand that each query
can consume multiple times

of work_mem.

If we go with this formula to RDS,
you know what will happen,

right?

Shaun: No, I don't.

What will RDS do?

Nik: max_connections, 5, 000.

Even for small clusters.

Oh, geez.

Yes.

Shaun: All right.

Nik: And they expect that everyone
will probably use RDS Proxy

But we observe a lot of customers
who come to us.

They don't know PgBouncer no,
this proxy only some poolers

or on application side And those
guys who run application nodes,

they put it to Kubernetes with
auto-scaling.

So nobody knows which, like, probability
of reaching those 5,

000 connections.

Shaun: Yeah, we could have a whole
another conversation on pooling.

My opinion there is like you should
never connect directly to

Postgres.

Nik: Right, yeah, poolers should
be inside.

Yeah, I agree.

But this is the reality and RDS
is the most popular 1 And people

come with 16 vCPUs, 2,500 or 5,000
max_connections.

Shaun: Yeah.

At that point you get to an area
where you have to say, sample

your number of active connections
and then it's a little more

involved, but it's roughly the
same idea.

Nik: Yeah.

So this is great.

And this is like for many years.

It was with number 1 like approach
We basically we tune based

on observations feedback loop,
right but I really want something

more like not looking into actual
thing, but predicting something,

right?

Because sometimes you launch new
service, or sometimes you expect

some growth, and observing production,
it just doesn't feel super,

it's very practical, of course.

And it can work in either way as
well.

For example, if we take your formula,
divide by 5, and even drop

that 5, just no additional dividing
at all.

And then we say, you know what,
we still need more memory.

And we just observe there's a lot
of like page cache is huge

Because work_mem is not allocated
immediately, right?

It's allocated in chunks like it's
gradually So work_mem is

limit for 1 operation inside query
But it doesn't like it doesn't

say that within a query, each
operation will have work_mem, maybe

less.

If we observe from reality that
we actually use much less, we

can over-commit here.

This is what we do.

Like we say, We go beyond theoretical
limits because we see that

practically we don't have out-of-memory
risks.

Theoretically we have them, but
practically no.

And I know such clusters, huge
clusters, they are running, like,

just ignoring this formula.

And it's okay because majority
of queries they actually use tiny

amount of work_mem.

What do you think about this?

Overall it's like a bag of very
different ideas and how to find

better like path to some recipe?

Shaun: The recipe I usually use
and the 1 I started with, honestly,

is to use the database statistics.

I think it's pg_stat_database, or
it could be in pg_database, where

it tracks the number of temp files
and the number of temp file

size.

So what I do is I find out the
average size of the temp files

Postgres is producing and then
I add that to whatever the work_mem

setting is.

So if it starts out at the default
4 megs and then I find out

that Postgres is producing on average
8 megabyte temporary files.

I'll add some padding onto that
and add that to the work_mem so

I end up with 12 or 16 would be
my setting.

And that reduces all your disk
spills by 90 plus percent and

that usually addresses the problem.

Nik: That's interesting.

Let's have some small pause here.

1st of all, obviously you take
shared blocks dirtied?

Oh, not shared, temp blocks dirtied,
right?

Shaun: Temp files and temp bytes,
like literally those 2 fields.

Nik: Ah, okay, in pg_stat_database.

I'm thinking about pg_stat_statements.

This is where I struggle, because
Postgres has a huge gap.

It doesn't have a number of calls.

It has only a number of transactions.

So you need to divide by calls
to have average temporary bytes

written per call, right?

But we don't have it in the
pg_stat_database.

Shaun: Yeah, you just have a very
coarse heuristic that just

says, I produced this many of temporary
files and these are how

big they were.

So you don't really have enough
fine-grained ability to see how

big they were like for each individual
thing.

Nik: Yeah, that's why I look at
pg_stat_statements because it has

calls.

Of course, it maybe tracks not
all queries, but by default only

5, 000.

pg_stat_statements.max is 5, 000.

But we have both temporary blocks
written and calls.

We can divide 1 by another and
get average per call.

But then I literally thought about
this last week, even before

your blog post, because we also,
we deal with temporary files

generation in many customers, it's
normal, they don't tune it

and then it becomes a problem.

Why it's a problem?

Because spilling to disk is slow,
right?

So it affects performance.

And I was thinking, OK, we have
average.

We know, for example, 8 megabytes,
as you said, like per call,

for example.

And we have 8 megabytes work_mem.

Thinking, OK, we will set up 16.

But then I was thinking, What if
it's average, right?

What if 90% of those queries are
tiny and the remaining 10, they

need 100 megabytes?

Shaun: Yeah, I usually run into
situations like that.

You just kind of have to live with
the disk spilling.

You obviously don't want to get
it past a certain amount where

your queries are having to fight
for disk I/O.

But at the same time, if the average
is too high, you can't make

it infinitely high.

Nik: Yeah.

So it's a problem of averages and
lack of percentiles again here.

Right.

Michael: I've seen once you understand
your workload, I've seen

some people tweak it on a per user.

Instead of setting it globally,
keep the global low, but then

set an application, like if there's
a reporting application that

we know only ever fires off 1 or
2 queries at a time, set its

work_mem higher.

Have you seen things like that
or done that in the past, Shaun?

Shaun: I haven't, but that would
definitely be another way out

of it.

The fact that you can set a user
level session GUCs is a way

out of a lot of ways.

In the past I've used that to turn
off nested looping for example.

Back when the planner wasn't so
great and it still occasionally

runs into stuff like this, you
get a query that just resists

every attempt to get to run with
the same plan.

So you just, You take out a hammer
and you knock out the parts

you don't want to do and magically
it starts working.

Now that's not so much a problem
with Postgres 19 coming with

the new hint syntax So you can
tell it to prefer certain paths

but before that you had a CTE or
some kind of wall of materialization

you had to construct to make it
act a certain way.

And 1 of those is, yeah, the coarse
knobs of adjusting the planner

or working with work_mem or some
other thing.

Nik: Yeah, and It's interesting
that work_mem is not, if you select

star from pg_settings, work_mem is
not sitting in the Planner category.

It's about memory.

But it affects how Planner chooses
the plan.

So it's also an interesting thing.

It might change the plan.

So yeah, and the good thing that
you can set either at session

level or at user level as well,
alter role.

So I don't think people use it
often, but it's a good thing for

some heavy queries to move this
workload to a different user

completely.

But also this makes understanding
of the whole picture harder,

slightly.

Shaun: Yeah, but if you have a
mixed use case system, you have

to choose your battles, right?

Nik: So

Shaun: I think Michael's point
is very salient because if you

have an OLTP system that's taking,
that's 90% of doing what it's

doing and every once in a while
a huge query sneaks in where

you've got a batch job you need
to run like setting for the long

batch job, the transaction length
to some larger amount.

So there's lots of other different
settings you might do for

that 1 particular thing that you
wouldn't want to change for

the whole server.

So usually in a case like that
I'd say make a reporting system

so you can direct your OLTP stuff
there but that's a architectural

thing for a later time.

Michael: 1 thing that's changed
in recent years is the introduction

of hash_mem_multiplier and the change
of the default of it from

1 to 2.

It was really great in your blog
post to see you taking that

into account when estimating how
much a single query is going

to take, but I haven't seen that
in many other blog posts even

since it was changed.

So do you think maybe some of the
formulas you might come across

via search or even LLMs these days,
might be maybe not even conservative

enough because there's this multiplier
now for quite a few operations.

Shaun: Yeah, the problem with using
the hash_mem_multiplier

as part of a generic formula is,
Unless you know how many hash

memory operations are going to
occur within a query, you're just

guessing.

At least with the number of nodes,
you can say, I'll say there's

like a 4 or 5 average or a 2 or
3 average or whatever it is for

your platform, you can say, just
multiply it out.

But with hash_mem_multiplier, it's just, it's
another factor that complicates

the formula in a way that's not
really meaningful.

Because we're already estimating,
right?

We're already just picking a number
out of the sky about how

many query nodes and how many things
that's going to be in your

estimate.

And even the extension that I wrote
is very coarse, it's just

Not every hash node will be 2x.

Not every, it doesn't handle
Appends, it doesn't walk the entire

tree.

Like it was a 1st approximation
to give a guess at a worst case

scenario.

Nik: Worst case scenario is a key
here, yeah, I agree.

Michael: I understand that, but
I think the worst case scenario

has just got worse since the, so
the multiplier of 2 means everybody's

worst-case scenario has got worse
because those nodes could well

could take more memory now.

Shaun: To an extent but I believe
the reason they actually put

that there was to constrain it,
constrain memory more.

Previously, I could have this wrong,
but I remember a couple

of threads in the mailing list
where someone was saying that

hash memory was unbounded by work_mem
at all.

So it was causing them to get OOMs
because 1 query would create

an infinitely large hash and it
would just crash the system.

So now they actually, hash mems,
honor a limit which they didn't

before.

So yes and no.

Nik: Yeah, and your extension is
looking at the plan, right?

So it knows exactly how many hashing
happened and other operations.

That's a great idea.

I think it's possible to apply
it at scale.

For example, at least if, very
roughly, we talk about estimates.

What if we take all queries from
pg_stat_statements, 5, 000

by default, maximum, and just collect
generic plans.

Yeah.

That's it, right?

So your extension, obviously, it
was like at a micro level, just

a single query, right?

And for those who haven't seen,
it's just producing some additional

hints, log messages, hints what
peak usage for this query could

be.

And then you can understand, like
for session level, at least

for this query, you can understand
how to avoid spilling to temporary

files, to disk.

But we can apply it to macro level,
right?

With just collecting generic plans.

Of course, it won't be super precise
because custom plans might

have very different opinion how
many hashing or ordering should

happen.

But it can be good enough for estimate,
I think.

Do you agree?

Shaun: Yeah.

So the way I wrote it was, the
extension was basically to exercise

an example of, here's how you write
a function that a user can

call.

Here's how you can leverage GUCs.

Here's how you can override hooks.

But because of all that, yeah,
you can, you've got to use a little

function you can call and it's
like a debugging process.

I've got this huge query, I might
want to run in production.

Will it blow things up?

Oh, it could use up to a gigabyte
of memory.

Whoops, let me go back and look
at that again.

Or maybe I want to adjust my
work_mem to make it use less memory

overall in general.

But then yeah, you could in theory
point whatever's in pg_stat_statements

at it and just send
them all through the function

in the loop, or just as part of
a query call and get some kind

of maximum estimate out of that.

Nik: Yeah, that's I think quite
doable.

And it could help to tune better,
actually, right?

Yeah, that's good that you aligned
here as well.

I have a very interesting specific
question.

Maybe you or Michael knows.

So Why does Postgres crash when
it reaches, like when there is

not enough memory?

Instead of crashing 1 backend somehow,
right?

It crashes the whole cluster.

Of course, if it's crashed the
backend abnormally, I understand

like for protection.

We need this to restart.

I understand that.

But I know RDS had some extension
or logic, proprietary 1, to

just to give up only those backends
which cannot be, cannot like

switch to memory, but not to lose
others.

But then they somehow removed it,
deprecated.

And I'm very curious what's happening
in this area and why Postgres

cannot protect other, like others
already running give them a

chance to complete, right?

Do you know, no?

Like, I'm very curious about this.

I think

Michael: it's a Linux thing.

Correct me if I'm wrong, Shaun,
but I think it's the out-of-memory

killer and the fact we've got the
postmaster.

Shaun: So yeah, if you've got a
backend that's using a lot of

RAM and Postgres gets killed arbitrarily
by the kernel, sure,

that'll cause it to complain about
stuff.

And I believe if there's no memory
available and you do a memory

request, you'll just get back an
error and your backend won't

die.

I need to run a test to see actually
what happens if you run

into a situation like that where
you've turned off over commit,

for example, so you actually get
an out of memory error from

your backend if it tries to allocate
more than is available.

But yeah, I think that's, I think
what it is that the OOM killer

terminates the backend, and then
since that backend can't clean

up after itself, you could have
corrupted memory context because

Postgres uses arenas with shared
memory context.

And if you can't roll back your
arena back or clean out your

context, the entire shared state
of the buffers could be corrupt

in some way that you can't really
consider.

So as a defensive measure, Postgres
shuts the entire thing down

and then restarts.

But aside from that I'm not entirely
sure.

We need someone who's worked with
it.

Andres or Masahiko Sawada, they would
know.

Or maybe Álvaro.

Nik: So yeah, I practically I would
prefer to have errors for

specific sessions, but all others
are still working.

And if we see that we're about
to be out of memory, that's practically

more convenient.

Similar case is with out-of-disk
space.

It's not a good place to be.

And I know some people place some
file filled by zeros and they

just remove it if urgency happens,
right?

Anyway, yeah, that's interesting
question as well.

Good, okay.

Speaking of writing extensions,
I have a feeling 20 years ago,

Postgres is extensible, extensions
are great, and then managed

services, managed providers, they
just somehow brought us to

the point when extensions are against
extensibility, because

they need to approve it.

And it's like with all those scary
stories about vulnerabilities,

their approval rates will slow
down.

Shaun: Yeah, so the issue with
Postgres extensions is they're

very much a double-edged sword.

It adds this new functionality
that Postgres never had, and that's

amazing.

We wouldn't have pgvector, for
example, right now, or PostGIS

or any of those other huge extensions
everyone relies on now,

without it.

But at the same time, the Postgres
extension mechanisms, and

I now know this first-hand since
I've played with it with my

tutorials, is awful.

Like, for example, the hook system.

There's no, like you normally see
register hook, some kind of

function you'd call that would
put your hook on a stack and it

would control that whatever the
hooks are called in, you wouldn't

be able to accidentally not call
the next extension in the list.

Right now, you actually have to
check and see if there's a next

or previous hook.

If there is, save it for later
and call it yourself in your extension.

And if you don't do that, or if
someone decides they want to

be a bad actor and don't do that
in their extension, the whole

stack gets broken.

The other issue is there's no sandbox.

Even browsers, right, just some
user's browser they're using

to browse the web has a sandbox
per thing, and Postgres has no

even concept of that right now.

So you end up with CVEs that get
escalated into the core because

the Postgres extensions are literally
calling core structures.

There's no API for any of this
stuff.

It's just, oh, you know Postgres
function?

Go ahead and include it and call
it internally.

You have direct core access to
every part of Postgres all the

time.

Nik: This would be great to fix,
by the way.

Do you know anyone looking in this
direction?

Shaun: I don't, but I can't imagine
that nobody's thought about

this or nobody's thought to, can
we add sandboxing or can we

add a hook management mechanism
of some kind.

But it's just low priority because
right now they, all the core

developers work on core Postgres
and their interest in extensions

is tangential at best, right?

Because if they want something,
they just put it in the core,

and if someone else wants something,
they can add it for their

own convenience.

Nik: And the core just experienced
the biggest, in terms of CVEs

fixed, the biggest minor release,
28.

Shaun: Oh, the 28 of the 28 CVEs
they fixed in 19, yeah.

Nik: 18.6, yeah.

Shaun: Yeah, right, 18.6.

And then 18.5 never came out because
it got reverted.

Nik: Which happened last time,
it happened in 2008, yeah.

Shaun: It's been a while.

Nik: Yeah, Yeah, obviously.

But exactly this question triggers
the question about will the

next minor release have even more
CVEs?

And this triggers the next question,
what about extensions we

have on managed providers?

Or not managed providers, just
if we'll stay close to focus.

Shaun: Not only will the next 1
have more, it's going to climb

exponentially, in my opinion.

Because, not just because of the
visibility, because obviously

the 28 number sounds high and people
are like, we should probably

take a look at this now and there's
gonna be more eyes on it,

but because of AI.

You said you didn't want, we weren't
gonna focus on that, but

if we're being honest here, everyone
is pointing their AI agent

at everything they possibly can.

And now those can find bugs that
we never would have thought

about because we just didn't have
time or priority or there wasn't

a test case for it.

And now there's infinite amount
of research area there.

So not only are we going to see
that, we're going to see it accelerate.

Michael: Is it infinite?

I think it's finite, isn't it?

Nik: There should be a plateau
at some point.

Michael: The number might be high,
and I have no idea if 28 is

getting even close to the right
order of magnitude per release

but it feels like it will accelerate
for a while and then hopefully

slow down?

Shaun: Yeah that's a valid point
but I think at least in the

short term we are definitely going
to see an acceleration.

28 is just a drop in the bucket
because I don't know about being

honest is the right way to phrase
this, but Postgres being written

in C has a lot of potential for
edge cases that we just haven't

even looked at.

You have fuzzers, you have memory
leak checkers, you've got pointer

checks that are done right now
by various different mechanisms

we have.

We've got our build farms.

There's lots of stuff to catch
this early, but you don't miss

28 CVEs because you're dumb.

It's just there are new attack
vectors that nobody thought of

and extensions open that up even
more.

So the popular extensions are going
to be also attack vectors

and since extensions themselves
are a way to get into the Postgres

core, if you exploit an extension,
you have access to core.

So that even adds more vectors
that either they have to start

to find a way to enforce a sandbox
or it's going to make that

discussion a lot more germane.

And I saw someone post on X, it
was a security researcher, and

he's going to do a four-part or
a five-part series on how he

broke into PostGIS, for example.

And he escalated that all the way
to a root RCE where he could

arbitrarily affect files on the
file system from PostGIS.

And that's just part 1 of 5.

And he's actually going to slowly
escalate the amount of attacks

he does all the way to the point
of getting root on the container

through Kubernetes.

So it's just ridiculous the amount
of analysis that people have

really not done to this point because
it just wasn't on their

radar.

But now it is and it has to be
because everyone and their dog

has an AI that can be like oh I'll
just attack this 24 hours

a day 7 days a week until I find
something.

Nik: We should say thank you to
Anthropic and OpenAI for limiting

capabilities.

You cannot do it with their latest
models right now.

They will like completely say I'm
not continuing.

Shaun: Yeah, those models, but
anyone who has a modern Qwen or

K3 or any of the local models that's
been put through a LoRA

that is uncensored, and then if
you have access to, I don't know,

$50, 000 worth of equipment, You
can direct it at anything.

Nik: That's interesting, because
I saw Anthropic was mentioned

a few times in the release notes
for those minor releases, but

I don't remember any other AI systems
mentioned.

Shaun: They're just the ones that
people are focusing on because

they're the frontier models, but
anything that's been open sourced

can be repurposed.

Nik: Yeah, yeah,

Shaun: yeah.

We're getting a little bit off
the topic, but the reason extensions

are at this point and Postgres,
and actually really any software

now, is because of AI, for good
or ill.

Nik: Yeah, maybe let's connect
topics so we could use AI to tune

work_mem.

Shaun: Yeah, I actually used it
to find all the bugs in my extension

because my extension was a proof
of concept, right?

It's just, here, you can do this.

It's kind of fun, throwaway kind
of material.

So it found all the issues in it.

Oh, you're not looking at append,
you're not looking at whatever,

you can use the built-in plan walker
and get all these extra

things you missed.

And oh, you don't realize that
if you run out with these inputs

it causes your extension to crash,
which takes down the

backend.

And it found dozens of things I
could fix, were I so inclined.

So anyone can do that to their
extension.

And it's a huge help.

Nik: And should do.

Shaun: Should, yes, also.

Nik: At this point, already, yeah.

Shaun: Because the amount of eyes,
like, the thing is, even in

our company, we don't have my pgEdge
Ansible project that I

work on for doing distributions
of architectures, there's like

maybe 2 other people in the company
that can help me do code

review on that.

But I always have Claude available
to look over things or

CodeRabbit or whatever tool you
want to use and they'll catch

stuff that I didn't consider.

Michael: I had a question on things
you considered.

I liked that it was simple, I liked
the formulas I could follow

them, the examples I could follow
them, But I did wonder if you,

like for example, sequential scans
were adding 1 times work_mem,

like it was every node type it
seemed.

Shaun: Yeah, like it was a very
naive, like I just, anything

that was a node I counted it, I
just didn't even like check,

The only thing that I gave an extra
bonus to was anything that

had the word hash in it.

Like literally, I just grepped for
the word hash for the node types

and I just put them all on that
big list.

But yeah, like that was the naive
approach.

A more refined approach would be
to actually go through and figure

out which nodes actually do what.

Because the parent hash node is
not where the memory gets allocated,

it's actually the child hash elements.

So I was actually double counting
the hash nodes, ironically

inflating the results.

So Yeah, obviously a good opportunity
there would be to spend

some time refining the algorithm,
but my worst case scenario

was just like, let's just count
all the nodes and then multiply

and then you get like a, here's
the maximum amount this thing

could possibly take.

It's off by a little bit, but it's
better than the estimates

we've been relying on.

And it was a semi-useful kind of
extension and it demonstrated

the process of writing an extension,
except for the fact that

I didn't create a memory context.

Michael: 1 more question was Back
to what you said at the start

around using max_connections as
a multiplier I wondered it wouldn't

be the same formula, but I wondered
if instead it might be sensible

to use some multiple of the number
of cores The reason I came

to that was thinking parallelism
like a parallel plan could use

multiple times the multiple work_mems.

So like I've seen sorts as part
of a parallel plan use kind of

4 or 5 times work_mem, But obviously
that's a much lower number

in most cases, so it'd be a very
different formula.

But I wondered if there was any
merit to that maybe in formulas.

Shaun: So, yeah, like, in that
case you would look at your max

parallel_workers_per_gather option,
because that's the maximum

number of cores that it would actually
leverage in a single query.

And then multiply that by the number
of backends.

But yeah, you could do that.

Michael: Yeah.

Just thinking if you've got 16
cores and you've got a bunch of

queries trying to fire off lots
of parallel workers.

The 1st few might get all of the
workers they want, but the next

ones are only going to get a single
1.

Like, it won't let you run more
than.

Shaun: Yeah, yeah, that would act
as a cap.

Michael: Have you ever done anything
like that, Nik?

Nik: Yeah, I barely understand
what you're saying, Michael.

Shaun: I think he was asking if
you had used the number of cores

as part of your estimate for work_mem.

And I kind of get where he's going
with it, because even if you

set max_parallel_workers_per_gather
to limit the amount, eventually

they'll run out of, you'll hit
your max_parallel_workers.

Nik: Right, not vCPU count, max_parallel_workers.

Postgres has no idea how many cores
or how much.

Shaun: Right, but which is why
you set the max_parallel_workers

and various other settings so you
wouldn't exceed it.

Nik: Why?

Like we can exceed it, we can exceed
it.

Shaun: You can, you can, you can.

I wouldn't recommend it.

Nik: I agree with you But looking
at guys who come to us like

16 or 32 cores and max_connections
5000, I already have a shift

in my mind.

I cannot convince them.

We spent a lot of efforts saying
this max_connections is abnormal.

Especially before Postgres 13,
14 when Andres Freund improved

work with snapshots, which is very
related to memory consumption,

right?

Yeah, we tried like max_connections
should be like 3, 4 times

more than vCPU count.

Shaun: That's what everyone says,
but...

Nik: No, not anymore.

We don't say it anymore because
since Postgres 14, it's much

better.

And I think I had some tests showing,
we should revisit this

by the way.

I want to revisit with benchmarks
and see exactly how it degrades.

But now it degrades less.

Shaun: Yeah, it's not nearly as
bad.

You still have to fight the kernel
process table, but it's not

as bad as it was.

Nik: Right, so this is just reality.

These guys come to us with RDS
and telling them that you need

to reduce max_connections drastically,
you need restart for it.

And if they don't have proper database
side pooler, we cannot

convince them to get rid of huge
amount of idle connections.

They just need them to satisfy
application guys needs because

those guys, as I said, they scale
their application nodes like

this, especially e-commerce when
Black Friday happens, they just

need to scale.

Or some new system as well, like
social media, they need to be

able to scale and they need those
idle connections.

So this, back to work_mem here,
I don't know, it depends.

Also, sometimes we need to reproduce
plans in an environment

which is much weaker physically
than production, because we study

behavior of Postgres.

And we are okay for some contention
happening in terms of physical

resources, But we want the planner
to behave exactly like in

production.

So there are some nuances.

But I agree with you.

Overall, I would like to see average
number of sessions below

vCPU count.

This is great.

Shaun: Yeah, that's usually that's
what I still tell people.

Not because necessarily they're
going to see a huge drop in performance

because it's let if we let's face
it's going to be around

the 20 or 30 percent mark at maximum
even if they're sending

hundreds of thousands but it's
still I would say a best practice

to do so

Michael: yeah I'm going Maybe going
back to basics a little bit,

when you're tuning work_mem, is
it always to do with latency?

Just like speed of queries on average?

Or are we sometimes trying to look
after the disks a little bit?

Are we trying to increase headroom
there a little bit?

Or is it mostly just user-facing
query times?

Shaun: I guess it depends on what
hat you're wearing.

As a DBA, you're just like, I don't
want my database to crash,

or I don't want the hardware to
burst into flames because it's

being misused in some way.

From that perspective you're like,
okay, I'll set it to be just

enough that I avoid lots of disk
spilling and then causing disk

wear or really slow latency.

But there's also a point of diminishing
returns, right?

If you set it to some infinitely
high amount, you're not gaining

anything out of it.

All you're really doing is making
it so your maximum is higher

for no reason, and you end up getting
more risk of an out-of-memory

error.

Really, it's just 1 of those things
like Nik had said, it's

How do we set it properly without
going overboard?

How do we go do it without going
too little?

And part of that is taking what
you have and using heuristics

to come up with some kind of reasonable
number, like using pg_stat_statements

and sending it
through some kind of estimation

process.

Or I found out that apparently
if you send the query through

the pre-execution step, it actually
calculates all the memory

that it would allocate, but it
doesn't allocate it yet.

So in theory I could walk the plan
nodes and actually get the

estimates directly from the planner,
whereas my approach was

very coarse and it just did it
based on the node types, you could

get it directly from the plan output,
from the executor step

itself, from the pre-executor.

And you could actually pull those
bits of data and actually get

an exact number of what the allocator
would have actually asked

for from Postgres.

So a better approach I would say,
at least as far as revising

my extension, would be to say use
the built-in plan walker, do

it after the pre-execution steps
so you have all the estimates

of memory usage that it would have
done in the 1st place, and

sum those totals instead.

Then what you end up with is a
real estimate of what all the

queries would have taken without
them executing.

And then you can use that to design
your ideal work_mem based

on your amount of average active
backends and whatnot.

But it's just 1 of those things
with Postgres.

You have to always go back and
retroactively examine how your

system's been operating.

And a lot of that is observation.

Do you have a dashboard?

Do you have observability and visibility
across your entire cluster

to see what, how it's actually
operating?

And that should be how you drive
your systems.

If you see that you're always running
out of memory at some point,

look at your memory settings, look
at shared_buffers, look at

work_mem, look at anything that
could possibly allocate stuff

and then maybe reduce it a little
bit if it's running out of

memory or increase it if it's not.

It's a delicate balancing act and
unfortunately there's no 1

size fits all way to addressing
everything, which is why there's

so many guides, there's blogs and
tutorials and videos and everything

galore of how to do it, and no
1 really can agree on 1 final

answer.

Nik: There is no official runbook
how-to documentation.

This is sad actually, but I can
imagine how hard it would be

to achieve consensus on the concrete
protocol.

Actually, we somehow avoided the
topic of swap as well.

We could enable swap, right?

This is like instead of temporary
file for each query, let's

enable on that far end.

If we achieve that, let's swap
there, 1 thing.

And another thing, like I just
resonate very, like a lot with

your words about, for example,
maintenance_work_mem, which usually

is inherited by autovacuum_work_mem,
being minus 1, right?

And then we tell everyone we should
have more workers for autovacuum

workers.

And then Postgres 17 silently,
like unexpectedly, lifts unspoken

limit 1 gigabyte.

It was not like obvious that we
actually were limited, but guys

already raised maintenance_work_mem
to say 8 gigabytes and

raise number of workers to say
25.

And now we have interesting memory
allocation for autovacuum,

which we didn't want.

So now we say raise the number
of autovacuum workers, but also

limit autovacuum_work_mem by 1
gigabyte, because when you will

upgrade to 17, so Layers of logic,
yeah.

Shaun: I totally forgot about maintenance_work_mem
and autovacuum_work_mem

because those, you don't
really think about those because

the 1 that really bites everyone
is work_mem because they set

it to some value and then it explodes
on them.

But yeah, the other 2 definitely
are a factor.

Nik: Swap, like swap, like I always
try to avoid swap on Postgres

machines, I remember, but it's
very old.

I haven't revisited this topic
because I remember dealing with

database, Postgres database, which
experiences heavy swap.

Since then, I always avoided it.

But then I remember Bruce Momjian
said, we should just, small

swap is good.

I said, no, it is completely avoided.

I would better see out of memory
and fix my memory settings and

so on.

What's your opinion about having
swap enabled on the machine

with Postgres?

Shaun: The problem I usually see
with swap is you can't really

account for what the kernel or
the memory pressure systems will

do.

And I've had problems in the past
with previous kernels doing

things that they shouldn't.

So I try to get as much control
as I can.

So in that case, I usually set
swappiness to 1, because if you

set it too low or to 0, the memory
pressure systems go wonky.

And then I set to some low amount,
like 2 gigs, 4 gigs, some

token amount just so the kernel
has area to work with.

Then I set overcommit_memory to
2 so you can't overcommit.

And then I set overcommit_kbytes
to the exact amount of physical

memory that there is on the system.

So it will not use swap because
it basically can't, because it's

been subtracted from the total.

Any allocation has to be physically
backed by actual RAM, at

least as far as the database is
concerned, so you don't end up

with overcommit and the OOM killer
won't kick in because there's

not anything using too much RAM,
because it can't.

If you make a request, you simply
get denied.

And then your backend will
go, oh, I can't allocate memory,

so I won't run this query for you.

And I'd rather have a failed query
and have someone have to go

back and look at their query or
revise it or whatever, then take

the system down because OOM killer
decided that there's a rogue

process.

So basically, it always comes down
to, as a DBA, getting as much

control as you can over the system
and enforcing it stringently,

which is a little harder to do
in the Kubernetes context because

those limits aren't enforced the
same way.

Nik: Right, and there is 1 more
point.

We somehow also avoided an important
topic.

We could plan everything very well,
shared_buffers,

maintenance_work_mem for index creation,
autovacuum workers, then our backends

with work_mem, But somehow we forget
about page cache.

And in many systems, page cache
is super important.

Sometimes, like if it's, for example,
if we had a situation when

it was exceeding shared_buffers,
for example, a simple example,

we perform minor upgrade, we restart
server, and we don't think

about pre-warming because actually
there is page cache sitting

there, which helps us to have better
performance sooner.

We recently had an internal discussion
about that.

Do we need to have a pre-warming,
pg_prewarm, automated or not

automated?

And the question is, if there is
huge page cache, probably we

don't need to bother.

But if we start tuning work_mem,
page cache will become thin and

very narrow, right?

And this can be a problem as well.

So it's very tricky, right?

To think about macro level and
everything.

Shaun: That definitely directs
how you will want to choose your

hardware because 1 of my talks
at Postgres Open, I think it was

2012, was about our woes with trying
to get pre-warming working.

We would have a Postgres crash
because we were using EDB at the

time and there were, we were using
a couple of extensions that

from EDB that were, let's just
say beta quality, but they were

doing something we needed.

So occasionally we'd get a crash,
fine, whatever.

So the database goes down, suddenly
our shared_buffers have been

invalidated, but we were also finding
that the page cache was

not sufficient because a lot of
those backends, at least at the

time, were Postgres backends and
they had their minimum memory

allocation and The page caches
were too small because of all

that.

You end up with Postgres RAM, backend
allocations, and then a

small functional page cache.

So what ended up happening is,
after the crash, Postgres would

take an hour to warm up based on
user queries.

So that whole time your latency
jumps by like 20 times because

you're running off of an old RAID
array or something.

So our fix was to get a Fusion-io
drive, which would be equivalent

to a 100, 000 IOPS device from EBS
or something, like an io2,

or just a really high-end NVMe
M.2 stick or something.

Nik: Local NVMe, yeah.

It's great.

Shaun: It was really the only way
out at the time because you'd

see the spike of activity and the
I/O usage from iostat, right?

It would shoot up to 100% usage
for, and it would just stay there

for an hour.

As soon as we upgraded to the newer
device, it would spike once

in the beginning, as soon as the
crash was over, and then it

would just hover around 20%.

But that 20% was on a 100, 000 IOPS
device.

So if it were anything less than
that, it would be a lot worse.

So even page cache can't really
save you in certain circumstances.

It really depends on your workload.

And with Postgres needing 0.25
of your RAM, well, not needing,

we recommend using 0.25 of your
RAM up to a certain limit.

If your page cache is too big,
then you're essentially double

buffering.

Nik: Yeah, and you have much higher
likelihood that temporary

files spilling to disk will happen,
right?

So this is the whole point to avoid.

This, the case you just described
exactly like what I'm saying

about, and Having faster disks
definitely helps.

These days we can have millions
of IOPS with local NVMEs, right?

But I also think if we tune work_mem,
so we make page cache quite

thin, and we need to think about
how fast or slow disks are.

And if we know they are slow, then
maybe we should consider automatic

pre-warming.

Because pg_prewarm right now supports
automated pre-warming, so

it can capture maybe this is the
exact moment, slow disks and

small page cache and which should
start.

This actually adds to the whole
picture of tuning work_mem, right?

Shaun: Yeah, and back when this
happened, pg_prewarm wasn't really

a thing.

So I cheated by just like using
dd.

So I queried the catalog, figured
out which backend files went

with the most used tables, and
I would just, before I started

the server, I would dd them all
into memory and then I would

start Postgres.

And then that solved 80% of the
problem, but that wasn't sustainable

long term, so that's why we bought
the storage.

But there's ways you can get around
it.

So definitely pg_prewarm.

It's an extension people don't
really think about, because it's

not really a problem so much anymore,
because everyone's got

infinite IOPS, it's roughly.

But if you don't, and you don't
want to pay io2 fees or load

up your system with expensive storage,
then pre-warming is still

an option.

Nik: I agree with you and like
bigger databases with local NVMes,

it's like we know a few companies
who bet on it heavily, right?

But also there are many more smaller
clusters, usually single

node clusters, which are needed
to support some AI building,

AI builders products.

They just experiment a lot and
they don't need serious database,

but they still need some database.

And in that case, tuning, like
what we just discussed could be

used for them because tuning work_mem,
so queries are good enough

in terms of performance, but also
you know that restart will

help you survive not being super
slow for 0.5 an hour.

So I think there are interesting
cases and this case with smaller

databases because of AI, I think
they will grow a lot.

Shaun: Well, I mean, that's actually
a good point, because especially

if you have database branching,
like I know that you have in

your product, you've got the ability
to fork off, like dev instances

of databases, and those are entirely
cold.

Without proper backing on them,
they're going to be slow for

a while, at least after start.

In the

Nik: case of DBLab, it's ZFS, and
ZFS has ARC.

We usually allocate 0.5 of memory
to it, so Those blocks are

warmed up already usually, so that
helps a lot.

We have a different concern.

Developers ask us, can you implement
cold cache?

Because we study EXPLAIN plans,
we want cold cache to see how

the worst case.

And this is tricky in this architecture
because it's a multi-tenant

thing.

A lot of Postgres exploration happening
on the same VM and we

need that cache actually.

Shaun: Yeah, how would you even
do that?

You'd have to move it over to another
instance, and that's cold.

Nik: Yeah, you cannot do it without
losing the common cache.

But common cache helps others,
because we have different use

cases.

It's not always exploration of
EXPLAIN plans, but also sometimes

just preview environments for testing.

And those guys want better performance.

So it's a complex topic.

But we learned very early, actually,
that we should match work_mem

to production because it affects
the planner behavior, which

I mentioned.

This is important for any lab environment.

Okay, I'm out of questions.

We touched a lot, like we went
quite broadly, touched a lot of

additional questions.

Thank you so much.

It was a very interesting discussion.

I especially like you confirmed
a lot of things I have in my

head, so it's great to hear confirmation.

But also learned a lot of new stuff,
thank you so much.

I will follow your new blog posts,
don't stop writing, it's very

interesting always.

Shaun: Yeah, I don't plan to.

And I always like to remind people
that if you've ever heard

of Perl as the pathologically eclectic
rubbish lister, that's

basically how my brain works.

Nik: Okay, okay.

Great, yeah.

Michael: Well, really nice to meet
you.

Thanks for joining us.

Nik: Thank you.

Shaun: You too.

Hope you have a good day.

Nik: You have a great week.

Creators and Guests

Some kind things our listeners have said