Prevent overly large and NaN row estimates in relations
authorDavid Rowley <[email protected]>
Sun, 18 Oct 2020 21:53:52 +0000 (10:53 +1300)
committerDavid Rowley <[email protected]>
Sun, 18 Oct 2020 21:53:52 +0000 (10:53 +1300)
Given a query with enough joins, it was possible that the query planner,
after multiplying the row estimates with the join selectivity that the
estimated number of rows would exceed the limits of the double data type
and become infinite.

To give an indication on how extreme a case is required to hit this, the
particular example case reported required 379 joins to a table without any
statistics, which resulted in the 1.0/DEFAULT_NUM_DISTINCT being used for
the join selectivity.  This eventually caused the row estimates to go
infinite and resulted in an assert failure in initial_cost_mergejoin()
where the infinite row estimated was multiplied by an outerstartsel of 0.0
resulting in NaN.  The failing assert verified that NaN <= Inf, which is
false.

To get around this we use clamp_row_est() to cap row estimates at a
maximum of 1e100.  This value is thought to be low enough that costs
derived from it would remain within the bounds of what the double type can
represent.

Aside from fixing the failing Assert, this also has the added benefit of
making it so add_path() will still receive proper numerical values as
costs which will allow it to make more sane choices when determining the
cheaper path in extreme cases such as the one described above.

Additionally, we also get rid of the isnan() checks in the join costing
functions. The actual case which originally triggered those checks to be
added in the first place never made it to the mailing lists.  It seems
likely that the new code being added to clamp_row_est() will result in
those becoming checks redundant, so just remove them.

The fairly harmless assert failure problem does also exist in the
backbranches, however, a more minimalistic fix will be applied there.

Reported-by: Onder Kalaci
Reviewed-by: Tom Lane
Discussion: https://postgr.es/m/DM6PR21MB1211FF360183BCA901B27F04D80B0@DM6PR21MB1211.namprd21.prod.outlook.com

src/backend/optimizer/path/costsize.c

index cd3716d494f033f2d2d1a753a291fc4606715120..733f7ea54328908f61c4f6b557a8415bed879fe5 100644 (file)
  */
 #define APPEND_CPU_COST_MULTIPLIER 0.5
 
+/*
+ * Maximum value for row estimates.  We cap row estimates to this to help
+ * ensure that costs based on these estimates remain within the range of what
+ * double can represent.  add_path() wouldn't act sanely given infinite or NaN
+ * cost values.
+ */
+#define MAXIMUM_ROWCOUNT 1e100
 
 double     seq_page_cost = DEFAULT_SEQ_PAGE_COST;
 double     random_page_cost = DEFAULT_RANDOM_PAGE_COST;
@@ -189,11 +196,14 @@ double
 clamp_row_est(double nrows)
 {
    /*
-    * Force estimate to be at least one row, to make explain output look
-    * better and to avoid possible divide-by-zero when interpolating costs.
-    * Make it an integer, too.
+    * Avoid infinite and NaN row estimates.  Costs derived from such values
+    * are going to be useless.  Also force the estimate to be at least one
+    * row, to make explain output look better and to avoid possible
+    * divide-by-zero when interpolating costs.  Make it an integer, too.
     */
-   if (nrows <= 1.0)
+   if (nrows > MAXIMUM_ROWCOUNT || isnan(nrows))
+       nrows = MAXIMUM_ROWCOUNT;
+   else if (nrows <= 1.0)
        nrows = 1.0;
    else
        nrows = rint(nrows);
@@ -2737,12 +2747,11 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path,
    QualCost    restrict_qual_cost;
    double      ntuples;
 
-   /* Protect some assumptions below that rowcounts aren't zero or NaN */
-   if (outer_path_rows <= 0 || isnan(outer_path_rows))
+   /* Protect some assumptions below that rowcounts aren't zero */
+   if (outer_path_rows <= 0)
        outer_path_rows = 1;
-   if (inner_path_rows <= 0 || isnan(inner_path_rows))
+   if (inner_path_rows <= 0)
        inner_path_rows = 1;
-
    /* Mark the path with the correct row estimate */
    if (path->path.param_info)
        path->path.rows = path->path.param_info->ppi_rows;
@@ -2952,10 +2961,10 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
                innerendsel;
    Path        sort_path;      /* dummy for result of cost_sort */
 
-   /* Protect some assumptions below that rowcounts aren't zero or NaN */
-   if (outer_path_rows <= 0 || isnan(outer_path_rows))
+   /* Protect some assumptions below that rowcounts aren't zero */
+   if (outer_path_rows <= 0)
        outer_path_rows = 1;
-   if (inner_path_rows <= 0 || isnan(inner_path_rows))
+   if (inner_path_rows <= 0)
        inner_path_rows = 1;
 
    /*
@@ -3185,8 +3194,8 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
                rescannedtuples;
    double      rescanratio;
 
-   /* Protect some assumptions below that rowcounts aren't zero or NaN */
-   if (inner_path_rows <= 0 || isnan(inner_path_rows))
+   /* Protect some assumptions below that rowcounts aren't zero */
+   if (inner_path_rows <= 0)
        inner_path_rows = 1;
 
    /* Mark the path with the correct row estimate */