Coverage Report

Created: 2025-09-27 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/postgres/src/backend/optimizer/geqo/geqo_eval.c
Line
Count
Source
1
/*------------------------------------------------------------------------
2
 *
3
 * geqo_eval.c
4
 *    Routines to evaluate query trees
5
 *
6
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
7
 * Portions Copyright (c) 1994, Regents of the University of California
8
 *
9
 * src/backend/optimizer/geqo/geqo_eval.c
10
 *
11
 *-------------------------------------------------------------------------
12
 */
13
14
/* contributed by:
15
   =*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
16
   *  Martin Utesch        * Institute of Automatic Control    *
17
   =               = University of Mining and Technology =
18
   *  utesch@aut.tu-freiberg.de  * Freiberg, Germany           *
19
   =*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
20
 */
21
22
#include "postgres.h"
23
24
#include <float.h>
25
#include <limits.h>
26
#include <math.h>
27
28
#include "optimizer/geqo.h"
29
#include "optimizer/joininfo.h"
30
#include "optimizer/pathnode.h"
31
#include "optimizer/paths.h"
32
#include "utils/memutils.h"
33
34
35
/* A "clump" of already-joined relations within gimme_tree */
36
typedef struct
37
{
38
  RelOptInfo *joinrel;    /* joinrel for the set of relations */
39
  int     size;     /* number of input relations in clump */
40
} Clump;
41
42
static List *merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump,
43
             int num_gene, bool force);
44
static bool desirable_join(PlannerInfo *root,
45
               RelOptInfo *outer_rel, RelOptInfo *inner_rel);
46
47
48
/*
49
 * geqo_eval
50
 *
51
 * Returns cost of a query tree as an individual of the population.
52
 *
53
 * If no legal join order can be extracted from the proposed tour,
54
 * returns DBL_MAX.
55
 */
56
Cost
57
geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
58
0
{
59
0
  MemoryContext mycontext;
60
0
  MemoryContext oldcxt;
61
0
  RelOptInfo *joinrel;
62
0
  Cost    fitness;
63
0
  int     savelength;
64
0
  struct HTAB *savehash;
65
66
  /*
67
   * Create a private memory context that will hold all temp storage
68
   * allocated inside gimme_tree().
69
   *
70
   * Since geqo_eval() will be called many times, we can't afford to let all
71
   * that memory go unreclaimed until end of statement.  Note we make the
72
   * temp context a child of the planner's normal context, so that it will
73
   * be freed even if we abort via ereport(ERROR).
74
   */
75
0
  mycontext = AllocSetContextCreate(CurrentMemoryContext,
76
0
                    "GEQO",
77
0
                    ALLOCSET_DEFAULT_SIZES);
78
0
  oldcxt = MemoryContextSwitchTo(mycontext);
79
80
  /*
81
   * gimme_tree will add entries to root->join_rel_list, which may or may
82
   * not already contain some entries.  The newly added entries will be
83
   * recycled by the MemoryContextDelete below, so we must ensure that the
84
   * list is restored to its former state before exiting.  We can do this by
85
   * truncating the list to its original length.  NOTE this assumes that any
86
   * added entries are appended at the end!
87
   *
88
   * We also must take care not to mess up the outer join_rel_hash, if there
89
   * is one.  We can do this by just temporarily setting the link to NULL.
90
   * (If we are dealing with enough join rels, which we very likely are, a
91
   * new hash table will get built and used locally.)
92
   *
93
   * join_rel_level[] shouldn't be in use, so just Assert it isn't.
94
   */
95
0
  savelength = list_length(root->join_rel_list);
96
0
  savehash = root->join_rel_hash;
97
0
  Assert(root->join_rel_level == NULL);
98
99
0
  root->join_rel_hash = NULL;
100
101
  /* construct the best path for the given combination of relations */
102
0
  joinrel = gimme_tree(root, tour, num_gene);
103
104
  /*
105
   * compute fitness, if we found a valid join
106
   *
107
   * XXX geqo does not currently support optimization for partial result
108
   * retrieval, nor do we take any cognizance of possible use of
109
   * parameterized paths --- how to fix?
110
   */
111
0
  if (joinrel)
112
0
  {
113
0
    Path     *best_path = joinrel->cheapest_total_path;
114
115
0
    fitness = best_path->total_cost;
116
0
  }
117
0
  else
118
0
    fitness = DBL_MAX;
119
120
  /*
121
   * Restore join_rel_list to its former state, and put back original
122
   * hashtable if any.
123
   */
124
0
  root->join_rel_list = list_truncate(root->join_rel_list,
125
0
                    savelength);
126
0
  root->join_rel_hash = savehash;
127
128
  /* release all the memory acquired within gimme_tree */
129
0
  MemoryContextSwitchTo(oldcxt);
130
0
  MemoryContextDelete(mycontext);
131
132
0
  return fitness;
133
0
}
134
135
/*
136
 * gimme_tree
137
 *    Form planner estimates for a join tree constructed in the specified
138
 *    order.
139
 *
140
 *   'tour' is the proposed join order, of length 'num_gene'
141
 *
142
 * Returns a new join relation whose cheapest path is the best plan for
143
 * this join order.  NB: will return NULL if join order is invalid and
144
 * we can't modify it into a valid order.
145
 *
146
 * The original implementation of this routine always joined in the specified
147
 * order, and so could only build left-sided plans (and right-sided and
148
 * mixtures, as a byproduct of the fact that make_join_rel() is symmetric).
149
 * It could never produce a "bushy" plan.  This had a couple of big problems,
150
 * of which the worst was that there are situations involving join order
151
 * restrictions where the only valid plans are bushy.
152
 *
153
 * The present implementation takes the given tour as a guideline, but
154
 * postpones joins that are illegal or seem unsuitable according to some
155
 * heuristic rules.  This allows correct bushy plans to be generated at need,
156
 * and as a nice side-effect it seems to materially improve the quality of the
157
 * generated plans.  Note however that since it's just a heuristic, it can
158
 * still fail in some cases.  (In particular, we might clump together
159
 * relations that actually mustn't be joined yet due to LATERAL restrictions;
160
 * since there's no provision for un-clumping, this must lead to failure.)
161
 */
