postgres_fdw: using TABLESAMPLE to collect remote sample
Hi,
here's a small patch modifying postgres_fdw to use TABLESAMPLE to
collect sample when running ANALYZE on a foreign table. Currently the
sampling happens locally, i.e. we fetch all data from the remote server
and then pick rows. But that's hugely expensive for large relations
and/or with limited network bandwidth, of course.
Alexander Lepikhov mentioned this in [1]/messages/by-id/bdb0bea2-a0da-1f1d-5c92-96ff90c198eb@postgrespro.ru, but it was actually proposed
by Stephen in 2020 [2]/messages/by-id/20200829162231.GE29590@tamriel.snowman.net but no patch even materialized.
So here we go. The patch does a very simple thing - it uses TABLESAMPLE
to collect/transfer just a small sample from the remote node, saving
both CPU and network.
And indeed, that helps quite a bit:
---------------------------------------------------------------------
create table t (a int);
Time: 10.223 ms
insert into t select i from generate_series(1,10000000) s(i);
Time: 552.207 ms
analyze t;
Time: 310.445 ms
CREATE FOREIGN TABLE ft (a int)
SERVER foreign_server
OPTIONS (schema_name 'public', table_name 't');
Time: 4.838 ms
analyze ft;
WARNING: SQL: DECLARE c1 CURSOR FOR SELECT a FROM public.t TABLESAMPLE
SYSTEM(0.375001)
Time: 44.632 ms
alter foreign table ft options (sample 'false');
Time: 4.821 ms
analyze ft;
WARNING: SQL: DECLARE c1 CURSOR FOR SELECT a FROM public.t
Time: 6690.425 ms (00:06.690)
---------------------------------------------------------------------
6690ms without sampling, and 44ms with sampling - quite an improvement.
Of course, the difference may be much smaller/larger in other cases.
Now, there's a couple issues ...
Firstly, the FDW API forces a bit strange division of work between
different methods and duplicating some of it (and it's already mentioned
in postgresAnalyzeForeignTable). But that's minor and can be fixed.
The other issue is which sampling method to use - we have SYSTEM and
BERNOULLI built in, and the tsm_system_rows as an extension (and _time,
but that's not very useful here). I guess we'd use one of the built-in
ones, because that'll work on more systems out of the box.
But that leads to the main issue - determining the fraction of rows to
sample. We know how many rows we want to sample, but we have no idea how
many rows there are in total. We can look at reltuples, but we can't be
sure how accurate / up-to-date that value is.
The patch just trusts it unless it's obviously bogus (-1, 0, etc.) and
applies some simple sanity checks, but I wonder if we need to do more
(e.g. look at relation size and adjust reltuples by current/relpages).
FWIW this is yet a bit more convoluted when analyzing partitioned table
with foreign partitions, because we only ever look at relation sizes to
determine how many rows to sample from each partition.
regards
[1]: /messages/by-id/bdb0bea2-a0da-1f1d-5c92-96ff90c198eb@postgrespro.ru
/messages/by-id/bdb0bea2-a0da-1f1d-5c92-96ff90c198eb@postgrespro.ru
[2]: /messages/by-id/20200829162231.GE29590@tamriel.snowman.net
/messages/by-id/20200829162231.GE29590@tamriel.snowman.net
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
postgres-fdw-analyze-sample-20220211.patchtext/x-patch; charset=UTF-8; name=postgres-fdw-analyze-sample-20220211.patchDownload+224-4
Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
So here we go. The patch does a very simple thing - it uses TABLESAMPLE
to collect/transfer just a small sample from the remote node, saving
both CPU and network.
This is great if the remote end has TABLESAMPLE, but pre-9.5 servers
don't, and postgres_fdw is supposed to still work with old servers.
So you need some conditionality for that.
regards, tom lane
On Fri, Feb 11, 2022 at 12:39 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
So here we go. The patch does a very simple thing - it uses TABLESAMPLE
to collect/transfer just a small sample from the remote node, saving
both CPU and network.This is great if the remote end has TABLESAMPLE, but pre-9.5 servers
don't, and postgres_fdw is supposed to still work with old servers.
So you need some conditionality for that.
I think it's going to be necessary to compromise on that at some
point. I don't, for example, think it would be reasonable for
postgres_fdw to have detailed knowledge of which operators can be
pushed down as a function of the remote PostgreSQL version. Nor do I
think that we care about whether this works at all against, say,
PostgreSQL 8.0. I'm not sure where it's reasonable to draw a line and
say we're not going to expend any more effort, and maybe 15 with 9.5
is a small enough gap that we still care at least somewhat about
compatibility. But even that is not 100% obvious to me.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
I think it's going to be necessary to compromise on that at some
point.
Sure. The existing postgres_fdw documentation [1]https://www.postgresql.org/docs/devel/postgres-fdw.html#id-1.11.7.45.17 already addresses
this issue:
postgres_fdw can be used with remote servers dating back to PostgreSQL
8.3. Read-only capability is available back to 8.1. A limitation
however is that postgres_fdw generally assumes that immutable built-in
functions and operators are safe to send to the remote server for
execution, if they appear in a WHERE clause for a foreign table. Thus,
a built-in function that was added since the remote server's release
might be sent to it for execution, resulting in “function does not
exist” or a similar error. This type of failure can be worked around
by rewriting the query, for example by embedding the foreign table
reference in a sub-SELECT with OFFSET 0 as an optimization fence, and
placing the problematic function or operator outside the sub-SELECT.
While I'm not opposed to moving those goalposts at some point,
I think pushing them all the way up to 9.5 for this one easily-fixed
problem is not very reasonable.
Given other recent discussion, an argument could be made for moving
the cutoff to 9.2, on the grounds that it's too hard to test against
anything older. But that still leaves us needing a version check
if we want to use TABLESAMPLE.
regards, tom lane
[1]: https://www.postgresql.org/docs/devel/postgres-fdw.html#id-1.11.7.45.17
On Fri, Feb 11, 2022 at 1:06 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
While I'm not opposed to moving those goalposts at some point,
I think pushing them all the way up to 9.5 for this one easily-fixed
problem is not very reasonable.Given other recent discussion, an argument could be made for moving
the cutoff to 9.2, on the grounds that it's too hard to test against
anything older. But that still leaves us needing a version check
if we want to use TABLESAMPLE.
OK, thanks.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 2/11/22 19:13, Robert Haas wrote:
On Fri, Feb 11, 2022 at 1:06 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
While I'm not opposed to moving those goalposts at some point,
I think pushing them all the way up to 9.5 for this one easily-fixed
problem is not very reasonable.Given other recent discussion, an argument could be made for moving
the cutoff to 9.2, on the grounds that it's too hard to test against
anything older. But that still leaves us needing a version check
if we want to use TABLESAMPLE.OK, thanks.
Yeah, I think checking server_version is fairly simple. Which is mostly
why I haven't done anything about that in the submitted patch, because
the other issues (what fraction to sample) seemed more important.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Hello,
I find it a great idea to use TABLESAMPLE in postgres_fdw ANALYZE.
Let me offer you some ideas how to resolve you problems.
Tomas Vondra < tomas.vondra@enterprisedb.com > writes:
The other issue is which sampling method to use - we have SYSTEM and
BERNOULLI built in, and the tsm_system_rows as an extension (and _time,
but that's not very useful here). I guess we'd use one of the built-in
ones, because that'll work on more systems out of the box.
It’s hard to choose between speed and quality, but I think we need
SYSTEM method here. This patch is for speeding-up ANALYZE,
and SYSTEM method will faster than BERNOULLI on fraction
values to 50%.
But that leads to the main issue - determining the fraction of rows to
sample. We know how many rows we want to sample, but we have no idea how
many rows there are in total. We can look at reltuples, but we can't be
sure how accurate / up-to-date that value is.The patch just trusts it unless it's obviously bogus (-1, 0, etc.) and
applies some simple sanity checks, but I wonder if we need to do more
(e.g. look at relation size and adjust reltuples by current/relpages).
I found a query on Stackoverflow (it does similar thing to what
estimate_rel_size does) that may help with it. So that makes
tsm_system_rows unnecessary.
----------------------------------------------------------------------
SELECT
(CASE WHEN relpages = 0 THEN float8 '0'
ELSE reltuples / relpages END
* (pg_relation_size(oid) / pg_catalog.current_setting('block_size')::int)
)::bigint
FROM pg_class c
WHERE c.oid = 'tablename'::regclass;
----------------------------------------------------------------------
And one more refactoring note. New function deparseAnalyzeSampleSql
duplicates function deparseAnalyzeSql (is previous in file deparse.c)
except for the new last line. I guess it would be better to add new
parameter — double sample_frac — to existing function
deparseAnalyzeSql and use it as a condition for adding
"TABLESAMPLE SYSTEM..." to SQL query (set it to zero when
do_sample is false). Or you may also add do_sample as a parameter to
deparseAnalyzeSql, but as for me that’s redundantly.
Stackoverflow: https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql
--
Sofia Kopikova
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company
Hi,
here's a slightly updated version of the patch series. The 0001 part
adds tracking of server_version_num, so that it's possible to enable
other features depending on it. In this case it's used to decide whether
TABLESAMPLE is supported.
The 0002 part modifies the sampling. I realized we can do something
similar even on pre-9.5 releases, by running "WHERE random() < $1". Not
perfect, because it still has to read the whole table, but still better
than also sending it over the network.
There's a "sample" option for foreign server/table, which can be used to
disable the sampling if needed.
A simple measurement on a table with 10M rows, on localhost.
old: 6600ms
random: 450ms
tablesample: 40ms (system)
tablesample: 200ms (bernoulli)
Local analyze takes ~190ms, so that's quite close.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
0001-postgres_fdw-track-server-version-for-conne-20220218.patchtext/x-patch; charset=UTF-8; name=0001-postgres_fdw-track-server-version-for-conne-20220218.patchDownload+46-1
0002-postgres_fdw-sample-data-on-remote-node-for-20220218.patchtext/x-patch; charset=UTF-8; name=0002-postgres_fdw-sample-data-on-remote-node-for-20220218.patchDownload+306-6
On 2022/02/18 22:28, Tomas Vondra wrote:
Hi,
here's a slightly updated version of the patch series.
Thanks for updating the patches!
The 0001 part
adds tracking of server_version_num, so that it's possible to enable
other features depending on it.
Like configure_remote_session() does, can't we use PQserverVersion() instead of implementing new function GetServerVersion()?
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On 2/22/22 01:36, Fujii Masao wrote:
On 2022/02/18 22:28, Tomas Vondra wrote:
Hi,
here's a slightly updated version of the patch series.
Thanks for updating the patches!
The 0001 part
adds tracking of server_version_num, so that it's possible to enable
other features depending on it.Like configure_remote_session() does, can't we use PQserverVersion()
instead of implementing new function GetServerVersion()?
Ah! My knowledge of libpq is limited, so I wasn't sure this function
exists. It'll simplify the patch.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 2/22/22 21:12, Tomas Vondra wrote:
On 2/22/22 01:36, Fujii Masao wrote:
On 2022/02/18 22:28, Tomas Vondra wrote:
Hi,
here's a slightly updated version of the patch series.
Thanks for updating the patches!
The 0001 part
adds tracking of server_version_num, so that it's possible to enable
other features depending on it.Like configure_remote_session() does, can't we use PQserverVersion()
instead of implementing new function GetServerVersion()?Ah! My knowledge of libpq is limited, so I wasn't sure this function
exists. It'll simplify the patch.
And here's the slightly simplified patch, without the part adding the
unnecessary GetServerVersion() function.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
0001-postgres_fdw-sample-data-on-remote-node-for-20220222.patchtext/x-patch; charset=UTF-8; name=0001-postgres_fdw-sample-data-on-remote-node-for-20220222.patchDownload+304-5
Hi,
On 2022-02-23 00:51:24 +0100, Tomas Vondra wrote:
And here's the slightly simplified patch, without the part adding the
unnecessary GetServerVersion() function.
Doesn't apply anymore: http://cfbot.cputube.org/patch_37_3535.log
Marked as waiting-on-author.
Greetings,
Andres Freund
Andres Freund <andres@anarazel.de> writes:
On 2022-02-23 00:51:24 +0100, Tomas Vondra wrote:
And here's the slightly simplified patch, without the part adding the
unnecessary GetServerVersion() function.
Doesn't apply anymore: http://cfbot.cputube.org/patch_37_3535.log
Marked as waiting-on-author.
Here's a rebased version that should at least pass regression tests.
I've not reviewed it in any detail, but:
* I'm not really on board with defaulting to SYSTEM sample method,
and definitely not on board with not allowing any other choice.
We don't know enough about the situation in a remote table to be
confident that potentially-nonrandom sampling is OK. So personally
I'd default to BERNOULLI, which is more reliable and seems plenty fast
enough given your upthread results. It could be an idea to extend the
sample option to be like "sample [ = methodname ]", if you want more
flexibility, but I'd be happy leaving that for another time.
* The code depending on reltuples is broken in recent server versions,
and might give useless results in older ones too (if reltuples =
relpages = 0). Ideally we'd retrieve all of reltuples, relpages, and
pg_relation_size(rel), and do the same calculation the planner does.
Not sure if pg_relation_size() exists far enough back though.
* Copying-and-pasting all of deparseAnalyzeSql (twice!) seems pretty
bletcherous. Why not call that and then add a WHERE clause to its
result, or just add some parameters to it so it can do that itself?
* More attention to updating relevant comments would be appropriate,
eg here you've not bothered to fix the adjacent falsified comment:
/* We've retrieved all living tuples from foreign server. */
- *totalrows = astate.samplerows;
+ if (do_sample)
+ *totalrows = reltuples;
+ else
+ *totalrows = astate.samplerows;
* Needs docs obviously. I'm not sure if the existing regression
testing covers the new code adequately or if we need more cases.
Having said that much, I'm going to leave it in Waiting on Author
state.
regards, tom lane
Attachments:
0001-postgres_fdw-sample-data-on-remote-node-for-20220716.patchtext/x-diff; charset=us-ascii; name*0=0001-postgres_fdw-sample-data-on-remote-node-for-20220716.p; name*1=atchDownload+303-5
On 7/16/22 23:57, Tom Lane wrote:
Andres Freund <andres@anarazel.de> writes:
On 2022-02-23 00:51:24 +0100, Tomas Vondra wrote:
And here's the slightly simplified patch, without the part adding the
unnecessary GetServerVersion() function.Doesn't apply anymore: http://cfbot.cputube.org/patch_37_3535.log
Marked as waiting-on-author.Here's a rebased version that should at least pass regression tests.
Thanks. I've been hacking on this over the past few days, and by
coincidence I've been improving exactly the stuff you've pointed out in
the review. 0001 is just the original patch rebased, 0002 includes all
the various changes.
I've not reviewed it in any detail, but:
* I'm not really on board with defaulting to SYSTEM sample method,
and definitely not on board with not allowing any other choice.
We don't know enough about the situation in a remote table to be
confident that potentially-nonrandom sampling is OK. So personally
I'd default to BERNOULLI, which is more reliable and seems plenty fast
enough given your upthread results. It could be an idea to extend the
sample option to be like "sample [ = methodname ]", if you want more
flexibility, but I'd be happy leaving that for another time.
I agree, I came roughly to the same conclusion, so I replaced the simple
on/off option with these options:
off - Disables the remote sampling, so we just fetch everything and do
sampling on the local node, just like today.
random - Remote sampling, but "naive" implementation using random()
function. The advantage is this reduces the amount of data we need to
transfer, but it still reads the whole table. This should work for all
server versions, I believe.
system - TABLESAMPLE system method.
bernoulli - TABLESAMOLE bernoulli (default for 9.5+)
auto - picks bernoulli on 9.5+, random on older servers.
I'm not sure about custom TABLESAMPLE methods - that adds more
complexity to detect if it's installed, it's trickier to decide what's
the best choice (for "auto" to make decide), and the parameter is also
different (e.g. system_rows uses number of rows vs. sampling rate).
* The code depending on reltuples is broken in recent server versions,
and might give useless results in older ones too (if reltuples =
relpages = 0). Ideally we'd retrieve all of reltuples, relpages, and
pg_relation_size(rel), and do the same calculation the planner does.
Not sure if pg_relation_size() exists far enough back though.
Yes, I noticed that too, and the reworked code should deal with this
reltuples=0 (by just disabling remote sampling).
I haven't implemented the reltuples/relpages correction yet, but I don't
think compatibility would be an issue - deparseAnalyzeSizeSql() already
calls pg_relation_size(), after all.
FWIW it seems a bit weird being so careful about adjusting reltuples,
when acquire_inherited_sample_rows() only really looks at relpages when
deciding how many rows to sample from each partition. If our goal is to
use a more accurate reltuples, maybe we should do that in the first step
already. Otherwise we may end up build with a sample that does not
reflect sizes of the partitions correctly.
Of course, the sample rate also matters for non-partitioned tables.
* Copying-and-pasting all of deparseAnalyzeSql (twice!) seems pretty
bletcherous. Why not call that and then add a WHERE clause to its
result, or just add some parameters to it so it can do that itself?
Right. I ended up refactoring this into a single function, with a
"method" parameter that determines if/how we do the remote sampling.
* More attention to updating relevant comments would be appropriate,
eg here you've not bothered to fix the adjacent falsified comment:/* We've retrieved all living tuples from foreign server. */ - *totalrows = astate.samplerows; + if (do_sample) + *totalrows = reltuples; + else + *totalrows = astate.samplerows;
Yep, fixed.
* Needs docs obviously. I'm not sure if the existing regression
testing covers the new code adequately or if we need more cases.
Yep, I added the "sampling_method" to postgres-fdw.sgml.
Having said that much, I'm going to leave it in Waiting on Author
state.
Thanks. I'll switch this to "needs review" now.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
0001-postgres_fdw-sample-data-on-remote-node-for-20220718.patchtext/x-patch; charset=UTF-8; name=0001-postgres_fdw-sample-data-on-remote-node-for-20220718.patchDownload+304-5
0002-rework-postgres_fdw-analyze-sampling-20220718.patchtext/x-patch; charset=UTF-8; name=0002-rework-postgres_fdw-analyze-sampling-20220718.patchDownload+289-174
Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
Thanks. I'll switch this to "needs review" now.
OK, I looked through this, and attach some review suggestions in the
form of a delta patch. (0001 below is your two patches merged, 0002
is my delta.) A lot of the delta is comment-smithing, but not all.
After reflection I think that what you've got, ie use reltuples but
don't try to sample if reltuples <= 0, is just fine. The remote
would only have reltuples <= 0 in a never-analyzed table, which
shouldn't be a situation that persists for long unless the table
is tiny. Also, if reltuples is in error, the way to bet is that
it's too small thanks to the table having been enlarged. But
an error in that direction doesn't hurt us: we'll overestimate
the required sample_frac and pull back more data than we need,
but we'll still end up with a valid sample of the right size.
So I doubt it's worth the complication to try to correct based
on relpages etc. (Note that any such correction would almost
certainly end in increasing our estimate of reltuples. But
it's safer to have an underestimate than an overestimate.)
I messed around with the sample_frac choosing logic slightly,
to make it skip pointless calculations if we decide right off
the bat to disable sampling. That way we don't need to worry
about avoiding zero divide, nor do we have to wonder if any
of the later calculations could misbehave.
I left your logic about "disable if saving fewer than 100 rows"
alone, but I have to wonder if using an absolute threshold rather
than a relative one is well-conceived. Sampling at a rate of
99.9 percent seems pretty pointless, but this code is perfectly
capable of accepting that if reltuples is big enough. So
personally I'd do that more like
if (sample_frac > 0.95)
method = ANALYZE_SAMPLE_OFF;
which is simpler and would also eliminate the need for the previous
range-clamp step. I'm not sure what the right cutoff is, but
your "100 tuples" constant is just as arbitrary.
I rearranged the docs patch too. Where you had it, analyze_sampling
was between fdw_startup_cost/fdw_tuple_cost and the following para
discussing them, which didn't seem to me to flow well at all. I ended
up putting analyze_sampling in its own separate list. You could almost
make a case for giving it its own <sect3>, but I concluded that was
probably overkill.
One thing I'm not happy about, but did not touch here, is the expense
of the test cases you added. On my machine, that adds a full 10% to
the already excessively long runtime of postgres_fdw.sql --- and I
do not think it's buying us anything. It is not this module's job
to test whether bernoulli sampling works on partitioned tables.
I think you should just add enough to make sure we exercise the
relevant code paths in postgres_fdw itself.
With these issues addressed, I think this'd be committable.
regards, tom lane
On 7/18/22 20:45, Tom Lane wrote:
Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
Thanks. I'll switch this to "needs review" now.
OK, I looked through this, and attach some review suggestions in the
form of a delta patch. (0001 below is your two patches merged, 0002
is my delta.) A lot of the delta is comment-smithing, but not all.
Thanks!
After reflection I think that what you've got, ie use reltuples but
don't try to sample if reltuples <= 0, is just fine. The remote
would only have reltuples <= 0 in a never-analyzed table, which
shouldn't be a situation that persists for long unless the table
is tiny. Also, if reltuples is in error, the way to bet is that
it's too small thanks to the table having been enlarged. But
an error in that direction doesn't hurt us: we'll overestimate
the required sample_frac and pull back more data than we need,
but we'll still end up with a valid sample of the right size.
So I doubt it's worth the complication to try to correct based
on relpages etc. (Note that any such correction would almost
certainly end in increasing our estimate of reltuples. But
it's safer to have an underestimate than an overestimate.)
I mostly agree, particularly for the non-partitioned case.
I we want to improve sampling for partitioned cases (where the foreign
table is just one of many partitions), I think we'd have to rework how
we determine sample size for each partition. Now we simply calculate
that from relpages, which seems quite fragile (different amounts of
bloat, different tuple densities) and somewhat strange for FDW serves
that don't use the same "page" concept.
So it may easily happen we determine bogus sample sizes for each
partition. The difficulties when calculating the sample_frac is just a
secondary issue.
OTOH the concept of a "row" seems way more general, so perhaps
acquire_inherited_sample_rows should use reltuples, and if we want to do
correction it should happen at this stage already.
I messed around with the sample_frac choosing logic slightly,
to make it skip pointless calculations if we decide right off
the bat to disable sampling. That way we don't need to worry
about avoiding zero divide, nor do we have to wonder if any
of the later calculations could misbehave.
Thanks.
I left your logic about "disable if saving fewer than 100 rows"
alone, but I have to wonder if using an absolute threshold rather
than a relative one is well-conceived. Sampling at a rate of
99.9 percent seems pretty pointless, but this code is perfectly
capable of accepting that if reltuples is big enough. So
personally I'd do that more likeif (sample_frac > 0.95)
method = ANALYZE_SAMPLE_OFF;which is simpler and would also eliminate the need for the previous
range-clamp step. I'm not sure what the right cutoff is, but
your "100 tuples" constant is just as arbitrary.
I agree there probably is not much difference between a threshold on
sample_frac directly and number of rows, at least in general. My
reasoning for switching to "100 rows" is that in most cases the network
transfer is probably more costly than "local costs", and 5% may be quite
a few rows (particularly with higher statistics target). I guess the
proper approach would be to make some simple costing, but that seems
like an overkill.
I rearranged the docs patch too. Where you had it, analyze_sampling
was between fdw_startup_cost/fdw_tuple_cost and the following para
discussing them, which didn't seem to me to flow well at all. I ended
up putting analyze_sampling in its own separate list. You could almost
make a case for giving it its own <sect3>, but I concluded that was
probably overkill.
Thanks.
One thing I'm not happy about, but did not touch here, is the expense
of the test cases you added. On my machine, that adds a full 10% to
the already excessively long runtime of postgres_fdw.sql --- and I
do not think it's buying us anything. It is not this module's job
to test whether bernoulli sampling works on partitioned tables.
I think you should just add enough to make sure we exercise the
relevant code paths in postgres_fdw itself.
Right, I should have commented on that. The purpose of those tests was
verifying that if we change the sampling method on server/table, the
generated query changes accordingly, etc. But that's a bit futile
because we don't have a good way of verifying what query was used - it
worked during development, as I added elog(WARNING).
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Tomas Vondra <tomas.vondra@enterprisedb.com> writes:
I we want to improve sampling for partitioned cases (where the foreign
table is just one of many partitions), I think we'd have to rework how
we determine sample size for each partition. Now we simply calculate
that from relpages, which seems quite fragile (different amounts of
bloat, different tuple densities) and somewhat strange for FDW serves
that don't use the same "page" concept.
So it may easily happen we determine bogus sample sizes for each
partition. The difficulties when calculating the sample_frac is just a
secondary issue.
OTOH the concept of a "row" seems way more general, so perhaps
acquire_inherited_sample_rows should use reltuples, and if we want to do
correction it should happen at this stage already.
Yeah, there's definitely something to be said for changing that to be
based on rowcount estimates instead of physical size. I think it's
a matter for a different patch though, and not a reason to hold up
this one.
regards, tom lane
Hi!
Here's an updated patch, including all the changes proposed by Tom in
his review. There were two more points left [1], namely:
1) The useless test added to postgres_fdw.sql consuming a lot of time.
I decided to just rip this out and replace it with a much simpler test,
that simply changes the sampling method and does ANALYZE. Ideally we'd
also verify we actually generated the right SQL query to be executed on
the remote server, but there doesn't seem to be a good way to do that
(e.g. we can't do EXPLAIN to show the "Remote SQL" on the "ANALYZE").
So I guess just changing the method and running ANALYZE will have to be
enough for now (coverage report should at least tell us we got to all
the SQL variants).
2) the logic disabling sampling when the fraction gets too high
I based the condition on the absolute number of "unsampled" rows, Tom
suggested maybe it should be just a simple condition on sample_frac
(e.g. 95%). But neither of us was sure what would be a good value.
So I did a couple tests to get at least some idea what would be a good
threshold, and it turns out the threshold is mostly useless. I'll
discuss the measurements I did in a bit (some of the findings are a bit
amusing or even hilarious, actually), but the main reasons are:
- The sampling overhead is negligible (compared to transferring the
data, even on localhost), and the behavior is very smooth. So almost
never exceed reading everything, unless sample_frac >= 0.99 and even
there it's a tiny difference. So there's almost no risk, I think.
- For small tables it doesn't really matter. It's going to be fast
anyway, and the difference between reading 10000 or 11000 rows is going
to be just noise. Who cares if this takes 10 or 11 ms ...
- For large tables, we'll never even get to these high sample_frac
values. Imagine a table with 10M rows - the highers stats target we
allow is 10k, and the sample size is 300 * target, so 3M rows. It
doesn't matter if the condition is 0.95 or 0.99, because for this table
we'll never ask for a sample above 30%.
- For the tables in between it might be more relevant, but the simple
truth is that reading the row and sampling it remotely is way cheaper
than the network transfer, even if on localhost. The data suggest that
reading+sampling a row costs ~0.2us at most, but sending it is ~1.5us
(localhost) or ~5.5us (local network).
So I just removed the threshold from the patch, and we'll request
sampling even with sample_frac=100% (if that happens).
sampling test
-------------
I did a simple test to collect some data - create a table, and sample
various fractions in the ways discussed in this patch - either locally
or through a FDW.
This required a bit of care to ensure the sampling happens in the right
place (and e.g. we don't push the random() or tablesample down), which I
did by pointing the FDW table not to a remote table, but to a view with
an optimization fence (OFFSET 0). See the run-local.sh script.
The script populates the table with different numbers of rows, samples
different fractions of it, etc. I also did this from a different
machine, to see what a bit more network latency would do (that's what
run-remote.sh is for).
results (fdw-sampling-test.pdf)
-------------------------------
The attached PDF shows the results - first page is for the foreign table
in the same instance (i.e. localhost, latency ~0.01ms), second page is
for FDW pointing to a machine in the same network (latency ~0.1ms).
Left column is always the table "directly", right column is through the
FDW. On the x-axis is the fraction of the table we sample, y-axis is
duration in milliseconds. "full" means "reading everything" (i.e. what
the FDW does now), the other options should be clear I think.
The first two dataset sizes (10k and 10M rows) are tiny (10M is ~2GB),
and fit into RAM, which is 8GB. The 100M is ~21GB, so much larger.
In the "direct" (non-FDW) sampling, the various sampling methods start
losing to seqscan fairly soon - "random" is consistently slower,
"bernoulli" starts losing at ~30%, "system" as ~80%. This is not very
surprising, particularly for bernoulli/random which actually read all
the rows anyway. But the overhead is pretty limited to ~30% on top of
the seqscan.
But in the "FDW" sampling (right column), it's entirely different story,
and all the methods clearly win over just transferring everything and
only then doing the sampling.
Who cares if the remote sampling means means we have to pay 0.2us
instead of 0.15us (per row), when the transfer costs 1.5us per row?
The 100M case shows an interesting behavior for the "system" method,
which quickly spikes to ~2x of the "full" method when sampling ~20% of
the table, and then gradually improves again.
My explanation is that this is due to "system" making the I/O patter
increasingly more random, because it jumps blocks in a way that makes
readahead impossible. And then as the fraction increases, it becomes
more sequential again.
All the other methods are pretty much equal to just scanning everything
sequentially, and sampling rows one by one.
The "system" method in TABLESAMPLE would probably benefit from explicit
prefetching, I guess. For ANALYZE this probably is not a huge, as we'll
never sample this large fraction for large tables (for 100M rows we peak
at ~3% with target 10000, which is way before the peak). And for smaller
tables we're more likely to hit cache (which is why the smaller data
sets don't have this issue). But for explicit TABLESAMPLE queries that
may not be the case.
Although, ANALYZE uses something like "system" to sample rows too, no?
However, even this is not an issue for the FDW case - in that case it
still clearly wins over the current "local sample" approach, because
transferring the data is so expensive which makes the "peak" into a tiny
hump.
The second page (different machine, local network) tells the same story,
except that the differences are even clearer.
ANALYZE test
------------
So I changed the patch, and did a similar test by running ANALYZE either
on the local or foreign table, using the different sampling methods.
This does not require the hacks to prevent pushdown etc. but it also
means we can't determine sample_frac directly, only through statistics
target (which is capped to 10k).
In this case, "local" means ANALYZE on the local table (which you might
think of as the "baseline"), and "off" means reading all data without
remote sampling.
For the two smaller data sets (10k and 10M rows), the benefits are
pretty obvious. We're very close to the "local" results, because we save
a lot on copying only some of the rows. For 10M we only get to ~30%
before we hit target=10k, which is we don't see it get closer to "off".
But now we get to the *hilarious* thing - if you look at the 10M result,
you may notice that *all* the sampling methods beat ANALYZE on the local
table.
For "system" (which wins from the very beginning) we might make some
argument that the algorithm is simpler than what ANALYZE does, skips
blocks differently, etc. - perhaps ...
But bernoulli/random are pretty much ideal sampling, reading/sampling
all rows. And yet both methods start winning after crossing ~1% on this
tables. In other words, it's about 3x faster to ANALYZE a table through
FDW than directly ;-)
Anyway, those issues have impact on this patch, I think. I believe the
results show what the patch does is reasonable.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
This patch looks good to me. I have two very minor nits: The inflation of the sample size by 10% is arbitrary but it doesn't seem unreasonable or concerning. It just makes me curious if there are any known cases that motivated adding this logic. Secondly, if the earliest non-deprecated version of PostgreSQL supports sampling, then you could optionally remove the logic that tests for that. The affected lines should be unreachable.
James Finnerty <jfinnert@amazon.com> writes:
This patch looks good to me. I have two very minor nits: The inflation
of the sample size by 10% is arbitrary but it doesn't seem unreasonable
or concerning. It just makes me curious if there are any known cases
that motivated adding this logic.
I wondered why, too.
Secondly, if the earliest non-deprecated version of PostgreSQL supports
sampling, then you could optionally remove the logic that tests for
that. The affected lines should be unreachable.
We've tried to keep postgres_fdw compatible with quite ancient remote
servers (I think the manual claims back to 8.3, though it's unlikely
anyone's tested that far back lately). This patch should not move those
goalposts, especially if it's easy not to.
regards, tom lane