Postgres FM

With Nikolay taking a well-earned break, Michael covers the basics of EXPLAIN — what it is, the parameters available, and a process he recommends for using it.
Here are some links to the things mentioned:


What did you like or not like? What should we discuss next time? Let us know via a YouTube comment, on social media, or by commenting on our Google doc!


Postgres FM is brought to you by:
With special thanks to:

Creators & Guests

Michael Christofides
Founder of pgMustard

What is Postgres FM?

A weekly podcast about all things PostgreSQL

Hello and welcome to PostgresFM,
a weekly show about

all things PostgreSQL.

I am Michael, founder of pgMustard
and today I am not joined

by Nikolay.

He is finally taking a well-earned

So this week you've just got me.

I am in the process of getting
a couple of guests as well, which

I'm excited about.

So hopefully for the next couple
of weeks we'll be able to give

you those conversations.

But in the meantime I'm going to
record an episode on pretty

much the only topic I feel comfortable
sharing knowledge on my

own, which is EXPLAIN.

And due to the nature of the product
I've been working on for

the past few years, I have spent
a lot of time looking at EXPLAIN

plans and helping folks with those.

So yeah, I've given a couple of
talks on this in the past, but

I realized we hadn't actually done
a podcast episode on it.

We've done a few related ones,
so long-term listeners will be

familiar with some of the things
we've talked about previously,

but we haven't done a very, an
intro level one or even I have a

process I'd like to recommend people

I haven't discussed that yet, so
I'm hoping to give you a couple

of those things.

But on the topic of guests, before
I move on, if you have any

recommendations or requests on
either the guest front or the

topic front, those are always welcome.

Please send those to us either
privately or publicly.

They'd be very, very welcome.

So EXPLAIN first, what is EXPLAIN
and what does it do?

So EXPLAIN is a statement that
you put in front of your query

and PostgreSQL will return the query
execution plan.

So by default you just get the

So what PostgreSQL would do, what
choices it would make to execute

that query.

It doesn't execute the query if
you just use EXPLAIN and it provides

back the estimated costs involved.

So you might have heard that PostgreSQL
has a cost-based optimizer.

This gives you some insight into
the costs that it's calculating

and why it's making certain choices.

So why it's choosing a certain
scan type over another scan

type or the join order it's
chosen or the join type it's

doing for a specific join.

So all of those are based on what's
possible and also what it

thinks will be fastest.

Now, the costs are in an arbitrary
unit, but the idea is the

lower the cost, the faster the
optimizer thinks it would be.

So, it is correlated to time, but
they're not in a unit of time.

So, EXPLAIN on its own can be useful
to quickly see, for example,

if you have a query that's timing
out or taking absolutely ages,

you can quickly have a look at
the query plan and just check

maybe why it might be slow.

But if we want to know for sure
why a query is slow and it's

executing in a reasonable amount
of time, chances are we're going

to want to use some EXPLAIN parameters
to not just get the plan

back but also run the query behind
the scenes and get execution


So the most common of those parameters
that you might have heard

alongside EXPLAIN is ANALYZE.

So and this is different from the
ANALYZE command for getting query


This is EXPLAIN ANALYZE specifically,
but there are loads more

parameters as well.

In fact, we did a whole episode
on why we believe, or Nikolay

and I believe, that BUFFERS should
be turned on by default with ANALYZE.

So BUFFERS is another parameter
that gives you extra runtime

or execution statistics alongside
the timings that you get with


So with BUFFERS, we also get information
about the blocks read,


So the amount of work being done
by the query, not just the timings.

So that can really help.

But there are a bunch of other
parameters as well.

I like to recommend people use
as many of them as they can.

Most of them don't have that much
additional overhead.


In fact, that's something worth
bearing in mind, that there are

a couple of caveats when it comes

It can both overreport and underreport the amount of time that

a query would take in the wild.

So for example, it's been referred
to as the observer effect

before, but if we have a slow system
clock, the overhead of checking

the timings can add significant
overhead to the total execution

time when we're running EXPLAIN
with ANALYZE and specifically

with timings on.

So, that's how it can overreport.

That's the easiest way it can overreport
on the execution time.

But it can also underreport if
for example, when we're

running EXPLAIN ANALYZE, Postgres
doesn't have to transmit the data

to us.

So if we're requesting thousands
of rows, and we're not close

to the database, so perhaps it's
a user doing a big report from

a different continent than the
database is on, your monitoring

might be showing that that query
is taking a few seconds to execute,

whereas maybe EXPLAIN ANALYZE would
report it only taking a few

hundred milliseconds because it
doesn't have to transmit that

data across the network.

So it can overreport and underreport,
but generally it's generally

that's pretty uncommon and also
directionally you'll still see

where the slowest parts of that
query plan are, even if the numbers

aren't precise.

So yeah, so those are the caveats.

In terms of the process I recommend,
the number 1, I think I've

mentioned it before in the podcast,
but it's so important, it's

worth repeating.

You need to be running on a realistic
size dataset to get realistic

performance data.

Now, it doesn't have to be the
exact same data.

The important part is the number
of rows.

So for example, if you were testing
out a new feature and your

local dataset only has a couple
of dozen rows, but production

has a few million, chances are
the Postgres plan is going to

be making different choices on
join order or join algorithm,

even scan type.

So it could even choose to do a
sequential scan if your data

is small enough, even if there
is a good index available.

So that can really surprise people,
but it's really important.

As the datasets get bigger, this
becomes slightly less important.

So if you're testing on tens of
millions of rows instead of hundreds

of millions, there's less of a
chance of an issue but still a

chance of an issue getting a different

So very much worth bearing that
in mind.

If you can test on a realistic
size dataset, please do.

There's lots of tools available
for doing that and for masking

data these days.

But that's, in fact, I think if
we've done an episode on that,

I think we have on benchmarking.

So I'll link that one up.

So that's tip number 1, or step
1 in the process.

Make sure you are, or the person
that you're helping, is testing

on a realistic dataset.

Use as many parameters as possible.

I've hinted at that already.

Mentioned ANALYZE and BUFFERS,
but there are also parameters

for settings, which tells you any
non-default planner-related

settings that you have.

So for example, if you've changed
work mem or random page cost,

these are common ones to have changed,
those will show up.

But also if you, for example, been
testing a few locally and

you've forgotten that you've still
got those set, they'll show

up in your EXPLAIN plan as well
which can be helpful.

Another important one is, well, another
one I find really useful

is verbose, especially when helping
other people, you get to

see information about the schema,
for example, you get fully

qualified object names, you also
get the output of each operation

in the query execution plan, what
data it's sending to its parent

operation, which can be useful
for advising certain optimizations.

A couple of others, a new one is
the write-ahead logging information,

or I say new, I think it's a few
versions old now.

You can also specify different

So JSON format is particularly
popular these days amongst tools,

but text format is generally still
the most concise and most

readable amongst especially people
that are most familiar with


They'll often ask to see the text
format plan because it's smaller

and more familiar to them.

Now there are a couple of other
parameters as well.

Less important because people generally
leave them on or leave

the defaults in place but timing
can be really useful if you

want to, for example, turn timing
information off but have ANALYZE


So you still get the full execution
timing statistics but

you don't measure the per node
timing, which is where the main

overhead is so I've seen some people
with some tool vendors recommend

turning that off with auto_explain
but worth measuring if it

does actually have any overhead
on your system before doing so

because timing information could
be really useful when looking

at speeding things up.

Costs you can turn off as well.

The only time I've seen that done
and the only time I've done

it myself is when writing blog
posts or sharing query plans where

I just want to make them slightly
less intimidating or draw attention

to the more important parts. So
those are on by default and the

other one that's on by default is
or at least when you use EXPLAIN

(ANALYZE) is summary, which puts
the information at the end.

So like planning timings and execution
times, which are very


So generally people don't turn
that off, but it means you can

turn that on, for example, with
EXPLAIN, which gives you the

planning time.

Because remember when we just do
EXPLAIN, we only plan the query,

we don't, Postgres doesn't execute

So you can get the planning time
just from EXPLAIN alone if you

use the summary parameter.

But generally I recommend putting
as many of those on as you

can, especially if you're using
a tool to parse it or if you

are getting help from others, the
more information the better

for them.

Cool, so that's the second step.

The third one is glance at the end.

So query plans, especially once
your queries get a bit more complex,

can get quite long.

You can get, feel a bit like a
wall of text.

But a few of the probably rarer
problems, to be honest, but a

few of the fairly common problems
are highlighted very much at

the end.

So things like, if your planning
time is 10X your execution time,

there's very little point looking
through the rest of the query

plan, looking for optimization
potential, because the vast majority

of the total time was in planning.

So that's only reported at the
end and the rest of the query

plan won't help you.

The other two things that are reported
at the end that also report

a total time are triggers.

So trigger executions, they can
be the performance issue.

If you have triggers involved,
there's going to be some overhead


They report a total time.

If that's a high percentage of
your total execution time, you

found your problem already without
looking through the whole

execution plan.

Same is true for just-in-time compilation.

That can in certain cases be 99%
of execution time, which again,

makes looking through the query
plan not as useful.


So that's the third one.

Fourth is then once you've looked
at those, then figure out,

okay, we do need to look at the
rest of the query plan.

Where is the time going?

Which parts of these are the most

Or if you have BUFFERS on, which
of these scans is doing the

most reads?

The reason I mention that that's
like step 4 is because I think

it's very easy at this point to
jump to other, like jump to looking

to other issues and I've been guilty
of this in the past myself

as well.

So experienced people, I think
maybe even more susceptible to

this than less experienced people,
if you notice a sequential

scan, your eye gets drawn to it
immediately and you think, oh,

maybe there's an issue there, or
you notice a large filter, or

a few thousand rows being filtered,
or a lot of loops or something.

If your eye gets drawn to these
problems that you're used to

