<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alexey Goncharuk</title>
    <description>The latest articles on DEV Community by Alexey Goncharuk (@agoncharuk).</description>
    <link>https://dev.to/agoncharuk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F636700%2F3a5ee135-aeb7-499d-bec7-ded6c6ed6dcb.jpeg</url>
      <title>DEV Community: Alexey Goncharuk</title>
      <link>https://dev.to/agoncharuk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agoncharuk"/>
    <language>en</language>
    <item>
      <title>Introduction to the Join Ordering Problem</title>
      <dc:creator>Alexey Goncharuk</dc:creator>
      <pubDate>Sun, 26 Sep 2021 13:49:00 +0000</pubDate>
      <link>https://dev.to/agoncharuk/introduction-to-the-join-ordering-problem-3kdh</link>
      <guid>https://dev.to/agoncharuk/introduction-to-the-join-ordering-problem-3kdh</guid>
      <description>&lt;p&gt;A typical database may execute an SQL query in multiple ways, depending on the selected operators' order and algorithms. One crucial decision is the order in which the optimizer should join relations. The difference between optimal and non-optimal join order might be orders of magnitude. Therefore, the optimizer must choose the proper order of joins to ensure good overall performance. In this blog post, we define the join ordering problem and estimate the complexity of join planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Consider the &lt;a href="https://docs.snowflake.com/en/user-guide/sample-data-tpch.html#database-entities-relationships-and-characteristics"&gt;TPC-H&lt;/a&gt; schema. The &lt;code&gt;customer&lt;/code&gt; may have &lt;code&gt;orders&lt;/code&gt;. Every order may have several positions defined in the &lt;code&gt;lineitem&lt;/code&gt; table. The &lt;code&gt;customer&lt;/code&gt; table has 150,000 records, the &lt;code&gt;orders&lt;/code&gt; table has 1,500,000 records, and the &lt;code&gt;lineitem&lt;/code&gt; table has 6,000,000 records. Intuitively, every customer places approximately ten orders, and every order contains four positions on average.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KUY7Eaos--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnmn7wnbo3lz8i6gmx9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KUY7Eaos--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnmn7wnbo3lz8i6gmx9e.png" alt="db_schema"&gt;&lt;/a&gt;&lt;br&gt;
Suppose that we want to retrieve all &lt;code&gt;lineitem&lt;/code&gt; positions for all orders placed by the given customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT 
  lineitem.*
FROM 
  customer,
  orders,
  lineitem
WHERE
  c_custkey = ? 
  AND c_custkey = o_custkey
  AND o_orderkey = l_orderkey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assume that we have a &lt;a href="https://www.querifylabs.com/blog/what-is-cost-based-optimization"&gt;cost model&lt;/a&gt; where an operator's cost is proportional to the number of processed tuples.&lt;/p&gt;

&lt;p&gt;We consider two different join orders. We can join &lt;code&gt;customer&lt;/code&gt; with &lt;code&gt;orders&lt;/code&gt; and then with &lt;code&gt;lineitem&lt;/code&gt;. This join order is very efficient because most customers are filtered early, and we have a tiny intermediate relation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A-C-B_hp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnwe8emq1cryh3pi2hg0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A-C-B_hp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnwe8emq1cryh3pi2hg0.png" alt="efficient_join"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alternatively, we can join &lt;code&gt;orders&lt;/code&gt; with &lt;code&gt;lineitem&lt;/code&gt; and then with &lt;code&gt;customer&lt;/code&gt;. It produces a large intermediate relation because we map every &lt;code&gt;lineitem&lt;/code&gt; to an &lt;code&gt;order&lt;/code&gt; only to discard most of the produced tuples in the second join.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Is1Fqokj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m6ta73qjqht19ggbotv4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Is1Fqokj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m6ta73qjqht19ggbotv4.png" alt="inefficient_join"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two join orders produce plans with very different costs. The first join strategy is highly superior to the second.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sUECO_qu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snseel7y3d17io4uzdmp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sUECO_qu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snseel7y3d17io4uzdmp.png" alt="join_order_comparison"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Search Space
&lt;/h2&gt;

&lt;p&gt;A perfect optimizer would need to construct all possible equivalent plans for a given query and choose the best plan. Let's now see how many options the optimizer would need to consider.&lt;/p&gt;