162
RelOptInfo *
163
gimme_tree(PlannerInfo *root, Gene *tour, int num_gene)
164
0
{
165
0
  GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private;
166
0
  List     *clumps;
167
0
  int     rel_count;
168
169
  /*
170
   * Sometimes, a relation can't yet be joined to others due to heuristics
171
   * or actual semantic restrictions.  We maintain a list of "clumps" of
172
   * successfully joined relations, with larger clumps at the front. Each
173
   * new relation from the tour is added to the first clump it can be joined
174
   * to; if there is none then it becomes a new clump of its own. When we
175
   * enlarge an existing clump we check to see if it can now be merged with
176
   * any other clumps.  After the tour is all scanned, we forget about the
177
   * heuristics and try to forcibly join any remaining clumps.  If we are
178
   * unable to merge all the clumps into one, fail.
179
   */
180
0
  clumps = NIL;
181
182
0
  for (rel_count = 0; rel_count < num_gene; rel_count++)
183
0
  {
184
0
    int     cur_rel_index;
185
0
    RelOptInfo *cur_rel;
186
0
    Clump    *cur_clump;
187
188
    /* Get the next input relation */
189
0
    cur_rel_index = (int) tour[rel_count];
190
0
    cur_rel = (RelOptInfo *) list_nth(private->initial_rels,
191
0
                      cur_rel_index - 1);
192
193
    /* Make it into a single-rel clump */
194
0
    cur_clump = (Clump *) palloc(sizeof(Clump));
195
0
    cur_clump->joinrel = cur_rel;
196
0
    cur_clump->size = 1;
197
198
    /* Merge it into the clumps list, using only desirable joins */
199
0
    clumps = merge_clump(root, clumps, cur_clump, num_gene, false);
200
0
  }
201
202
0
  if (list_length(clumps) > 1)
203
0
  {
204
    /* Force-join the remaining clumps in some legal order */
205
0
    List     *fclumps;
206
0
    ListCell   *lc;
207
208
0
    fclumps = NIL;
209
0
    foreach(lc, clumps)
210
0
    {
211
0
      Clump    *clump = (Clump *) lfirst(lc);
212
213
0
      fclumps = merge_clump(root, fclumps, clump, num_gene, true);
214
0
    }
215
0
    clumps = fclumps;
216
0
  }
217
218
  /* Did we succeed in forming a single join relation? */
219
0
  if (list_length(clumps) != 1)
220
0
    return NULL;
221
222
0
  return ((Clump *) linitial(clumps))->joinrel;
223
0
}
224
225
/*
226
 * Merge a "clump" into the list of existing clumps for gimme_tree.
227
 *
228
 * We try to merge the clump into some existing clump, and repeat if
229
 * successful.  When no more merging is possible, insert the clump
230
 * into the list, preserving the list ordering rule (namely, that
231
 * clumps of larger size appear earlier).
232
 *
233
 * If force is true, merge anywhere a join is legal, even if it causes
234
 * a cartesian join to be performed.  When force is false, do only
235
 * "desirable" joins.
236
 */
237
static List *
238
merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump, int num_gene,
239
      bool force)
240
0
{
241
0
  ListCell   *lc;
242
0
  int     pos;
243
244
  /* Look for a clump that new_clump can join to */
245
0
  foreach(lc, clumps)
246
0
  {
247
0
    Clump    *old_clump = (Clump *) lfirst(lc);
248
249
0
    if (force ||
250
0
      desirable_join(root, old_clump->joinrel, new_clump->joinrel))
251
0
    {
252
0
      RelOptInfo *joinrel;
253
254
      /*
255
       * Construct a RelOptInfo representing the join of these two input
256
       * relations.  Note that we expect the joinrel not to exist in
257
       * root->join_rel_list yet, and so the paths constructed for it
258
       * will only include the ones we want.
259
       */
260
0
      joinrel = make_join_rel(root,
261
0
                  old_clump->joinrel,
262
0
                  new_clump->joinrel);
263
264
      /* Keep searching if join order is not valid */
265
0
      if (joinrel)
266
0
      {
267
        /* Create paths for partitionwise joins. */
268
0
        generate_partitionwise_join_paths(root, joinrel);
269
270
        /*
271
         * Except for the topmost scan/join rel, consider gathering
272
         * partial paths.  We'll do the same for the topmost scan/join
273
         * rel once we know the final targetlist (see
274
         * grouping_planner).
275
         */
276
0
        if (!bms_equal(joinrel->relids, root->all_query_rels))
277
0
          generate_useful_gather_paths(root, joinrel, false);
278
279
        /* Find and save the cheapest paths for this joinrel */
280
0
        set_cheapest(joinrel);
281
282
        /* Absorb new clump into old */
283
0
        old_clump->joinrel = joinrel;
284
0
        old_clump->size += new_clump->size;
285
0
        pfree(new_clump);
286
287
        /* Remove old_clump from list */
288
0
        clumps = foreach_delete_current(clumps, lc);
289
290
        /*
291
         * Recursively try to merge the enlarged old_clump with
292
         * others.  When no further merge is possible, we'll reinsert
293
         * it into the list.
294
         */
295
0
        return merge_clump(root, clumps, old_clump, num_gene, force);
296
0
      }
297
0
    }
298
0
  }
299
300
  /*
301
   * No merging is possible, so add new_clump as an independent clump, in
302
   * proper order according to size.  We can be fast for the common case
303
   * where it has size 1 --- it should always go at the end.
304
   */
305
0
  if (clumps == NIL || new_clump->size == 1)
306
0
    return lappend(clumps, new_clump);
307
308
  /* Else search for the place to insert it */
309
0
  for (pos = 0; pos < list_length(clumps); pos++)
310
0
  {
311
0
    Clump    *old_clump = (Clump *) list_nth(clumps, pos);
312
313
0
    if (new_clump->size > old_clump->size)
314
0
      break;       /* new_clump belongs before old_clump */
315
0
  }
316
0
  clumps = list_insert_nth(clumps, pos, new_clump);
317
318
0
  return clumps;
319
0
}
320
321
/*
322
 * Heuristics for gimme_tree: do we want to join these two relations?
323
 */
324
static bool
325
desirable_join(PlannerInfo *root,
326
         RelOptInfo *outer_rel, RelOptInfo *inner_rel)
327
0
{
328
  /*
329
   * Join if there is an applicable join clause, or if there is a join order
330
   * restriction forcing these rels to be joined.
331
   */
332
0
  if (have_relevant_joinclause(root, outer_rel, inner_rel) ||
333
0
    have_join_order_restriction(root, outer_rel, inner_rel))
334
0
    return true;
335
336
  /* Otherwise postpone the join till later. */
337
0
  return false;
338
0
}