looking for, you can get tripped
up or get, you can end up going

down a bit of a rabbit hole on

So my tip is to really check where
the timings go, like where

which of these is really taking
up the most time and then focus

your attention there.

Only once you've worked that out
focus on that part of the query

or that part of the query plan.

Is there a bottleneck we can solve?

And maybe we'll come back to the
other issue we spotted later,

but also the query might be fast
enough by that point and we

can move on.

So, yeah, that's the fourth and

So work out where the what the
most expensive parts of the query

plan are and then and only then
what you can do about it.

So on the timings front, there
are a few things to be aware of.

So the execution time is reported
inclusive of so 1 operation's

execution time is inclusive of
all of its children.

So a little bit of subtraction
you'd think might be enough there.

to work out maybe which of the
operations is most expensive on

its own.

But you have to take into account
a bunch of other things as


So, those execution times are a
per loop average.

So, for example, if one of our operations
is being looped over

multiple times, which is especially
common in join operations.

But also, parallel execution utilizes
loops as how it reports.

So, there's a few ways where we
have to then multiply it by the

number of loops before subtracting
from the parent operation.

So these things, you can see how
they can start to get a little

bit complex.

In addition, Postgres reports these
timings in milliseconds and

doesn't include thousands separators.

So I know I personally can start
to struggle with the arithmetic

of all that once it starts getting
into the multiple seconds.

As such, there's a ton of tools
that can help you with this.

I work on one, but there are some
really good free open source

ones available.

Lots of editors include them by
default and pretty much all of

them make at least some effort
to get those timings pretty good.

So some popular ones are Depesz
EXPLAIN, a really nice

thing about that is that it preserves
the text format.

So it's really expert friendly
if you want to share an EXPLAIN

plan with the Postgres mailing
lists, the one they'll be most familiar

with and most comfortable with
will be the Depesz EXPLAIN one.

But EXPLAIN Dalibo is really
popular these days, much more

visual, includes more charts on,
for example, where the timings

are going, where the BUFFERS, like
which operations do the most

buffer hits, that kind of thing.

Much more beginner friendly, and
as I would probably class my

tool as well, pgMustard is visual as
well, but also helps with that

next step a bit more on what you
can possibly do about things.

But yeah, so the main reason I want to mention those

is they can really help on the
timings front.

So you can paste your query plan
in and quickly see where the

timings are going.

I would say that a lot of them
don't do that step 3.

So a lot of them don't immediately
draw your attention if there's

triggers taking up a lot of time
or just in time compilation

taking up a lot of time or even
planning time.

So it is still worth doing that
step first and then checking

out a tool or two that you're familiar
with for the next steps.

Yeah, that's pretty much it for
the process.

Obviously, that fifth step, working
out what to do next, is a

hugely deep topic.

And it's actually where a lot of
the normal talks on EXPLAIN,

if you go to a conference and see
a talk on EXPLAIN, that's normally

where they'll pick up.

So the best one I've seen is a bit
old now.

It's from probably about 2016.

But Josh Berkus gave it a few times.

But I'll share one of the recordings
that I liked a lot.

Yeah, he spent about an hour going
through a lot of the different

join types, scan types, types of
issues you can see, things to

be aware of.

And yeah, it's a lot, even then,
probably only covers the first

10% of what you could know.

There are more recent talks, one
by Bruce Momjian actually, who'd

covered some of the rarer scan

So I'll link that one up as well.

But this is a deep, deep topic
that you can dive into.

I'm a few years in and I don't
consider myself an expert yet.

So that gives you an idea.

But yeah, it's an extremely powerful
tool for query optimization

in general.

Naturally, to speed things up,
it really helps to know which

part's slow.

So please use it.

Let me know how you get on.

It'd be cool to hear.

We've done a bunch of related episodes,
so a couple of those

to check out are there's an extension
called auto_explain.

Now, that will let you run your
queries as normal, let's say,

maybe even on production, and the
users get the results of the

query back, which we don't when
we run EXPLAIN.

I don't know if I mentioned that
up top.

But the people using your database
will continue to run their

queries and get their results as

But in the background, the execution
plans will be being logged,

any beyond a certain timing threshold
that you set will get logged

to your Postgres logs.

So we've done a whole episode on

Can be extremely useful.

And I'll link that up.

We've also done one on row estimates.

So that's another common issue.

So with EXPLAIN, we get to see
how many rows Postgres expected

to be returned at each stage.

And when it's wrong, when it's
wrong by a lot especially, that

can lead to all sorts of issues
with inefficient plans being


So we did a whole episode on that.

In fact, our first ever episode
was on slow queries and slow


So that's worth checking out if
you haven't already.

And we did a more advanced one on
query optimization, which again,

we still didn't cover some of the
basics of explaining.

So I'm glad to have done this one

Welcome any questions.

And please do remember to send
your guest and topic suggestions


Happy holidays, hope you all had
a good one and catch you next

time, hopefully with a guest.


Hopefully with a guest.