&lt;p&gt;We model an n-way join as a sequence of &lt;code&gt;n-1&lt;/code&gt; 2-way joins that form a full binary tree. Leaf nodes are original relations, and internal nodes are join relations. For 3 relations there are 12 valid join orders:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--izWbU4Wd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w0qwbyo6ias84lz49edw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--izWbU4Wd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w0qwbyo6ias84lz49edw.png" alt="all_join_orders"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We count the number of possible join orders for &lt;code&gt;N&lt;/code&gt; relations in two steps. First, we count the number of different orders of leaf nodes. For the first leaf, we choose one of &lt;code&gt;N&lt;/code&gt; relations; for the second leaf, we choose one of remaining &lt;code&gt;N-1&lt;/code&gt; relations, etc. This gives us &lt;code&gt;N!&lt;/code&gt; different orders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UVeArRAA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxagq3utti1l2orcmtrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UVeArRAA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxagq3utti1l2orcmtrj.png" alt="counting_orders"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Second, we need to calculate the number of all possible shapes of a full binary tree with &lt;code&gt;N&lt;/code&gt; leaves, which is the number of ways of associating &lt;code&gt;N-1&lt;/code&gt; applications of a binary operator. This number is known to be equal to &lt;a href="https://en.wikipedia.org/wiki/Catalan_number"&gt;Catalan number&lt;/a&gt; &lt;code&gt;C(N-1)&lt;/code&gt;. Intuitively, for the given fixed order of &lt;code&gt;N&lt;/code&gt; leaf nodes, we need to find the number of ways to set &lt;code&gt;N-1&lt;/code&gt; pairs of open and close parenthesis. E.g., for the four relations &lt;code&gt;[a,b,c,d]&lt;/code&gt;, we have five different parenthesizations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lNUhlEfn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukoyxcx9sbl07l3h16ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lNUhlEfn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukoyxcx9sbl07l3h16ue.png" alt="join_shapes"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Multiplying the two parts, we get the final equation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hCZeNZSC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubl3id5rebyeakrewin8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hCZeNZSC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubl3id5rebyeakrewin8.gif" alt="join_order_formula"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;The number of join orders grows exponentially. For example, for three tables, the number of all possible join plans is &lt;code&gt;12&lt;/code&gt;; for five tables, it is &lt;code&gt;1,680&lt;/code&gt;; for ten tables, it is &lt;code&gt;17,643,225,600&lt;/code&gt;. Practical optimizers use different techniques to ensure a good enough performance of the join enumeration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Et6KgQtD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5o94bqh2ltc6x7303pi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Et6KgQtD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5o94bqh2ltc6x7303pi1.png" alt="number_of_joins"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;First, optimizers might use caching to minimize memory consumption. Two widely used techniques are &lt;a href="https://en.wikipedia.org/wiki/Dynamic_programming"&gt;dynamic programming&lt;/a&gt; and &lt;a href="https://www.querifylabs.com/blog/memoization-in-cost-based-optimizers"&gt;memoization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second, optimizers may use various heuristics to limit the search space instead of doing an exhaustive search. A common heuristic is to prune the join orders that yield cross-products. While good enough in the general case, this heuristic may lead to non-optimal plans, e.g., for some star joins. A more aggressive pruning approach is to enumerate only left- or right-deep trees. This significantly reduces planning complexity but degrades the plan quality even further. Probabilistic algorithms might be used (e.g., &lt;a href="https://en.wikipedia.org/wiki/Genetic_algorithm"&gt;genetic algorithms&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Simulated_annealing"&gt;simulated annealing&lt;/a&gt;), also without any guarantees on the plan optimality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we took a sneak peek at the join ordering problem and got a bird's-eye view of its complexity. In further posts, we will explore the complexity of join order planning for different graph topologies, dive into details of concrete enumeration techniques, and analyze existing and potential strategies of join planning in &lt;a href="https://calcite.apache.org/"&gt;Apache Calcite&lt;/a&gt;. Stay tuned!&lt;/p&gt;

&lt;p&gt;We are always ready to help you with your query optimizer design. &lt;a href="https://www.querifylabs.com/#contact-form"&gt;Just let us know&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>queryoptimizer</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>What is Cost-based Optimization?</title>
      <dc:creator>Alexey Goncharuk</dc:creator>
      <pubDate>Wed, 02 Jun 2021 07:39:46 +0000</pubDate>
      <link>https://dev.to/agoncharuk/what-is-cost-based-optimization-4jc0</link>
      <guid>https://dev.to/agoncharuk/what-is-cost-based-optimization-4jc0</guid>
      <description>&lt;p&gt;In our previous blog posts (&lt;a href="https://www.querifylabs.com/blog/rule-based-query-optimization"&gt;1&lt;/a&gt;, &lt;a href="https://www.querifylabs.com/blog/memoization-in-cost-based-optimizers"&gt;2&lt;/a&gt;), we explored how query optimizers may consider different equivalent plans to find the best execution path. But what does it mean for the plan to be the "best" in the first place? In this blog post, we will discuss what a plan cost is and how it can be used to drive optimizer decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Consider the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM fact 
