Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-41159

ORDER BY join-order costing overcosts sort-before-join plan, causing ~8x slowdown

    XMLWordPrintable

Details

    • Bug
    • Status: Confirmed (View Workflow)
    • Major
    • Resolution: Unresolved
    • 10.11, 11.4, 11.8, 12.3, 13.1, 12.3.2
    • 10.11, 11.4, 11.8, 12.3, 13.1
    • Optimizer
    • Related to performance
    • The optimizer may overestimate the sorting cost of an ORDER BY plan that sorts only a small outer table before joining, causing a substantially slower join order to be selected.

    Description

      Summary

      I found a reproducible optimizer performance issue on MariaDB 12.3.2.

      For a three-table INNER JOIN with ORDER BY, the optimizer chooses:

      t3 -> t1 -> t0
        -> filesort
      

      The selected plan materializes the joined result and sorts about 6.3 million rows.

      Its estimated cost is:

      2165.444633
      

      Three ANALYZE FORMAT=JSON executions take:

      19477.663 ms
      18778.653 ms
      19432.592 ms
      

      Median:

      19432.592 ms
      

      Forcing the alternative join order:

      t1 -> t3 -> t0
      

      produces a very different ORDER BY strategy: MariaDB sorts only the 502-row t1 input first, then performs the nested-loop joins.

      This alternative has a higher estimated cost:

      2249.918896
      

      but its three runtimes are:

      2234.385 ms
      2565.211 ms
      2389.387 ms
      

      Median:

      2389.387 ms
      

      Thus, the plan estimated as about 3.9% more expensive is actually about 8.13x faster.

      Reproducer

      Run the attached setup file first:

      mariadb < 01_setup.sql
      

      Then run the query test:

      mariadb < 02_query_test_with_trace.sql > query_test_with_trace.out 2>&1
      

      The main query is:

      SELECT t1.c1 AS ref0
      FROM t3
      JOIN t0 ON t3.c1 = t0.c0
      JOIN t1 ON t3.c0 = t1.c0
      ORDER BY t1.c1 DESC;
      

      The diagnostic alternative fixes the join order:

      SELECT t1.c1 AS ref0
      FROM t1
      STRAIGHT_JOIN t3 ON t3.c0 = t1.c0
      STRAIGHT_JOIN t0 ON t3.c1 = t0.c0
      ORDER BY t1.c1 DESC;
      

      Default plan

      The optimizer chooses:

      t3 full scan
        -> t1 ref lookup
        -> t0 ref lookup
        -> filesort
      

      Estimated cost:

      2165.444633
      

      ANALYZE FORMAT=JSON shows:

      filesort:
        r_output_rows: 6303964
        r_sort_passes: 12
      

      Three runtimes:

      19477.663 ms
      18778.653 ms
      19432.592 ms
      median: 19432.592 ms
      

      Faster alternative

      Forcing:

      t1 -> t3 -> t0
      

      produces:

      filesort t1
        -> t3 ref lookup
        -> t0 ref lookup
      

      Only the outer t1 input is sorted:

      filesort:
        r_output_rows: 502
      

      The sorting operation itself takes less than 1 ms in the measured executions.

      Estimated cost:

      2249.918896
      

      Three runtimes:

      2234.385 ms
      2565.211 ms
      2389.387 ms
      median: 2389.387 ms
      

      The alternative is approximately 8.13x faster despite being assigned a higher estimated cost.

      Optimizer trace / possible root cause

      I also collected optimizer trace with:

      SET optimizer_prune_level=0;
      

      The trace shows that the faster t1 -> t3 -> t0 join order is explicitly considered, so this is not simply a case where heuristic pruning removes the good join order.

      For t1 -> t3 -> t0, the trace reports:

      plan_prefix: "t1,t3"
      table: "t0"
      rows_for_plan: 2642528
      cost_for_plan: 439.3101042
      cost_for_sorting: 1810.608792
      

      giving the final cost:

      2249.918896
      

      For the selected t3 -> t1 -> t0 order, the trace reports:

      plan_prefix: "t3,t1"
      table: "t0"
      rows_for_plan: 2542230
      cost_for_plan: 423.9419111
      cost_for_sorting: 1741.502722
      

      giving:

      2165.444633
      

      and the trace selects:

      best_join_order: ["t3", "t1", "t0"]
      

      The notable point is that cost_for_sorting for both alternatives closely follows the estimated final join cardinality.

      However, the executable t1-first plan does not sort the final joined result.

      EXPLAIN FORMAT=JSON for that plan shows:

      read_sorted_file
        filesort
          table: t1
          rows: 502
      

      and ANALYZE FORMAT=JSON confirms that only 502 rows are actually sorted before the joins.

      The optimizer trace for the forced t1 -> t3 -> t0 plan still reports:

      rows_for_plan: 2642528
      cost_for_plan: 439.3101042
      cost_for_sorting: 1810.608792
      

      even though its physical plan sorts only the 502-row outer table.

      This suggests that join-order costing charges the t1-first alternative a sorting cost based on the estimated final join cardinality, while the final executable plan can satisfy ORDER BY by sorting only the small outer table before the joins.

      As a result, the sort-before-join benefit is not reflected correctly in the join-order ranking, and the optimizer selects the much slower plan.

      ORDER BY control

      Removing ORDER BY while keeping the same joins gives:

      r_total_time_ms: 2180.044
      

      This is close to the approximately 2.39-second median of the t1-first plan with ORDER BY.

      Therefore, the large slowdown is specifically associated with sort placement:

      Selected plan:
        join first
        -> sort 6,303,964 rows
       
      Faster plan:
        sort 502 rows
        -> join
      

      optimizer_prune_level control

      Setting:

      SET optimizer_prune_level=0;
      

      does not change the selected plan.

      The optimizer still chooses:

      t3 -> t1 -> t0
        -> filesort
      

      with cost:

      2165.444633
      

      The optimizer trace also explicitly contains the t1 -> t3 -> t0 alternative.

      Therefore, the issue appears to be in cost ranking rather than heuristic join-order pruning.

      Expected behavior

      The optimizer should account for the physical ordering property of the t1-first plan.

      Since ORDER BY can be satisfied by sorting only the 502-row outer table before the joins, this plan should not be charged a sorting cost comparable to sorting the estimated multi-million-row final join result.

      The optimizer should either choose the t1 -> t3 -> t0 plan or estimate its cost lower than the plan that sorts the full joined result.

      Actual behavior

      The optimizer assigns the t1-first plan a higher estimated cost and selects t3 -> t1 -> t0.

      The selected plan sorts 6,303,964 rows and takes about 19.43 seconds median.

      The higher-cost alternative sorts only 502 rows and takes about 2.39 seconds median.

      The selected plan is approximately 8.13x slower.

      Attachments

        1. 01_setup.sql
          75 kB
          Zack Zhang
        2. 02_query_test_with_trace.out
          281 kB
          Zack Zhang
        3. 02_query_test_with_trace.sql
          3 kB
          Zack Zhang
        4. mdev-41159.test
          1 kB
          Shipra Jain

        Issue Links

          Activity

            People

              psergei Sergei Petrunia
              zhaoyangzhang Zack Zhang
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

              Dates

                Created:
                Updated:

                Git Integration

                  Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.