Oracle Scratchpad

November 6, 2023

Swap_Join_Inputs

Filed under: Column groups,Hints,Joins,Oracle,Statistics — Jonathan Lewis @ 2:47 pm GMT Nov 6,2023

This is just a short note (I hope) prompted by a conversation on the Oracle-L list server. A query from a 3rd party application was doing a very large full tablescan with hash join when it should have been doing a high precision index driven nested loop join, and the poor choice of plan was due an optimizer defect when handling column groups (fixed in 23c) when one or more of the columns involved is always, or nearly always, null.

As a follow-up the owner of the problem asked what hints should go into an SQL Patch to make the optimizer use the nested loop. There’s a simple guideline that usually works for this type of “wrong join” problem: report the “Outline Data” from the current execution plan; find the relevant join hint(s) (in this case a use_hash() hint and a full() hint), change those join hint(s) (e.g. use_nl(), index()), and write the entire edited outline data into an SQL Patch watching out for a recently fixed defect in the SQL patch code.

There are, however, various refinements that add complexity to this strategy, as you can appreciate from a note I wrote some years ago about how to hint a hash join properly. This note is an example of handling one such refinement.

The query was a little complex, and the optimizer had unnested a subquery that consisted of a union all of 4 branches, and one of those branches had contributed a very large cardinality estimate to the total for the view, so the optimizer had chosen a hash join between the unnested subquery and a large table. Stripped to a bare minimum that part of the plan looked like this:

HASH JOIN
      VIEW                   VW_NSO_1
      TABLE ACCESS FULL      PO_LINE_LOCATIONS_ALL        

A quick scan of the Outline Data found the join hint (converted to lower case, with quotes removed): use_hash(@sel$ac90cd92 vw_nso_1@sel$ac90cd92), and an over-hasty response might be to convert the use_hash to a use_nl and leave it at that – except for three possible warnings:

  1. we wanted to see a nested loop into po_line_locations_all, so the suggested use_nl() hint would be looping into the wrong “table”
  2. the plan shows the view vw_nso_1 as the build table, while the reported hint is about vw_nso_1 being the second table in the join order
  3. there are further references to vw_nso_1 (and to po_line_locations_all) in the outline data

Here’s the complete set of original hints that might have been relevant to this particular part of the plan:

full(@sel$ac90cd92 po_line_locations_all@sel$2)
no_access(@sel$ac90cd92 vw_nso_1@sel$ac90cd92)
leading(@sel$ac90cd92 po_line_locations_all@sel$2 vw_nso_1@sel$ac90cd92)
use_hash(@sel$ac90cd92 vw_nso_1@sel$ac90cd92)
swap_join_inputs(@sel$ac90cd92 vw_nso_1@sel$ac90cd92)

index_rs_asc(@sel$b584fdd1 po_line_locations_all@sel$16 (......))
batch_table_access_by_rowid(@sel$b584fdd1 po_line_locations_all@sel$16)

index_rs_asc(@sel$5ed1c707 po_line_locations_all@sel$13 (......))
batch_table_access_by_rowid(@sel$5ed1c707 po_line_locations_all@sel$13)

index_rs_asc(@sel$2f35337b po_line_locations_all@sel$10 (......))
batch_table_access_by_rowid(@sel$2f35337b po_line_locations_all@sel$10)

index_rs_asc(@sel$1b7d9ae9 po_line_locations_all@sel$7 (......))

This is where knowing about the “Query Block / Object Alias” section the execution plans is important – I’ve split the list into several parts based on the query block (@sel$xxxxxxxxxx) they are aimed at, and it’s only the first 5 we need to worry about.

Conveniently this part of the plan is a self-contained query block (@sel$ac90cd92) and we can see why we have an apparent contradiction between vw_nso_1 being the second table in the join order while being the build table: it’s second because of the leading() hint which dictates the join order, but it becomes the build table, hence appearing to be the first table in the join order, because of the swap_join_inputs() hint.

What we want is a join order where vw_nso_1 really is the first table in the join order, followed by a nested loop join into po_line_locations_all, using an index (not the full tablescan that the current hints dictate). It would probably be a good idea to get rid of the redundant no_swap_join_inputs() hints at the same time because that hint applies only to hash joins. So I think we need to replace the 5 hints above with the following 4 hints:

no_access(@sel$ac90cd92 vw_nso_1@sel$ac90cd92)
leading(@sel$ac90cd92 vw_nso_1@sel$ac90cd92 po_line_locations_all@sel$2)
use_nl(@sel$ac90cd92 po_line_locations_all@sel$2)
index(@sel$ac90cd92 po_line_locations_all@sel$2(line_location_id))

The index hint references column line_location_id because that’s the access predicate used in original hash join and I’m assuming that there is an index that starts with that column. It’s always a bit hit and miss with hinting and it might have been sufficient (as a first attempt) to use the index hint without trying to reference a specific index, and there might be good reasons for adding more columns to the list, or simple naming the index rather than describing it.

It’s quite likely that if this change in the hints is sufficient the resulting Outline Data would look a little different anyway; in particular the index() hint that I’ve suggested might get expanded to index_rs_asc(), and there might be a batch_table_access_by_rowid() added. Basically you do test runs until you get the result you want and then use the resulting Outline Data for the patch (although, occasionally, you still find that the Outline Data doesn’t reproduce the plan that it came from).

Frivolous Footnote

There were 75 hints totalling 3,191 bytes in the original Outline Data. If the text gets too long and messy for you to cope with when you create the patch you can probably remove all the double quotes, all the table names from the fully qualified column names in indexes, all the outline() and outline_leaf() hints, all the opt_param() hints that reflect system level parameter changes and the begin/end_outline_data hints and the ignore_optim_embedded_hints hint. You could also change long index descriptions to index_names and, if you’re like me, change it all to lower case anyway because I hate reading in capitals – and if you do change it all to lower case you have to remove the quotes. When I did all this to the original outline data the result was 1,350 bytes for 30 hints.

3 Comments »

  1. […] Swap_Join_Inputs […]

    Pingback by Hinting Catalogue | Oracle Scratchpad — November 6, 2023 @ 3:09 pm GMT Nov 6,2023 | Reply

  2. I think my approach would be ever so slightly different.
    Depending on the time pressures and exact circumstances, this might involve running the SQL and reproducing the issue in prod and/or non-prod – hinting the sql if necessary to get that bad plan.
    Then in some manual runs of the same SQL, manually hint it simply to get the desired shape/impact and subject to validation then take the entire outline of THAT/THOSE executions top and tail it into a SQL Patch.

    Comment by Dom Brooks — November 7, 2023 @ 2:49 pm GMT Nov 7,2023 | Reply

    • Dom,
      Thanks for the comment.

      As you say – time and circumstances will have an impact on approach. The note generated a little activity on X-twitter, where I made the comment:

      There’s what I do because I can anticipate side effects, then there’s what I suggest someone else does. It took a few minutes to spot that the problem was the tablescan cardinality, leading to a bad unnested view cardinality, where an “easy” test (not fix) would be to add a hint like: cardinality(@sel$ac90cd92 vw_nso_1@sel$ac90cd92 20). The cardinality() hint is undocumented, of course, but this is a test to see if it works around the hash join / nl issue. If the correct plan had dropped out (which might have included a translation from cardinality() to opt_estimate()) it would have been an alternative way to get the Outline Data that needed to be in the SQL Patch. My strategy (for others) is never depend on anything less than a complete hint set to get, and keep, the execution plan you need.

      Adding the cardinality hint (maybe just /*+ cardinality(@vw_nso_1 20) */ would be my equivalent of your “manually hint it simply”. But when advising someone what to do by email – and especially when it’s such a well-defined problem, viz: change a specific hash join to a nested loop join – I’d rather make suggestions that minimise the chance of misunderstandings, or the need for further initiative to work around unexpected results.

      The really important point that you comment highlights, though, is that if you experiment with a couple of hints and get the plan you want you haven’t proved that your hints solve the problem unless the plan you got before you added the hints was the bad plan that you’re trying to work around. (I would argue, though, that you’ve got to get the bad plan without adding any hints for the test to be valid.)

      Regards
      Jonathan Lewis

      Comment by Jonathan Lewis — November 8, 2023 @ 11:15 am GMT Nov 8,2023 | Reply


RSS feed for comments on this post. TrackBack URI

Comments and related questions are welcome.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Website Powered by WordPress.com.