WHERE event_date BETWEEN ? AND ?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We may do the full table scan and then apply the filter. Alternatively, we may utilize a secondary index on the attribute &lt;code&gt;event_date&lt;/code&gt;, merging the scan and the filter into a single index lookup. This sounds like a good idea because we reduce the amount of data that needs to be processed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--p_TLZV4o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607a85e7fe7bfa2c106c7233_index-is-better.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--p_TLZV4o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607a85e7fe7bfa2c106c7233_index-is-better.png" alt="Example: Index is better"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We may instruct the optimizer to apply such transformations unconditionally, based on the observation that the index lookup is likely to improve the plan's quality. This is an example of &lt;strong&gt;heuristic&lt;/strong&gt; optimization.&lt;/p&gt;

&lt;p&gt;Now consider that our filter has low selectivity. In this case, we may scan the same blocks of data several times, thus increasing the execution time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eCniKYFT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607a85f17332606a7a737099_index-is-worse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eCniKYFT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607a85f17332606a7a737099_index-is-worse.png" alt="Example: Index is worse"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, rules that unanimously produce better plans are rare. A transformation may be useful in one set of circumstances and lead to a worse plan in another. For this reason, heuristic planning cannot guarantee optimality and may produce arbitrarily bad plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;In the previous example, we have two alternative plans, each suitable for a particular setting. Additionally, in some scenarios, the optimization target may change. For example, a plan that gives the smallest latency might not be the best if our goal is to minimize the hardware usage costs in the cloud. So how do we decide which plan is better?&lt;/p&gt;

&lt;p&gt;First of all, we should define the optimization goal, which could be minimal latency, maximal throughput, etc. Then we may associate every plan with a value that describes how "far" the plan is from the ideal target. For example, if the optimization goal is latency, we may assign every plan with an estimated execution time. The closer the plan's cost to zero, the better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ec1RNUOg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607aaba3f9f15f9bdabfdafd_cost-distance.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ec1RNUOg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://uploads-ssl.webflow.com/5fe5c475cb3c75040200bfe6/607aaba3f9f15f9bdabfdafd_cost-distance.png" alt="Example: Cost distance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The underlying hardware and software are often complicated, so we rarely can estimate the optimization target precisely. Instead, we may use a collection of assumptions that approximate the behavior of the actual system. We call it the &lt;strong&gt;cost model&lt;/strong&gt;. The cost model is usually based on parameters of the algorithms used in a plan, such as the estimated amount of consumed CPU and RAM, the amount of network and disk I/O, etc. We also need data statistics: operator cardinalities, filter selectivities, etc. The goal of the model is to consider these characteristics to produce a cost of the plan. For example, we may use coefficients to combine the parameters in different ways depending on the optimization goal.&lt;/p&gt;

&lt;p&gt;The cost of the &lt;code&gt;Filter&lt;/code&gt; might be a function of the input cardinality and predicate complexity. The cost of the &lt;code&gt;NestedLoopJoin&lt;/code&gt; might be proportional to the estimated number of restarts of the inner input. The &lt;code&gt;HashJoin&lt;/code&gt; cost might have a linear dependency on the inputs cardinalities and also model spilling to disk with some pessimistic coefficients if the size of the hash table becomes too large to fit into RAM.&lt;/p&gt;

