From: Tomas Vondra <tomas.vondra@2ndquadrant.com>
To: Jeff Janes <jeff.janes@gmail.com>
Cc: Robert Haas <robertmhaas@gmail.com>
Cc: pgsql-hackers@postgresql.org <pgsql-hackers@postgresql.org>
Subject: Re: PATCH: adaptive ndistinct estimator v4
Date: Wed, 17 Jun 2015 16:47:28 +0200
Message-ID: <55818880.10300@2ndquadrant.com> (raw)
In-Reply-To: <CAMkU=1yR=i5F1RnmJsB9C-iEbTE3J1sR+4mRbcPy6mwCotPxfQ@mail.gmail.com>
References: <541A2335.3060100@agliodbs.com>
<WM!ce2c941557a0e9090a3cb66d0469128c782bb4d7b1f60d15a5c91279578d4c6fd237efa5b4660e94a0056752c695113e!@asav-2.01.com>
<542C4E60.3030505@agliodbs.com>
<CA+U5nMKqqPNpSr2CQ2V0=9bjWVv-OA28o3BoxYP=7YVfyswdzw@mail.gmail.com>
<CAEYLb_UTz=dtxYOCYF_D1s7wQJfoZUrey6Fjxq08VFueXWoEKQ@mail.gmail.com>
<WM!341e0135c2f371f48830484cdb3da37506d776453b10db7a9af11d648214c4b437402f04e7ccb3d30918cbc64dfee95d!@asav-1.01.com>
<542DADEB.2080103@agliodbs.com>
<CAM-w4HNfZymQmTu3+TxQQD-e6_410-sDnZBaW8neurmTFh4GbA@mail.gmail.com>
<f4c06746b31e468f9550fb9924798da3.squirrel@2.emaily.eu>
<54381D20.9010501@fuzzy.cz>
<CAMkU=1yoiQt6+_XMACL=PTv-aHwc1E0BgiHGhKW3HL+ByN6zpw@mail.gmail.com>
<5472416C.3080506@fuzzy.cz>
<5483B346.6000203@fuzzy.cz>
<551AEF45.7010700@2ndquadrant.com>
<CA+TgmoaCsdM5WSpaVNmbWBN8+wuOGfYt1reZttGmDG1CnJvtLQ@mail.gmail.com>
<5542D4CF.5060700@2ndquadrant.com>
<CAMkU=1yR=i5F1RnmJsB9C-iEbTE3J1sR+4mRbcPy6mwCotPxfQ@mail.gmail.com>
List-Unsubscribe: <mailto:majordomo@postgresql.org?body=unsub%20pgsql-hackers>
Hi,
On 05/13/15 23:07, Jeff Janes wrote:
> With the warning it is very hard to correlate the discrepancy you do
> see with which column is causing it, as the warnings don't include
> table or column names (Assuming of course that you run it on a
> substantial database--if you just run it on a few toy cases then the
> warning works well).
That's true. I've added attnum/attname to the warning in the attached
version of the patch.
> If we want to have an explicitly experimental patch which we want
> people with interesting real-world databases to report back on, what
> kind of patch would it have to be to encourage that to happen? Or are
> we never going to get such feedback no matter how friendly we make
> it? Another problem is that you really need to have the gold standard
> to compare them to, and getting that is expensive (which is why we
> resort to sampling in the first place). I don't think there is much
> to be done on that front other than bite the bullet and just do
> it--perhaps only for the tables which have discrepancies.
Not sure. The "experimental" part of the patch was not really aimed at
the users outside the development community - it was meant to be used by
members of the community, possibly testing it on customer databases I
don't think adding the GUC into the final release is a good idea, it's
just a noise in the config no-one would actually use.
> Some of the regressions I've seen are at least partly a bug:
>
> + /* find the 'm' value minimizing the difference */
> + for (m = 1; m <= total_rows; m += step)
> + {
> + double q = k / (sample_rows * m);
>
> sample_rows and m are both integers, and their product overflows
> vigorously. A simple cast to double before the multiplication fixes
> the first example I produced. The estimate goes from 137,177 to
> 1,108,076. The reality is 1,062,223.
>
> Perhaps m should be just be declared a double, as it is frequently
> used in double arithmetic.
Yeah, I just discovered this bug independently. There's another bug that
the adaptive_estimator takes total_rows as integer, so it breaks for
tables with more than INT_MAX rows. Both are fixed in the v5.
>
> Therefore, I think that:
>
> 1. This should be committed near the beginning of a release cycle,
> not near the end. That way, if there are problem cases, we'll have a
> year or so of developer test to shake them out.
>
>
> It can't hurt, but how effective will it be? Will developers know or
> care whether ndistinct happened to get better or worse while they
> are working on other things? I would think that problems will be
> found by focused testing, or during beta, and probably not by
> accidental discovery during the development cycle. It can't hurt, but
> I don't know how much it will help.
I agree with that - it's unlikely the regressions will get discovered
randomly. OTOH I'd expect non-trivial number of people on this list to
have a few examples of ndistinct failures, and testing those would be
more useful I guess. But that's unlikely to find the cases that worked
OK before and got broken by the new estimator :-(
> I agree with the "experimental GUC". That way if hackers do happen to
> see something suspicious, they can just turn it off and see what
> difference it makes. If they have to reverse out a patch from 6 months
> ago in an area of the code they aren't particularly interested in and
> then recompile their code and then juggle two different sets of
> binaries, they will likely just shrug it off without investigation.
+1
--
Tomas Vondra http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackersAttachments:ndistinct-estimator-v5.patch (text/x-diff, 13.2K) downloaddiff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.cindex 861048f..f030702 100644--- a/src/backend/commands/analyze.c+++ b/src/backend/commands/analyze.c@@ -16,6 +16,7 @@
#include <math.h>
+#include "access/hash.h"
#include "access/multixact.h"
#include "access/transam.h"
#include "access/tupconvert.h"
@@ -97,6 +98,9 @@ static void update_attstats(Oid relid, bool inh,
static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
+static double adaptive_estimator(double total_rows, int sample_rows,+ int *f, int f_max);+static int hash_comparator(const void *a, const void *b);
/*
* analyze_rel() -- analyze one relation
@@ -1698,6 +1702,7 @@ static void compute_scalar_stats(VacAttrStatsP stats,
int samplerows,
double totalrows);
static int compare_scalars(const void *a, const void *b, void *arg);
+static int compare_scalars_simple(const void *a, const void *b, void *arg);
static int compare_mcvs(const void *a, const void *b);
@@ -1816,6 +1821,23 @@ compute_minimal_stats(VacAttrStatsP stats,
StdAnalyzeData *mystats = (StdAnalyzeData *) stats->extra_data;
/*
+ * The adaptive ndistinct estimator requires counts for all the+ * repetition counts - we can't do the sort-based count directly+ * (because this handles data types with just = operator), and the+ * MCV-based counting seems insufficient. We'll instead compute+ * hash values, and sort those. We're using just 32-bit hashes,+ * which may result in a few collisions - for 30k rows (sampled+ * rows for default_statistics_target=100) there's 1:10 chance of+ * a hash collision (assuming all values are distinct). But this+ * seems like a small error compared to the other factors involved+ * (sampling, ...) or compared to the MCV-based counting.+ */+ uint32 *hashes = (uint32*)palloc0(samplerows * sizeof(uint32));++ /* number of computed hashes (technically equal to nonnull_cnt) */+ int nhashes = 0;++ /*
* We track up to 2*n values for an n-element MCV list; but at least 10
*/
track_max = 2 * num_mcv;
@@ -1876,6 +1898,36 @@ compute_minimal_stats(VacAttrStatsP stats,
total_width += strlen(DatumGetCString(value)) + 1;
}
+ /* compute the hash value, depending on the data type kind */+ if (stats->attrtype->typbyval)+ {+ /* simple pass-by-value data type, with 'typlen' bytes */+ hashes[nhashes++]+ = DatumGetUInt32(hash_any((unsigned char *) &value,+ stats->attrtype->typlen));+ }+ else if (is_varlena)+ {+ /* regular varlena data type */+ hashes[nhashes++]+ = DatumGetUInt32(hash_any((unsigned char *) VARDATA_ANY(value),+ VARSIZE_ANY_EXHDR(DatumGetPointer(value))));+ }+ else if (is_varwidth)+ {+ /* pass-by-reference with a variable length (e.g. cstring) */+ hashes[nhashes++]+ = DatumGetUInt32(hash_any((unsigned char *) DatumGetCString(value),+ strlen(DatumGetCString(value))));+ }+ else+ {+ /* pass-by-reference with fixed length (e.g. name) */+ hashes[nhashes++]+ = DatumGetUInt32(hash_any((unsigned char *) DatumGetCString(value),+ stats->attrtype->typlen));+ }+
/*
* See if the value matches anything we're already tracking.
*/
@@ -1931,6 +1983,43 @@ compute_minimal_stats(VacAttrStatsP stats,
int nmultiple,
summultiple;
+ /* values needed by the adaptive ndistinct estimator */+ int f_max = 0;+ int *f_count = (int*)palloc0(sizeof(int) * (nhashes + 1));+ int prev_index;++ /* sort the hashes and then count the repetitions */+ qsort(hashes, nhashes, sizeof(uint32), hash_comparator);++ /*+ * Counting repetitions - walk through the sorted array, compare+ * the value to the previous one, and whenever it changes the+ * we can compute the repetitions using the array indexes.+ */+ prev_index = 0;+ for (i = 1; i < nhashes; i++)+ {+ /* the hashes are different - store the repetition count */+ if (hashes[i] != hashes[i-1])+ {+ f_count[i - prev_index] += 1;++ if (f_max < (i - prev_index))+ f_max = (i - prev_index);++ prev_index = i;+ }+ }++ /* the last element is not updated in the loop */+ f_count[nhashes - prev_index] += 1;++ if (f_max < (nhashes - prev_index))+ f_max = (nhashes - prev_index);++ /* wide values are assumed to be distinct */+ f_count[1] += toowide_cnt;+
stats->stats_valid = true;
/* Do the simple null-frac and width stats */
stats->stanullfrac = (double) null_cnt / (double) samplerows;
@@ -1988,6 +2077,7 @@ compute_minimal_stats(VacAttrStatsP stats,
double numer,
denom,
stadistinct;
+ double adaptdistinct;
numer = (double) samplerows *(double) d;
@@ -2001,6 +2091,31 @@ compute_minimal_stats(VacAttrStatsP stats,
if (stadistinct > totalrows)
stadistinct = totalrows;
stats->stadistinct = floor(stadistinct + 0.5);
++ /*+ * When computing the adaptive estimate, we're only considering+ * non-null values, so we need to perform correction of the+ * total rows / sample rows to reflect this. Otherwise the+ * coefficients (f_count / f_max) are out of sync. We could+ * probably do the inverse thing (including NULL values into+ * f_count) with the same effect.+ */+ adaptdistinct+ = adaptive_estimator(totalrows * (nonnull_cnt / (double)samplerows),+ nonnull_cnt, f_count, f_max);++ elog(WARNING, "ndistinct estimate attnum=%d attname=%s current=%.2f adaptive=%.2f",+ stats->attr->attnum, NameStr(stats->attr->attname),+ stadistinct, adaptdistinct);++ /* if we've seen 'almost' all rows, use the estimate instead */+ if (samplerows >= 0.95 * totalrows)+ {+ adaptdistinct = (d + d/0.95)/2;+ elog(WARNING, " corrected ndistinct estimate current=%.2f adaptive=%.2f",+ stadistinct, adaptdistinct);+ }+
}
/*
@@ -2225,6 +2340,12 @@ compute_scalar_stats(VacAttrStatsP stats,
int slot_idx = 0;
CompareScalarsContext cxt;
+ /* f values for the estimator - messy and we likely need much+ * less memory, but who cares */+ int f_max = 0; /* max number of duplicates */+ int *f_count = (int*)palloc0(sizeof(int)*(values_cnt+1));+ int first_index = 0; /* first index of a group */+
/* Sort the collected values */
cxt.ssup = &ssup;
cxt.tupnoLink = tupnoLink;
@@ -2295,6 +2416,35 @@ compute_scalar_stats(VacAttrStatsP stats,
}
}
+ /*+ * Counting repetitions - walk through the sorted array, compare+ * the value to the previous one, and whenever it changes the+ * we can compute the repetitions using the array indexes.+ */+ for (i = 1; i < values_cnt; i++)+ {+ /* the hashes are different - store the repetition count */+ if (compare_scalars_simple(&values[i], &values[first_index], &cxt) != 0)+ {+ /* found first element of the following group, so (i-first) is the count */+ f_count[i - first_index] += 1;++ if (f_max < (i - first_index))+ f_max = (i - first_index);++ first_index = i;+ }+ }++ /* the last element is not updated in the loop */+ f_count[values_cnt - first_index] += 1;++ if (f_max < (values_cnt - first_index))+ f_max = (values_cnt - first_index);++ /* compensate for wide values (assumed to be distinct) */+ f_count[1] += toowide_cnt;+
stats->stats_valid = true;
/* Do the simple null-frac and width stats */
stats->stanullfrac = (double) null_cnt / (double) samplerows;
@@ -2337,6 +2487,7 @@ compute_scalar_stats(VacAttrStatsP stats,
double numer,
denom,
stadistinct;
+ double adaptdistinct; /* adaptive estimate */
numer = (double) samplerows *(double) d;
@@ -2350,6 +2501,30 @@ compute_scalar_stats(VacAttrStatsP stats,
if (stadistinct > totalrows)
stadistinct = totalrows;
stats->stadistinct = floor(stadistinct + 0.5);
++ /*+ * When computing the adaptive estimate, we're only considering+ * non-null values, so we need to perform correction of the+ * total rows / sample rows to reflect this. Otherwise the+ * coefficients (f_count / f_max) are out of sync. We could+ * probably do the inverse thing (including NULL values into+ * f_count) with the same effect.+ */+ adaptdistinct+ = adaptive_estimator(totalrows * (nonnull_cnt / (double)samplerows),+ nonnull_cnt, f_count, f_max);++ elog(WARNING, "ndistinct estimate attnum=%d attname=%s current=%.2f adaptive=%.2f",+ stats->attr->attnum, NameStr(stats->attr->attname),+ stadistinct, adaptdistinct);++ /* if we've seen 'almost' all rows, use the estimate instead */+ if (samplerows >= 0.95 * totalrows)+ {+ adaptdistinct = (d + d/0.95)/2;+ elog(WARNING, " corrected ndistinct estimate current=%.2f adaptive=%.2f",+ stadistinct, adaptdistinct);+ }
}
/*
@@ -2665,6 +2840,21 @@ compare_scalars(const void *a, const void *b, void *arg)
}
/*
+ * qsort_arg comparator for sorting ScalarItems+ *+ */+static int+compare_scalars_simple(const void *a, const void *b, void *arg)+{+ Datum da = ((const ScalarItem *) a)->value;+ Datum db = ((const ScalarItem *) b)->value;++ CompareScalarsContext *cxt = (CompareScalarsContext *) arg;++ return ApplySortComparator(da, false, db, false, cxt->ssup);+}++/*
* qsort comparator for sorting ScalarMCVItems by position
*/
static int
@@ -2675,3 +2865,131 @@ compare_mcvs(const void *a, const void *b)
return da - db;
}
++/*+ * Implements AEE ndistinct estimator, desctibed in the paper "Towards+ * Estimation Error Guarantees for Distinct Values, ACM 2000" paper, with+ * a minor tweak for highly skewed distributions, where the AEE is quite+ * unstable. In those cases (when either f1 or f2 are 0) we simply use+ * the GEE estimator, which seems to work better in those cases.+ *+ * The AEE estimator is based on solving this equality (for "m")+ *+ * m - f1 - f2 = f1 * (A + A(m)) / (B + B(m))+ *+ * where A, B are effectively constants (not depending on m), and A(m)+ * and B(m) are functions. This is equal to solving+ *+ * 0 = f1 * (A + A(m)) / (B + B(m)) - (m - f1 - f2)+ *+ * Instead of looking for the exact solution to this equation (which+ * might be fractional), we'll look for a natural number minimizing+ * the absolute difference. Number of (distinct) elements is a natural+ * number, and we don't mind if the number is slightly wrong. It's+ * just an estimate, after all. The error from sampling will be much+ * worse in most cases.+ *+ * We know the acceptable values of 'm' are [d,N] where 'd' is the number+ * of distinct elements in the sample, and N is the number of rows in+ * the table (not just the sample). For large tables (billions of rows)+ * that'd be quite time-consuming to compute, so we'll approximate the+ * solution by gradually increasing the step to ~1% of the current value+ * of 'm'. This will make it much faster and yet very accurate.+ *+ * All this of course assumes the function behaves reasonably (not+ * oscillating etc.), but that's a safe assumption as the estimator+ * would perform terribly otherwise (and we're falling back to GEE for+ * cases where this behavior could happen).+ */+static double+adaptive_estimator(double total_rows, int sample_rows, int *f, int f_max)+{+ int i;+ double m;+ double A = 0.0, B = 0.0;+ double opt_m = 0;+ double opt_diff = total_rows;+ double step = 1.0;+ double ndistinct;+ int d = f[1] + f[2];++ double k = (f[1] + 2*f[2]);++ /* for highly-skewed distributions, the GEE works better */+ if (f[1] == 0 || f[2] == 0)+ {+ d = f[1];+ ndistinct = sqrt(total_rows / (double)sample_rows) * f[1];++ for (i = 2; i <= f_max; i++)+ {+ ndistinct += f[i];+ d += f[i];+ }++ /* sanity checks that the estimate is within [d,total_rows] */+ if (ndistinct < d)+ ndistinct = d;+ else if (ndistinct > total_rows / sample_rows * d)+ ndistinct = total_rows / sample_rows * d;++ return ndistinct;+ }++ /* compute the 'constant' parts of the equality (A, B) */+ for (i = 3; i <= f_max; i++)+ {+ double p = i / (double)sample_rows;+ A += powl((1.0 - p), sample_rows ) * f[i];+ B += i * powl((1.0 - p), sample_rows-1) * f[i];+ d += f[i];+ }++ /* find the 'm' value minimizing the difference */+ for (m = 1; m <= total_rows; m += step)+ {+ double q = k / (sample_rows * m);++ double A_m = A + m * powl((1 - q), sample_rows );+ double B_m = B + k * powl((1 - q), sample_rows-1);++ double diff = fabsl(f[1] * (A_m / B_m) - (m - f[1] - f[2]));++ /*+ * The function should have a single minimum - stop if we've passed+ * it, but not on the first element.+ */+ if ((m > 1) && (opt_diff < diff))+ break;++ opt_diff = diff;+ opt_m = m;++ /* tweak the step to 1% to make it faster */+ step = ((int)(0.01 * m) > step) ? (int)(0.01 * m) : step;+ }++ /* compute the final estimate */+ ndistinct = d + opt_m - f[1] - f[2];++ /* sanity checks that the estimate is within [d,total_rows] */+ if (ndistinct < d)+ ndistinct = d;+ else if (ndistinct > total_rows / sample_rows * d)+ ndistinct = total_rows / sample_rows * d;++ return ndistinct;+}++static int+hash_comparator(const void *a, const void *b)+{+ uint32 ai = *(uint32*)a;+ uint32 bi = *(uint32*)b;+ if (ai < bi)+ return -1;+ else if (ai > bi)+ return 1;+ else+ return 0;+}
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-performance@postgresql.org
Cc: tomas.vondra@2ndquadrant.com, jeff.janes@gmail.com, robertmhaas@gmail.com, pgsql-hackers@postgresql.org
Subject: Re: PATCH: adaptive ndistinct estimator v4
In-Reply-To: <55818880.10300@2ndquadrant.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox