MySQL 9.5.0
Source Code Documentation
graph_simplification.h
Go to the documentation of this file.
1/* Copyright (c) 2021, 2025, Oracle and/or its affiliates.
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License, version 2.0,
5 as published by the Free Software Foundation.
6
7 This program is designed to work with certain software (including
8 but not limited to OpenSSL) that is licensed under separate terms,
9 as designated in a particular file or component or in included license
10 documentation. The authors of MySQL hereby grant you an additional
11 permission to link the program and your derivative works with the
12 separately licensed software that they have either included with
13 the program or referenced in the documentation.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License, version 2.0, for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
23
24#ifndef SQL_JOIN_OPTIMIZER_GRAPH_SIMPLIFICATION_H_
25#define SQL_JOIN_OPTIMIZER_GRAPH_SIMPLIFICATION_H_
26
27/**
28 @file
29
30 Heuristic simplification of query graphs to make them execute faster,
31 largely a direct implementation of [Neu09] (any references to just
32 “the paper” will generally be to that). This is needed for when
33 query hypergraphs have too many possible (connected) subgraphs to
34 evaluate all of them, and we need to resort to heuristics.
35
36 The algorithm works by evaluating pairs of neighboring joins
37 (largely, those that touch some of the same tables), finding obviously _bad_
38 pairwise orderings and then disallowing them. I.e., if join A must
39 very likely happen before join B (as measured by cost heuristics),
40 we disallow the B-before-A join by extending the hyperedge of
41 B to include A's nodes. This makes the graph more visually complicated
42 (thus making “simplification” a bit of a misnomer), but reduces the search
43 space, so that the query generally is faster to plan.
44
45 Obviously, as the algorithm is greedy, it will sometimes make mistakes
46 and make for a more expensive (or at least higher-cost) query.
47 This isn't necessarily an optimal or even particularly good algorithm;
48 e.g. LinDP++ [Rad19] claims significantly better results, especially
49 on joins that are 40 tables or more. However, using graph simplification
50 allows us to handle large queries reasonably well, while still reusing nearly
51 all of our query planning machinery (i.e., we don't have to implement a
52 separate query planner and cost model for large queries).
53
54 Also note that graph simplification only addresses the problem of subgraph
55 pair explosion. If each subgraph pair generates large amounts of candidate
56 access paths (e.g. through parameterized paths), each subgraph pair will in
57 itself be expensive, and graph simplification does not concern itself with
58 this at all. Thus, to get a complete solution, we must _also_ have heuristic
59 pruning of access paths within a subgraph, which we're currently missing.
60
61
62 [Neu09] Neumann: “Query Simplification: Graceful Degradation for Join-Order
63 Optimization”.
64 [Rad19] Radke and Neumann: “LinDP++: Generalizing Linearized DP to
65 Crossproducts and Non-Inner Joins”.
66 */
67
68#include <assert.h>
69#include <stddef.h>
70
71#include <limits>
72#include <vector>
73
74#include "my_compiler.h"
75#include "priority_queue.h"
79#include "sql/mem_root_array.h"
80#include "sql/sql_array.h"
81
82class THD;
83struct JoinHypergraph;
84
85/**
86 An estimate of the output from table access or join.
87*/
88struct RelationMetrics final {
89 /// The number of outpur rows.
90 double rows;
91 /// The average row size.
92 double row_size;
93};
94
95// Exposed for unit testing.
97 public:
99
100 // Do a single simplification step. The return enum is mostly for unit tests;
101 // general code only needs to care about whether it returned
102 // NO_SIMPLIFICATION_POSSIBLE or not.
104 // No (more) simplifications are possible on this hypergraph.
106
107 // We applied a simplification of the graph (forcing one join ahead of
108 // another).
110
111 // We applied a simplification, but it was one that was forced upon us;
112 // we intended to apply the opposite, but discovered it would leave the
113 // graph
114 // in an impossible state. Thus, the graph has been changed, but the actual
115 // available join orderings are exactly as they were.
117
118 // We applied a step that was earlier undone using UndoSimplificationStep().
119 // (We do not know whether it was originally APPLIED_SIMPLIFICATION or
120 // APPLIED_NOOP.)
122 };
124
125 // Undo the last applied simplification step (by DoSimplificationStep()).
126 // Note that this does not reset the internal state, i.e., it only puts
127 // the graph back into the state before the last DoSimplificationStep()
128 // call. This means that the internal happens-before graph and cardinalities
129 // remain as if the step was still done. This is because if calling
130 // DoSimplificationStep() after an UndoSimplificationStep() call,
131 // no new work is done; the change is simply replayed again, with no
132 // new computation done. We only need to search for more simplifications
133 // once we've replayed all undone steps. This also means that we make
134 // the assumption that nobody else is changing the graph during the
135 // lifetime of GraphSimplifier.
136 //
137 // You can call UndoSimplificationStep() several times, as long as there
138 // is at least one simplification step to undo; undo/redo works essentially
139 // as a stack.
141
142 // How many steps we've (successfully) done and not undone.
143 int num_steps_done() const {
144 assert(m_done_steps.size() < size_t{std::numeric_limits<int>::max()});
145 return static_cast<int>(m_done_steps.size());
146 }
147
148 // How many steps we've undone.
149 int num_steps_undone() const {
150 assert(m_undone_steps.size() < size_t{std::numeric_limits<int>::max()});
151 return static_cast<int>(m_undone_steps.size());
152 }
153
154 private:
155 // Update the given join's cache in the priority queue (or take it in
156 // or out of the queue), presumably after best_step.benefit has changed
157 // for that join.
158 //
159 // After this operation, m_pq should be in a consistent state.
160 void UpdatePQ(size_t edge_idx);
161
162 // Recalculate the benefit of all orderings involving the given edge,
163 // i.e., the advantage of ordering any other neighboring join before
164 // or after it. (These are stored in m_cache; see NeighborCache for
165 // more information on the scheme.) You will typically need to call this
166 // after having modified the given join (hyperedge endpoint). Note that
167 // if a given ordering has become less advantageous, this may entail
168 // recalculating other nodes recursively as well, but this should be rare
169 // (again, see the comments on NeighborCache).
170 //
171 // “begin” and “end” are the range of other joins to compare against
172 // (edge1_idx itself is always excluded). It should normally be set to
173 // 0 and N (the number of edges) to compare against all, but during the
174 // initial population in the constructor, where every pair is considered,
175 // it is be used to avoid redundant computation.
176 //
177 // It would have been nice to somehow be able to use neighbor-of-neighbor
178 // information to avoid rescanning all candidates for neighbors
179 // (and the paper mentions “materializing all neighbors of a join”),
180 // but given how hyperedges work, there doesn't seem to be a trivial way
181 // of doing that (after A has absorbed B's into one of its hyperedges,
182 // it seems it could gain new neighbors that were neither neighbors of
183 // A nor B).
184 void RecalculateNeighbors(size_t edge1_idx, size_t begin, size_t end);
185
187 double benefit;
190 };
191
192 // Returns whether two joins are neighboring (share edges),
193 // and if so, estimates the benefit of joining one before the other
194 // (including which one should be first) and writes into “step”.
195 ALWAYS_INLINE bool EdgesAreNeighboring(size_t edge1_idx, size_t edge2_idx,
197
201
202 // Old and new versions of after_edge_idx.
205 };
206
207 // Convert a simplification step (join A before join B) to an actual
208 // idea of how to modify the given edge (new values for join B's
209 // hyperedge endpoints).
212
214 // Steps that we have applied so far, in chronological order.
215 // Used so that we can undo them easily on UndoSimplificationStep().
217
218 // Steps that we used to have applied, but have undone, in chronological
219 // order of the undo (ie., latest undone step last).
220 // DoSimplificationStep() will use these to quickly reapply an undone
221 // step if needed (and then move it to the end of done_steps again).
223
224 // Cache the metrics of (a join of) the nodes on each side of each
225 // hyperedge, corresponding 1:1 index-wise to m_graph->edges. So if
226 // e.g. m_graph->graph.edges[0].left contains {t1,t2,t4}, then
227 // m_edge_metrics[0].left will contain the metrics of joining
228 // t1, t2 and t4 together.
229 //
230 // This cache is so that we don't need to make repeated calls to
231 // GetMetrics(), which is fairly expensive. It is updated when we
232 // apply simplification steps (which change the hyperedges).
233 struct EdgeMetrics {
236 };
238
239 // The graph we are simplifying.
241
242 // Stores must-happen-before relationships between the joins (edges),
243 // so that we don't end up with impossibilities. See OnlineCycleFinder
244 // for more information.
246
247 // Used for storing which neighbors are possible to simplify,
248 // and how attractive they are. This speeds up repeated application of
249 // DoSimplificationStep() significantly, as we don't have to recompute
250 // the same information over and over again. This is keyed on the numerically
251 // lowest join of the join pair, i.e., information about the benefit of
252 // ordering join A before or after join B is stored on m_cache[min(A,B)].
253 // These take part in a priority queue (see m_pq below), so that we always
254 // know cheaply which one is the most attractive.
255 //
256 // There is a maybe surprising twist here; for any given cache node (join),
257 // we only store the most beneficial ordering, and throw away all others.
258 // This is because our benefit values keep changing all the time; once we've
259 // chosen to put A before B, it means we've changed B, and that means every
260 // single join pair involving B now needs to be recalculated anyway
261 // (the costs, and thus ordering benefits, are highly dependent on the
262 // hyperedge of B). Thus, storing only the best one (and by extension,
263 // not having information about the other ones in the priority queue)
264 // allows us to very quickly and easily throw away half of the invalidated
265 // ones. We still need to check the other half (the ones that may be the best
266 // for other nodes) to see if we need to invalidate them, but actual
267 // invalidation is rare, as it only happens for the best simplification
268 // involving that node (i.e., 1/N).
269 //
270 // It's unclear if this is the same scheme that the paper alludes to;
271 // it mentions a priority queue and ordering by neighbor-involving joins,
272 // but very little detail.
274 // The best simplification involving this join and a higher-indexed join,
275 // and the index of that other node. (best_neighbor could be inferred
276 // from the indexes in best_step and this index, but we keep it around
277 // for simplicity.) best_neighbor == -1 indicates that there are no
278 // possible reorderings involving this join and a higher-indexed one
279 // (so it should not take part in the priority queue).
282
283 // Where we are in the priority queue (heap index);
284 // Priority_queue will update this for us (through MarkNeighborCache)
285 // whenever we are insert into or moved around in the queue.
286 // This is so that we can easily tell the PQ to recalculate our position
287 // whenever best_step.benefit changes. -1 means that we are
288 // currently not in the priority queue.
289 int index_in_pq = -1;
290 };
292
293 // A priority queue of which simplifications are the most attractive,
294 // containing pointers into m_cache. See the documentation on NeighborCache
295 // for more information.
297 bool operator()(const NeighborCache *a, const NeighborCache *b) const {
298 return a->best_step.benefit < b->best_step.benefit;
299 }
300 };
302 void operator()(size_t index, NeighborCache **cache) {
303 (*cache)->index_in_pq = index;
304 }
305 };
307 NeighborCache *,
308 std::vector<NeighborCache *, Mem_root_allocator<NeighborCache *>>,
309 CompareByBenefit, MarkNeighborCache>
311};
312
313void SetNumberOfSimplifications(int num_simplifications,
314 GraphSimplifier *simplifier);
315
316// See comment in .cc file.
317void SimplifyQueryGraph(THD *thd, int subgraph_pair_limit,
318 JoinHypergraph *graph, GraphSimplifier *simplifier);
319
320#endif // SQL_JOIN_OPTIMIZER_GRAPH_SIMPLIFICATION_H_
A wrapper class which provides array bounds checking.
Definition: sql_array.h:48
Definition: graph_simplification.h:96
ALWAYS_INLINE bool EdgesAreNeighboring(size_t edge1_idx, size_t edge2_idx, ProposedSimplificationStep *step)
Definition: graph_simplification.cc:687
Bounds_checked_array< NeighborCache > m_cache
Definition: graph_simplification.h:291
THD * m_thd
Definition: graph_simplification.h:213
void RecalculateNeighbors(size_t edge1_idx, size_t begin, size_t end)
Definition: graph_simplification.cc:636
GraphSimplifier(THD *thd, JoinHypergraph *graph)
Definition: graph_simplification.cc:591
SimplificationStep ConcretizeSimplificationStep(GraphSimplifier::ProposedSimplificationStep step)
Definition: graph_simplification.cc:819
JoinHypergraph * m_graph
Definition: graph_simplification.h:240
SimplificationResult
Definition: graph_simplification.h:103
@ APPLIED_REDO_STEP
Definition: graph_simplification.h:121
@ NO_SIMPLIFICATION_POSSIBLE
Definition: graph_simplification.h:105
@ APPLIED_SIMPLIFICATION
Definition: graph_simplification.h:109
@ APPLIED_NOOP
Definition: graph_simplification.h:116
Bounds_checked_array< EdgeMetrics > m_edge_metrics
Definition: graph_simplification.h:237
void UndoSimplificationStep()
Definition: graph_simplification.cc:947
SimplificationResult DoSimplificationStep()
Definition: graph_simplification.cc:874
Mem_root_array< SimplificationStep > m_undone_steps
Definition: graph_simplification.h:222
void UpdatePQ(size_t edge_idx)
Definition: graph_simplification.cc:616
int num_steps_done() const
Definition: graph_simplification.h:143
int num_steps_undone() const
Definition: graph_simplification.h:149
Priority_queue< NeighborCache *, std::vector< NeighborCache *, Mem_root_allocator< NeighborCache * > >, CompareByBenefit, MarkNeighborCache > m_pq
Definition: graph_simplification.h:310
OnlineCycleFinder m_cycles
Definition: graph_simplification.h:245
Mem_root_array< SimplificationStep > m_done_steps
Definition: graph_simplification.h:216
A typesafe replacement for DYNAMIC_ARRAY.
Definition: mem_root_array.h:432
A fast online cycle finder, based on [Pea03].
Definition: online_cycle_finder.h:54
Implements a priority queue using a vector-based max-heap.
Definition: priority_queue.h:104
For each client connection we create a separate thread with THD serving as a thread/connection descri...
Definition: sql_lexer_thd.h:36
void SimplifyQueryGraph(THD *thd, int subgraph_pair_limit, JoinHypergraph *graph, GraphSimplifier *simplifier)
Repeatedly apply simplifications (in the order of most to least safe) to the given hypergraph,...
Definition: graph_simplification.cc:991
void SetNumberOfSimplifications(int num_simplifications, GraphSimplifier *simplifier)
Definition: graph_simplification.cc:960
Definition of an undirected (join) hypergraph.
Header for compiler-dependent features.
#define ALWAYS_INLINE
Definition: my_compiler.h:99
bool index(const std::string &value, const String &search_for, uint32_t *idx)
Definition: contains.h:76
const char * begin(const char *const c)
Definition: base64.h:44
Cursor end()
A past-the-end Cursor.
Definition: rules_table_service.cc:192
Definition: graph_simplification.h:296
bool operator()(const NeighborCache *a, const NeighborCache *b) const
Definition: graph_simplification.h:297
Definition: graph_simplification.h:233
RelationMetrics right
Definition: graph_simplification.h:235
RelationMetrics left
Definition: graph_simplification.h:234
Definition: graph_simplification.h:301
void operator()(size_t index, NeighborCache **cache)
Definition: graph_simplification.h:302
Definition: graph_simplification.h:273
int best_neighbor
Definition: graph_simplification.h:280
ProposedSimplificationStep best_step
Definition: graph_simplification.h:281
int index_in_pq
Definition: graph_simplification.h:289
Definition: graph_simplification.h:186
int before_edge_idx
Definition: graph_simplification.h:188
int after_edge_idx
Definition: graph_simplification.h:189
double benefit
Definition: graph_simplification.h:187
Definition: graph_simplification.h:198
int after_edge_idx
Definition: graph_simplification.h:200
int before_edge_idx
Definition: graph_simplification.h:199
hypergraph::Hyperedge new_edge
Definition: graph_simplification.h:204
hypergraph::Hyperedge old_edge
Definition: graph_simplification.h:203
A struct containing a join hypergraph of a single query block, encapsulating the constraints given by...
Definition: make_join_hypergraph.h:98
An estimate of the output from table access or join.
Definition: graph_simplification.h:88
double rows
The number of outpur rows.
Definition: graph_simplification.h:90
double row_size
The average row size.
Definition: graph_simplification.h:92
Definition: hypergraph.h:81