&lt;p&gt;In practical systems, the cost is usually implemented as a scalar value:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Apache Calcite&lt;/strong&gt;, the cost is modeled as a &lt;a href="https://github.com/apache/calcite/blob/branch-1.24/core/src/main/java/org/apache/calcite/plan/RelOptCostImpl.java#L42"&gt;scalar&lt;/a&gt; representing the number of rows being processed.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Catalyst&lt;/strong&gt;, the Apache Spark optimizer, the cost is a &lt;a href="https://github.com/apache/spark/blob/branch-3.1/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/CostBasedJoinReorder.scala#L384"&gt;vector&lt;/a&gt; of the number of rows and the number of bytes being processed. The vector is converted into a &lt;a href="https://github.com/apache/spark/blob/branch-3.1/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/CostBasedJoinReorder.scala#L372"&gt;scalar&lt;/a&gt; value during comparison.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Presto&lt;/strong&gt;/&lt;strong&gt;Trino&lt;/strong&gt;, the cost is a &lt;a href="https://github.com/trinodb/trino/blob/355/core/trino-main/src/main/java/io/trino/cost/PlanCostEstimate.java#L34-L37"&gt;vector&lt;/a&gt; of estimated CPU, memory, and network usage. The vector is also converted into a &lt;a href="https://github.com/trinodb/trino/blob/355/core/trino-main/src/main/java/io/trino/cost/CostComparator.java#L64-L72"&gt;scalar&lt;/a&gt; value during comparison.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;CockroachDB&lt;/strong&gt;, the cost is an abstract 64-bit floating-point &lt;a href="https://github.com/cockroachdb/cockroach/blob/v20.2.7/pkg/sql/opt/memo/cost.go#L18"&gt;scalar&lt;/a&gt; value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scalar is a common choice for practical systems, but this is not a strict requirement. Any representation could be used, as long as it satisfies the requirements of your system and allows you to decide which plan is better. In &lt;a href="https://en.wikipedia.org/wiki/Multi-objective_optimization"&gt;multi-objective optimization&lt;/a&gt;, costs are often represented as vectors that do not have a strict order in the general case. In parallel query planning, a parallel plan requiring a larger amount of work can provide better latency than a sequential plan that does less work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost-based Optimization
&lt;/h2&gt;

&lt;p&gt;Once we know how to compare the plans, different strategies can be used to search for the best one. A common approach is to enumerate all possible plans for a query and choose a plan with the lowest cost.&lt;/p&gt;

&lt;p&gt;Since the number of possible query plans grows exponentially with the query complexity, &lt;a href="https://en.wikipedia.org/wiki/Dynamic_programming"&gt;dynamic programming&lt;/a&gt; or &lt;a href="https://www.querifylabs.com/blog/memoization-in-cost-based-optimizers"&gt;memoization&lt;/a&gt; could be used to encode alternative plans in a memory-efficient way.&lt;/p&gt;

&lt;p&gt;If the search space is still too large, we may prune the search space. In top-down optimizers, we may use the &lt;a href="https://en.wikipedia.org/wiki/Branch_and_bound"&gt;branch-and-bound&lt;/a&gt; pruning to discard the alternative sub-plans if their costs are greater than the cost of an already known containing plan.&lt;/p&gt;

&lt;p&gt;Heuristic pruning may reduce the search space at the cost of the possibility of missing the optimal plan. Common examples of heuristic pruning are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Probabilistic join order enumeration may reduce the number of alternative plans (e.g., &lt;a href="https://en.wikipedia.org/wiki/Genetic_algorithm"&gt;genetic algorithms&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Simulated_annealing"&gt;simulated annealing&lt;/a&gt;). Postgres uses the &lt;a href="https://www.postgresql.org/docs/13/geqo-pg-intro.html"&gt;genetic query optimizer&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The multi-phase optimizers split the whole optimization problem into smaller stages and search for an optimal plan locally within each step. Apache Flink, Presto/Trino, and CockroachDB all use multi-phase greedy optimization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The cost-based optimization estimates the quality of the plans concerning the optimization target, allowing an optimizer to choose the best execution plan. The cost model depends heavily on metadata maintained by the database, such as estimated cardinalities and selectivities.&lt;/p&gt;

&lt;p&gt;Practical optimizers typically use ordered scalar values as a plan cost. This approach might not be suitable for some complex scenarios, such as the multi-objective query optimization or deciding on the best parallel plan.&lt;/p&gt;

&lt;p&gt;Dynamic programming or memoization is often used in cost-based optimization to encode the search space efficiently. If the search space is too large, various pruning techniques could be used, such as branch-and-bound or heuristic pruning.&lt;/p&gt;

&lt;p&gt;In future blog posts, we will explore some of these concepts in more detail. Stay tuned!&lt;/p&gt;

&lt;p&gt;We are always ready to help you with your query optimizer design. Just &lt;a href="https://www.querifylabs.com/#contact-form"&gt;let us know&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>queryoptimizer</category>
      <category>apachecalcite</category>
      <category>sql</category>
      <category>database</category>
    </item>
  </channel>
</rss>
