From aff928adbe10ab6da91d578cbf47eed71a42a01b Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Sat, 17 Jun 2023 14:27:01 -0600 Subject: [PATCH] opt: infer equality filters for self joins When a table is joined to itself with an equality that forms a key over both join inputs, it is possible to infer equality filters between each pair of columns at the same ordinal position in the base table. This patch improves the logical props builder to infer these self-join equalities in a join's FuncDepSet. This improves the quality of information available to optimization rules, and in particular, join elimination rules. Informs #102614 Release note: None --- .../execbuilder/testdata/distsql_single_flow | 70 ++++--- pkg/sql/opt/exec/execbuilder/testdata/join | 16 +- .../testdata/sql_activity_stats_compaction | 148 +++++--------- .../opt/exec/execbuilder/testdata/update_from | 44 ++--- pkg/sql/opt/memo/logical_props_builder.go | 64 ++++++ pkg/sql/opt/memo/testdata/logprops/join | 183 +++++++++++++++++- pkg/sql/opt/norm/testdata/rules/combo | 2 +- pkg/sql/opt/norm/testdata/rules/join | 96 +++++---- pkg/sql/opt/norm/testdata/rules/project | 64 +++--- pkg/sql/opt/norm/testdata/rules/prune_cols | 2 +- pkg/sql/opt/optbuilder/testdata/update_from | 52 +++-- pkg/sql/opt/ordering/lookup_join_test.go | 8 +- pkg/sql/opt/xform/testdata/external/hibernate | 31 ++- pkg/sql/opt/xform/testdata/physprops/ordering | 5 +- pkg/sql/opt/xform/testdata/rules/join_order | 106 ++++++---- 15 files changed, 543 insertions(+), 348 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/testdata/distsql_single_flow b/pkg/sql/opt/exec/execbuilder/testdata/distsql_single_flow index 943cc94adc6b..07bbd7945ea5 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/distsql_single_flow +++ b/pkg/sql/opt/exec/execbuilder/testdata/distsql_single_flow @@ -1,16 +1,18 @@ # LogicTest: 5node statement ok -CREATE TABLE t (a INT PRIMARY KEY, b INT, c INT) +CREATE TABLE t1 (a INT PRIMARY KEY, b INT, c INT); +CREATE TABLE t2 (a INT PRIMARY KEY, b INT, c INT); # Move the single range to a remote node. statement ok -ALTER TABLE t EXPERIMENTAL_RELOCATE VALUES (ARRAY[2], 2); +ALTER TABLE t1 EXPERIMENTAL_RELOCATE VALUES (ARRAY[2], 2); +ALTER TABLE t2 EXPERIMENTAL_RELOCATE VALUES (ARRAY[2], 2); # There are no stats on the table, so the single flow should stay on the remote # node. query T -EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b +EXPLAIN (VEC) SELECT * FROM t1, t2 WHERE t1.a = t2.b ---- │ ├ Node 1 @@ -22,7 +24,7 @@ EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b └ *colfetcher.ColBatchScan query T -EXPLAIN (DISTSQL) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b +EXPLAIN (DISTSQL) SELECT * FROM t1, t2 WHERE t1.a = t2.b ---- distribution: full vectorized: true @@ -33,20 +35,42 @@ vectorized: true │ ├── • scan │ missing stats -│ table: t@t_pkey +│ table: t1@t1_pkey │ spans: FULL SCAN │ └── • scan missing stats - table: t@t_pkey + table: t2@t2_pkey spans: FULL SCAN · -Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJysklFr2zAQx9_3KcQ9JUNpLDnbg6Dg0njUI026OLDBKEWxL4mpY3nSma6EfPdhp10T06TbmB6MdTr97v8_3QbcjxwUhN9uRhfRmHWGUTyLv4y6LA5H4eWMvWefppNrRuwiZiT4049kX6_CachInGl2zkiezYFDYVIc6zU6UN9BAAcJtxxKaxJ0ztg6vGmSovQnKI9DVpQV1eFbDomxCGoDlFGOoGBseqbs-8AhRdJZ3qRtOZiKXi450ksENdirEg1B-Vu-V0icLjTT8xynqFO0fe-gHFBAd-U9PgKHS5NX68Ippjmbc5YAh7jUdaAHx2SJlizvX2WJ_ypLtmSJo7Je1FSFsSlaTNvv8XbKK96utFt9NlmBti8PreW4oE4guuc2W66oE8gucJhUpFggeCB54PNgwIMPPPh41J_f8icP_L0xdlN0pSkc_tHcea1KPVG7xXSJu-45U9kEb6xJmtzddtKAmkCKjnang90mKp6PHFnU699Ts08SJ0n-AUmcJMm_IMl9kmiT_JMk77g7WXdskZuHuywFBd7T6r3yeV5QX9BLVz9bvDIPDXb2WNZNX-jcIYdrfY9DJLTrrMgcZQkoshVut-9-BQAA__9iPZLx +Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJykkt9v2jAQx9_3V1j3VKajxA7bg6VKmQpTmSh0gLRJU1WZ5ICoIc7si7oK8b9PCesKUaH74QdLPp8_9_2ebwP-ewYa-l9vhh8GI3HWG0xn08_Dlpj2h_3LmXgrPk7G14IlClbiy1V_0hcsz424EKzO54CQ24RGZk0e9DeQgKDgFqFwNibvravCmzppkPwAHSCkeVFyFb5FiK0j0BvglDMCDSPbtkUnBISE2KRZnbZFsCU_P_JslgS6u1dl0AMdbnGvkDxdaGbmGU3IJOQ6wUE5YBmxvCvu6REQLm1WrnOvhUExRxEDwrQwVaANx4TJhrDgX4XJhjAVsfofYaohTB4V9qynzK1LyFHS_JPXU15wd2X86pNNc3IddWguowWfRbJ14dLlis8i1QKEcclaRBIjhVGIURejdxi9P-ovbPhTB_5eGb0J-cLmnv5o9oJGpbas3FKypF33vC1dTDfOxnXu7jiuQXUgIc-72-7uMMifrjw7Muvfc7NPkidJ4QFJniSpvyCpfZJsksKTpOC4O1V1bJHZh7s0AQ3Br9V-YXtaUD0wS19923RlH2rs7LGomr4wmSeEa3NPPWJy6zRPPacxaHYlbbdvfgYAAP__MJ2RJw== # Inject stats so that column 'b' has few unique values whereas column 'c' has # many unique values. statement ok -ALTER TABLE t INJECT STATISTICS '[ +ALTER TABLE t1 INJECT STATISTICS '[ + { + "columns": ["a"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 10000, + "distinct_count": 10000 + }, + { + "columns": ["b"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 10000, + "distinct_count": 3 + }, + { + "columns": ["c"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 10000, + "distinct_count": 100 + } +]' + +statement ok +ALTER TABLE t2 INJECT STATISTICS '[ { "columns": ["a"], "created_at": "2018-01-01 1:00:00.00000+00:00", @@ -69,7 +93,7 @@ ALTER TABLE t INJECT STATISTICS '[ # Now check that the single flow with a join is moved to the gateway. query T -EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b +EXPLAIN (VEC) SELECT * FROM t1, t2 WHERE t1.a = t2.b ---- │ └ Node 1 @@ -78,7 +102,7 @@ EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b └ *colfetcher.ColBatchScan query T -EXPLAIN (DISTSQL) SELECT * FROM t AS t1, t AS t2 WHERE t1.a = t2.b +EXPLAIN (DISTSQL) SELECT * FROM t1, t2 WHERE t1.a = t2.b ---- distribution: local vectorized: true @@ -90,20 +114,20 @@ vectorized: true │ ├── • scan │ estimated row count: 10,000 (100% of the table; stats collected ago) -│ table: t@t_pkey +│ table: t1@t1_pkey │ spans: FULL SCAN │ └── • scan estimated row count: 10,000 (100% of the table; stats collected ago) - table: t@t_pkey + table: t2@t2_pkey spans: FULL SCAN · -Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJysktFr2zAQxt_3V4h7asaltZRtD4KCS5NRj7Tp4sAGwxTFviSmjuVJZ7oS8r8PO92amHSjY3ow1un0u-_70Ab89wI0jL7eji-iG3EyjOJZ_HncE_FoPLqcibfi43RyLVhcxIIlPv0o8eVqNB0JlqdGnAtWp3NAKG1GN2ZNHvQ3kJAgVM6m5L11TWnTNkTZD9ABQl5WNTflBCG1jkBvgHMuCDTMzLygKZmM3FkACBmxyYsWyyHfVff0CAiXtqjXpdfCoJijSAEhrkxT6EOyRbA1P4_wbJYEWu5pioaggy3-myz5X2Wpjiz5oqxnNXVpXUaOsgMlSXPzby1HvF0Zv_pk85LcmTq0VtCCT0LZO3f5csUnoeoBwqRmLUKJocJwgOE7DN9j-OFFf4OOP_Wa2KfkK1t66vo8OinoTOrLxi1lS9ql523tUrp1Nm17d9tJC2oLGXnenardJirbI9lMcGTWv1_NPkm-gqT2SbJLUn8kDQ5IwaGmBGFR2Ie7PAMNwdPqH_n8WtBcMEvfhB2v7EOLnT1WTVQLU3hCuDb3NCQmt87L3HOegmZX03b75mcAAAD__wrGV6A= +Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJykklFr2zAQx9_3KcQ9JePSWsq2B0FBo8loRtp0SWCDYYpiXxJTx_KkM10J-e7DzrYmJt3opgeDTtLvfv_DWwjfctAw_HI7fj-6EZ3BaDaffRp3xWw4Hl7OxWvxYTq5FixRsBKfr4bToWB5ZsWFYHW2AITCpXRjNxRAfwUJMULpXUIhOF-Xts2FUfoddISQFWXFdTlGSJwn0FvgjHMCDXO7yGlKNiV_HgFCSmyzvMGyNCzvynt6BIRLl1ebImhhUSxQJIAwK21d6EG8Q3AVPzUJbFcEWh5YjQagox3-m5hsiSnD6n_EVEtMPiv25FMVzqfkKT1yieuXf7tyIt2VDeuPLivIn6vjcDktuWNk98JnqzV3jOoCwqRiLYxEo9D00bxB8xbNu2fz9Vv51EsGP6VQuiJQO-fJTlGrU0_WaSld0X56wVU-oVvvkubufjtpQE0hpcD7U7XfjIrmSNYdPNnN7__mkCRfQFKHJNkmqT-S-kek6NgpRljm7uEuS0FD9HP1Tnx-Lagf2FWohz1bu4cGO38s61EtbR4I4dre04CY_CYrssBZApp9Rbvdqx8BAAD__-o0VdY= # If we add a not very selective filter, the flow is still moved to the gateway. query T -EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.b = 1 AND t1.a = t2.a +EXPLAIN (VEC) SELECT * FROM t1, t2 WHERE t1.b = 1 AND t1.a = t2.a ---- │ └ Node 1 @@ -113,7 +137,7 @@ EXPLAIN (VEC) SELECT * FROM t AS t1, t AS t2 WHERE t1.b = 1 AND t1.a = t2.a └ *colfetcher.ColBatchScan query T -EXPLAIN (DISTSQL) SELECT * FROM t AS t1, t AS t2 WHERE t1.b = 1 AND t1.a = t2.a +EXPLAIN (DISTSQL) SELECT * FROM t1, t2 WHERE t1.b = 1 AND t1.a = t2.a ---- distribution: local vectorized: true @@ -126,7 +150,7 @@ vectorized: true │ ├── • scan │ estimated row count: 10,000 (100% of the table; stats collected ago) -│ table: t@t_pkey +│ table: t2@t2_pkey │ spans: FULL SCAN │ └── • filter @@ -135,14 +159,14 @@ vectorized: true │ └── • scan estimated row count: 10,000 (100% of the table; stats collected ago) - table: t@t_pkey + table: t1@t1_pkey spans: FULL SCAN · -Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJyskl9r2zAUxd_3KcR9ajalsWRnDEHAoUmZR_50cWCDEYpi36SmjuVJMl0J-e7DdrcmJsmWMT8Y6Ur6nXN0tQXzPQUBw693o34wIVeDIJyHn0ctEg5Hw5s5eUtuZ9MxsaQfEsvoy4CTLx-HsyG5sux6SXqEtUh_MqimkvSI5deyBRQyFeNEbtCA-AYMFhRyrSI0RumytK02BPEPEA6FJMsLW5YXFCKlEcQWbGJTBAFzuUxxhjJG3XGAQoxWJmmFtb69zx_xGSjcqLTYZEYQScmSkggohLksC21Y7Ciowr5KGCvXCILteQoGIJwd_Tdb7L_a4g1b7BJbt0lqUaPu8ENPdV0Qn5ctE0IEk_mHkxbchgV-0sKrcpEpHaPG-EB4UZ7805YjOcao1_hJJRnqjnsYJcWVvfLZu1ZPJ-uHeggUpoUVxPeo36X-e-oz6nPquycjeo2I7iW3PFFtlXe8ZtKjQt2GkHeJ0AxNrjKDf6XkNJTarLxZjNdYd8qoQkd4p1VU7a2n0wpUFWI0tl5160mQVUusVNAoN79f4z6JnSXxA5KzT3KaJH6BJ75P4k2Se5bknfbkNkneWVL3XLoFhVWqnu6TGAQ4L1_7yO_XB-UBuTblAwgf1FOFnT_nZftWMjVIYSwfcYAW9SbJEmOTCITVBe52b34GAAD__75k2MI= +Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJykkl9v2jAUxd_3Kaz7BJspsROmyRJSqkK1TPzpAGmTJlSZ5JJGDXFmO-oqxHefkmwrRMDG6ocovrZ_5xxfb8F8T0HA8Ovd6DqYkNYgmC_mn0dtMh-OhjcL8pbczqZjYhkllpMvH4ezIWlZdrUifcLa5HoyqKaS9InlV7INFDIV4URu0ID4BgyWFHKtQjRG6bK0rTYE0Q8QDoUkywtblpcUQqURxBZsYlMEAQu5SnGGMkLddYBChFYmaYW13Lf8Pn_EZ6Bwo9JikxlBJCUrSkKgMM9lWejAckdBFfZFxFgZIwi25yoYgHB29P-MsYYx5lv2GmO8YYxdYuw2SS1q1F1-6KquC-Lzsm1CiGCy-HDSgtuwwE9aeFEuMqUj1BgdCC_Lk3_bciTHGHWMn1SSoe66h1FSXNuWz961-zqJH-pfoDAtrCC-R_0e9d9Tn1GfU989GdFrRHQvueWJ6qi86zWTHhXqNYS8S4RmaHKVGfwnJaeh1GHlzWIUY90powod4p1WYbW3nk4rUFWI0Nh61a0nQVYtsVJBo9z8eY37JHaWxA9Izj7JaZL4BZ74Pok3Se5Zknfak9skeWdJvXPplhTWqXq6TyIQ4PwanSOf3wPKAzI25QOYP6inCrt4zsv2rWVqkMJYPuIALepNkiXGJiEIqwvc7d78DAAA___NVNb4 # However, if we add a selective filter, the flow is kept on the remote node. query T -EXPLAIN (VEC) SELECT * FROM t AS t1 INNER MERGE JOIN t AS t2 ON t1.a = t2.a WHERE t1.c = 1 +EXPLAIN (VEC) SELECT * FROM t1 INNER MERGE JOIN t2 ON t1.a = t2.a WHERE t1.c = 1 ---- │ ├ Node 1 @@ -155,7 +179,7 @@ EXPLAIN (VEC) SELECT * FROM t AS t1 INNER MERGE JOIN t AS t2 ON t1.a = t2.a WHER └ *colfetcher.ColBatchScan query T -EXPLAIN (DISTSQL) SELECT * FROM t AS t1 INNER MERGE JOIN t AS t2 ON t1.a = t2.a WHERE t1.c = 1 +EXPLAIN (DISTSQL) SELECT * FROM t1 INNER MERGE JOIN t2 ON t1.a = t2.a WHERE t1.c = 1 ---- distribution: full vectorized: true @@ -172,12 +196,12 @@ vectorized: true │ │ │ └── • scan │ estimated row count: 10,000 (100% of the table; stats collected ago) -│ table: t@t_pkey +│ table: t1@t1_pkey │ spans: FULL SCAN │ └── • scan estimated row count: 10,000 (100% of the table; stats collected ago) - table: t@t_pkey + table: t2@t2_pkey spans: FULL SCAN · -Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJysktFr2zAQxt_3V4h7ajelsWx3DEHBpXU3l8Tp7MAGoxTVvqSmjuVJMl0J-d-H7HZNTJMuY34I8afT777vfEvQP0vgEH6_Gp1GMTk4j9Jp-nV0SNJwFJ5NyXtykUzGxJDTlBhGojgOEzIOk88huZxE8dOBSyYxMexIkBNi3CNBvn0Jk9AqGTkhDChUMsdYLFAD_wFWcOGaQq1khlpLZeVlWxTlv4A7FIqqboyVrylkUiHwJZjClAgcYjmQ9dAHCjkaUZRt2YqCbMzLJW3EHIEfr3WJzoH7K7rWiO1uNBW3JSYoclRDZ6MdmMDc1Pf4CBTOZNksKs2JoOSWkgwopLWwwgC22WI9W84-ti6K0qBCNWSbnjqdk8CzU-ecR_H001YLbs8C-9fJuP91Ml7PlrvV1oubppIqR4V5fyXeLnkl2xjVHC9lUaEaepvZSpyZg4B9ODxRxfyu-wsUJo3hJGA0cGng0cCnwTENPm6N6PciehsR31j-BHUtK41_tf1Or9OA2cCYz7EboJaNyvBKyayt7V4nLagVctSmOz3uXqLq-UgbhWLxZ3fXSWwnyd2D5O4k-Rsktk5ifZK3B8ldJ7l9kr-T5GxP59nZz0r5cFPkwMF5egav_Dw_YC-IubYLkN7JhxY7fazt55uJUiOFsbjHczSoFkVVaFNkwI1qcLV69zsAAP__78Xc4A== +Diagram: https://cockroachdb.github.io/distsqlplan/decode.html#eJykk1Fr2zAUhd_3K8R9ajelsWRnDEHAo3E3l8TpnMAGoxTVvklNHcuTZLoS8t-H7XVNTJMumx9CfHT13XOupTWYHzkICL5djT-GETkZhbP57Mv4lMyCcXA-J2_JRTydEMtIGEVBTCZB_Ckgl9MwIpaTaUQsO5NkSCw_k-Tr5yAOaiUhQ8KAQqFSjOQKDYjvUAscrimUWiVojNK1vG6KwvQnCIdCVpSVreVrConSCGINNrM5goBI9VTZ94BCilZmeVO2oaAq-7zJWLlEEIOtLuEIhLehW43Y4UZzeZtjjDJF3Xd22oFlvmU35T0-AoVzlVerwggiKbmlJAEKs1LWQg_2GWMdY84xxi6y3KJG3We7rlpdEN-t5y6ECKP5h70WeMcC-9fZ8M5suG_5_8zG7Rjje409-6kKpVPUmHaPxeslL6SboF7ipcoK1H13N12OC3vis3enQ50t79q_QGFaWUF8Rn1OfZf6HvUH1H-_N6LXiejuRHzlAsRoSlUY_Ksb4HQ69VgdGNMltgM0qtIJXmmVNLXt67QBNUKKxrarg_YlLJ6WjNUoV39O7zaJHSTxI0j8IMnbIbFtEuuS3CNIfJvEuyTvIMnZn86tZ7_I1cNNloIA5_fTe-Hn6YF6g1ya-gDM7tRDg50_lvXnW8jcIIWJvMcRWtSrrMiMzRIQVle42bz5FQAA__89HtsW diff --git a/pkg/sql/opt/exec/execbuilder/testdata/join b/pkg/sql/opt/exec/execbuilder/testdata/join index 411d16ade524..193a5d5264a3 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/join +++ b/pkg/sql/opt/exec/execbuilder/testdata/join @@ -939,32 +939,30 @@ vectorized: true spans: FULL SCAN query T -EXPLAIN (VERBOSE) SELECT * FROM pkBAC AS l JOIN pkBAC AS r USING(a, b, c) +EXPLAIN (VERBOSE) SELECT * FROM pkBAC AS l JOIN pkBAC AS r USING(a, b) ---- distribution: local vectorized: true · • project -│ columns: (a, b, c, d, d) +│ columns: (a, b, c, d, c, d) │ └── • merge join (inner) │ columns: (a, b, c, d, a, b, c, d) - │ estimated row count: 1 (missing stats) - │ equality: (b, a, c) = (b, a, c) - │ left cols are key - │ right cols are key - │ merge ordering: +"(b=b)",+"(a=a)",+"(c=c)" + │ estimated row count: 100 (missing stats) + │ equality: (b, a) = (b, a) + │ merge ordering: +"(b=b)",+"(a=a)" │ ├── • scan │ columns: (a, b, c, d) - │ ordering: +b,+a,+c + │ ordering: +b,+a │ estimated row count: 1,000 (missing stats) │ table: pkbac@pkbac_pkey │ spans: FULL SCAN │ └── • scan columns: (a, b, c, d) - ordering: +b,+a,+c + ordering: +b,+a estimated row count: 1,000 (missing stats) table: pkbac@pkbac_pkey spans: FULL SCAN diff --git a/pkg/sql/opt/exec/execbuilder/testdata/sql_activity_stats_compaction b/pkg/sql/opt/exec/execbuilder/testdata/sql_activity_stats_compaction index d2e8f79dc57a..9e837c118008 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/sql_activity_stats_compaction +++ b/pkg/sql/opt/exec/execbuilder/testdata/sql_activity_stats_compaction @@ -267,33 +267,19 @@ vectorized: true └── • project │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) │ - └── • project - │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) + └── • lookup join (inner) + │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) + │ estimated row count: 0 + │ table: statement_statistics@primary + │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8,aggregated_ts,fingerprint_id,transaction_fingerprint_id,plan_hash,app_name,node_id) + │ equality cols are key │ - └── • lookup join (inner) - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) - │ estimated row count: 0 - │ table: statement_statistics@primary - │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8,aggregated_ts,fingerprint_id,transaction_fingerprint_id,plan_hash,app_name,node_id) - │ equality cols are key - │ - └── • render - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq: mod(fnv32(crdb_internal.datums_to_bytes(aggregated_ts, app_name, fingerprint_id, node_id, plan_hash, transaction_fingerprint_id)), 8) - │ render aggregated_ts: aggregated_ts - │ render fingerprint_id: fingerprint_id - │ render transaction_fingerprint_id: transaction_fingerprint_id - │ render plan_hash: plan_hash - │ render app_name: app_name - │ render node_id: node_id - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8: crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8 - │ - └── • scan - columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) - estimated row count: 1,024 (0.10% of the table; stats collected ago) - table: statement_statistics@primary - spans: /0-/0/2022-05-04T15:59:59.999999001Z - limit: 1024 + └── • scan + columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) + estimated row count: 1,024 (0.10% of the table; stats collected ago) + table: statement_statistics@primary + spans: /0-/0/2022-05-04T15:59:59.999999001Z + limit: 1024 statement ok ALTER TABLE system.transaction_statistics INJECT STATISTICS '[ @@ -481,31 +467,19 @@ vectorized: true └── • project │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, app_name, node_id) │ - └── • project - │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) + └── • lookup join (inner) + │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) + │ estimated row count: 0 + │ table: transaction_statistics@primary + │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8,aggregated_ts,fingerprint_id,app_name,node_id) + │ equality cols are key │ - └── • lookup join (inner) - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) - │ estimated row count: 0 - │ table: transaction_statistics@primary - │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8,aggregated_ts,fingerprint_id,app_name,node_id) - │ equality cols are key - │ - └── • render - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq: mod(fnv32(crdb_internal.datums_to_bytes(aggregated_ts, app_name, fingerprint_id, node_id)), 8) - │ render aggregated_ts: aggregated_ts - │ render fingerprint_id: fingerprint_id - │ render app_name: app_name - │ render node_id: node_id - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8: crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8 - │ - └── • scan - columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) - estimated row count: 1,024 (0.10% of the table; stats collected ago) - table: transaction_statistics@primary - spans: /0-/0/2022-05-04T15:59:59.999999001Z - limit: 1024 + └── • scan + columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) + estimated row count: 1,024 (0.10% of the table; stats collected ago) + table: transaction_statistics@primary + spans: /0-/0/2022-05-04T15:59:59.999999001Z + limit: 1024 query T EXPLAIN (VERBOSE) @@ -568,33 +542,19 @@ vectorized: true └── • project │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) │ - └── • project - │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) + └── • lookup join (inner) + │ columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) + │ estimated row count: 0 + │ table: statement_statistics@primary + │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8,aggregated_ts,fingerprint_id,transaction_fingerprint_id,plan_hash,app_name,node_id) + │ equality cols are key │ - └── • lookup join (inner) - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, statistics, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) - │ estimated row count: 0 - │ table: statement_statistics@primary - │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8,aggregated_ts,fingerprint_id,transaction_fingerprint_id,plan_hash,app_name,node_id) - │ equality cols are key - │ - └── • render - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq, aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8_eq: mod(fnv32(crdb_internal.datums_to_bytes(aggregated_ts, app_name, fingerprint_id, node_id, plan_hash, transaction_fingerprint_id)), 8) - │ render aggregated_ts: aggregated_ts - │ render fingerprint_id: fingerprint_id - │ render transaction_fingerprint_id: transaction_fingerprint_id - │ render plan_hash: plan_hash - │ render app_name: app_name - │ render node_id: node_id - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8: crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8 - │ - └── • scan - columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) - estimated row count: 1,024 (0.10% of the table; stats collected ago) - table: statement_statistics@primary - spans: /0/2022-05-04T14:00:00Z/"123"/"234"/"345"/"test"/1-/0/2022-05-04T15:59:59.999999001Z - limit: 1024 + └── • scan + columns: (aggregated_ts, fingerprint_id, transaction_fingerprint_id, plan_hash, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_plan_hash_transaction_fingerprint_id_shard_8) + estimated row count: 1,024 (0.10% of the table; stats collected ago) + table: statement_statistics@primary + spans: /0/2022-05-04T14:00:00Z/"123"/"234"/"345"/"test"/1-/0/2022-05-04T15:59:59.999999001Z + limit: 1024 query T EXPLAIN (VERBOSE) @@ -652,31 +612,19 @@ vectorized: true └── • project │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, app_name, node_id) │ - └── • project - │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) + └── • lookup join (inner) + │ columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) + │ estimated row count: 0 + │ table: transaction_statistics@primary + │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8,aggregated_ts,fingerprint_id,app_name,node_id) + │ equality cols are key │ - └── • lookup join (inner) - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8, execution_count, service_latency, cpu_sql_nanos, contention_time, total_estimated_execution_time, p99_latency) - │ estimated row count: 0 - │ table: transaction_statistics@primary - │ equality: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id) = (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8,aggregated_ts,fingerprint_id,app_name,node_id) - │ equality cols are key - │ - └── • render - │ columns: (crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq, aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8_eq: mod(fnv32(crdb_internal.datums_to_bytes(aggregated_ts, app_name, fingerprint_id, node_id)), 8) - │ render aggregated_ts: aggregated_ts - │ render fingerprint_id: fingerprint_id - │ render app_name: app_name - │ render node_id: node_id - │ render crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8: crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8 - │ - └── • scan - columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) - estimated row count: 1,024 (0.10% of the table; stats collected ago) - table: transaction_statistics@primary - spans: /0/2022-05-04T14:00:00Z/"123"/"test"/2-/0/2022-05-04T15:59:59.999999001Z - limit: 1024 + └── • scan + columns: (aggregated_ts, fingerprint_id, app_name, node_id, crdb_internal_aggregated_ts_app_name_fingerprint_id_node_id_shard_8) + estimated row count: 1,024 (0.10% of the table; stats collected ago) + table: transaction_statistics@primary + spans: /0/2022-05-04T14:00:00Z/"123"/"test"/2-/0/2022-05-04T15:59:59.999999001Z + limit: 1024 statement ok RESET CLUSTER SETTING sql.stats.flush.interval diff --git a/pkg/sql/opt/exec/execbuilder/testdata/update_from b/pkg/sql/opt/exec/execbuilder/testdata/update_from index 3f30507397d3..e387ffa7a9b6 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/update_from +++ b/pkg/sql/opt/exec/execbuilder/testdata/update_from @@ -17,20 +17,13 @@ vectorized: true │ └── • render │ - └── • merge join - │ equality: (a) = (a) - │ left cols are key - │ right cols are key - │ - ├── • scan - │ missing stats - │ table: abc@abc_pkey - │ spans: FULL SCAN + └── • render │ └── • scan missing stats table: abc@abc_pkey spans: FULL SCAN + locking strength: for update # Update from another table. statement ok @@ -86,20 +79,13 @@ vectorized: true │ └── • render │ - └── • merge join - │ equality: (a) = (a) - │ left cols are key - │ right cols are key - │ - ├── • scan - │ missing stats - │ table: abc@abc_pkey - │ spans: FULL SCAN + └── • render │ └── • scan missing stats table: abc@abc_pkey spans: FULL SCAN + locking strength: for update # Check if RETURNING * returns everything query T @@ -126,27 +112,21 @@ vectorized: true │ render b: b │ render c: c │ - └── • merge join (inner) + └── • render │ columns: (a, b, c, a, b, c) - │ estimated row count: 1,000 (missing stats) - │ equality: (a) = (a) - │ left cols are key - │ right cols are key - │ merge ordering: +"(a=a)" - │ - ├── • scan - │ columns: (a, b, c) - │ ordering: +a - │ estimated row count: 1,000 (missing stats) - │ table: abc@abc_pkey - │ spans: FULL SCAN + │ render a: a + │ render b: b + │ render c: c + │ render a: a + │ render b: b + │ render c: c │ └── • scan columns: (a, b, c) - ordering: +a estimated row count: 1,000 (missing stats) table: abc@abc_pkey spans: FULL SCAN + locking strength: for update # Update values of table from values expression query T diff --git a/pkg/sql/opt/memo/logical_props_builder.go b/pkg/sql/opt/memo/logical_props_builder.go index b37ebbb63cd0..aa2194d911b2 100644 --- a/pkg/sql/opt/memo/logical_props_builder.go +++ b/pkg/sql/opt/memo/logical_props_builder.go @@ -24,6 +24,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/buildutil" + "github.com/cockroachdb/cockroach/pkg/util/intsets" "github.com/cockroachdb/errors" "github.com/cockroachdb/redact" ) @@ -2513,6 +2514,69 @@ func (h *joinPropsHelper) setFuncDeps(rel *props.Relational) { // created new possibilities for simplifying removed columns. rel.FuncDeps.ProjectCols(rel.OutputCols) } + h.addSelfJoinImpliedFDs(rel) +} + +// addSelfJoinImpliedFDs adds any extra equality FDs that are implied by a self +// join equality between key columns on a table. +func (h *joinPropsHelper) addSelfJoinImpliedFDs(rel *props.Relational) { + md := h.join.Memo().Metadata() + leftCols, rightCols := h.leftProps.OutputCols, h.rightProps.OutputCols + if !rel.FuncDeps.ComputeEquivClosure(leftCols).Intersects(rightCols) { + // There are no equalities between left and right columns. + return + } + // Map from the table ID to the column ordinals within the table. + getTables := func(cols opt.ColSet) map[opt.TableID]intsets.Fast { + var tables map[opt.TableID]intsets.Fast + cols.ForEach(func(col opt.ColumnID) { + if tab := md.ColumnMeta(col).Table; tab != opt.TableID(0) { + if tables == nil { + tables = make(map[opt.TableID]intsets.Fast) + } + colOrds := tables[tab] + colOrds.Add(tab.ColumnOrdinal(col)) + tables[tab] = colOrds + } + }) + return tables + } + leftTables := getTables(leftCols) + if leftTables == nil { + return + } + rightTables := getTables(rightCols) + if rightTables == nil { + return + } + for leftTable, leftTableOrds := range leftTables { + for rightTable, rightTableOrds := range rightTables { + if md.TableMeta(leftTable).Table.ID() != md.TableMeta(rightTable).Table.ID() { + continue + } + // This is a self-join. If there are equalities between columns at the + // same ordinal positions in each (meta) table and those columns form a + // key on each input, *every* pair of columns at the same ordinal position + // is equal. + var eqCols opt.ColSet + colOrds := leftTableOrds.Intersection(rightTableOrds) + for colOrd, ok := colOrds.Next(0); ok; colOrd, ok = colOrds.Next(colOrd + 1) { + leftCol, rightCol := leftTable.ColumnID(colOrd), rightTable.ColumnID(colOrd) + if rel.FuncDeps.AreColsEquiv(leftCol, rightCol) { + eqCols.Add(leftCol) + eqCols.Add(rightCol) + } + } + if !eqCols.Empty() && h.leftProps.FuncDeps.ColsAreStrictKey(eqCols) && + h.rightProps.FuncDeps.ColsAreStrictKey(eqCols) { + // Add equalities between each pair of columns at the same ordinal + // position, ignoring those that aren't part of the output. + for colOrd, ok := colOrds.Next(0); ok; colOrd, ok = colOrds.Next(colOrd + 1) { + rel.FuncDeps.AddEquivalency(leftTable.ColumnID(colOrd), rightTable.ColumnID(colOrd)) + } + } + } + } } func (h *joinPropsHelper) cardinality() props.Cardinality { diff --git a/pkg/sql/opt/memo/testdata/logprops/join b/pkg/sql/opt/memo/testdata/logprops/join index d2ef3a87543a..637c2c0dd3d5 100644 --- a/pkg/sql/opt/memo/testdata/logprops/join +++ b/pkg/sql/opt/memo/testdata/logprops/join @@ -23,6 +23,10 @@ exec-ddl CREATE TABLE abc (a INT, b INT, c INT, PRIMARY KEY (a, b, c)) ---- +exec-ddl +CREATE TABLE xyz (x INT, y INT, z INT, PRIMARY KEY (x, y)) +---- + exec-ddl CREATE TABLE ref ( r1 INT NOT NULL, @@ -2243,14 +2247,14 @@ full-join (hash) # Self-join case. Since the condition is equating a key column with itself, # every row from both inputs is guaranteed to be included in the join output # exactly once. -norm +norm disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM xysd INNER JOIN xysd AS a ON xysd.x = a.x ---- inner-join (hash) ├── columns: x:1(int!null) y:2(int) s:3(string) d:4(decimal!null) x:7(int!null) y:8(int) s:9(string) d:10(decimal!null) ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) ├── key: (7) - ├── fd: (1)-->(2-4), (3,4)~~>(1,2), (7)-->(8-10), (9,10)~~>(7,8), (1)==(7), (7)==(1) + ├── fd: (1)-->(2-4), (3,4)~~>(1,2), (7)-->(8-10), (9,10)~~>(7,8), (1)==(7), (7)==(1), (2)==(8), (8)==(2), (3)==(9), (9)==(3), (4)==(10), (10)==(4) ├── prune: (2-4,8-10) ├── interesting orderings: (+1) (-3,+4,+1) (+7) (-9,+10,+7) ├── scan xysd @@ -2275,7 +2279,7 @@ inner-join (hash) # Self-join case with a constant equality filter. The filter pushed down on both # sides of the join is redundant, so all rows on the left side of the join will # be preserved. -norm +norm disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM xysd INNER JOIN xysd AS a ON xysd.x = a.x WHERE xysd.x = 10 ---- inner-join (hash) @@ -2283,7 +2287,7 @@ inner-join (hash) ├── cardinality: [0 - 1] ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) ├── key: () - ├── fd: ()-->(1-4,7-10), (7)==(1), (1)==(7) + ├── fd: ()-->(1-4,7-10), (7)==(1), (1)==(7), (2)==(8), (8)==(2), (3)==(9), (9)==(3), (4)==(10), (10)==(4) ├── prune: (2-4,8-10) ├── select │ ├── columns: xysd.x:1(int!null) xysd.y:2(int) xysd.s:3(string) xysd.d:4(decimal!null) @@ -2372,7 +2376,7 @@ inner-join (cross) └── filters (true) # Case with a self-join in the input of an InnerJoin. -norm +norm disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM fk INNER JOIN (SELECT * FROM xysd INNER JOIN xysd AS a ON xysd.x = a.x) f(x) ON r1 = f.x ---- @@ -2380,7 +2384,7 @@ inner-join (hash) ├── columns: k:1(int!null) v:2(int) r1:3(int!null) r2:4(int) x:7(int!null) y:8(int) s:9(string) d:10(decimal!null) x:13(int!null) y:14(int) s:15(string) d:16(decimal!null) ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) ├── key: (1) - ├── fd: (1)-->(2-4), (7)-->(8-10), (9,10)~~>(7,8), (13)-->(14-16), (15,16)~~>(13,14), (7)==(3,13), (13)==(3,7), (3)==(7,13) + ├── fd: (1)-->(2-4), (7)-->(8-10), (9,10)~~>(7,8), (13)-->(14-16), (15,16)~~>(13,14), (7)==(3,13), (13)==(3,7), (8)==(14), (14)==(8), (9)==(15), (15)==(9), (10)==(16), (16)==(10), (3)==(7,13) ├── prune: (1,2,4,8-10,14-16) ├── interesting orderings: (+1) (+7) (-9,+10,+7) (+13) (-15,+16,+13) ├── scan fk @@ -2394,7 +2398,7 @@ inner-join (hash) │ ├── columns: xysd.x:7(int!null) xysd.y:8(int) xysd.s:9(string) xysd.d:10(decimal!null) a.x:13(int!null) a.y:14(int) a.s:15(string) a.d:16(decimal!null) │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) │ ├── key: (13) - │ ├── fd: (7)-->(8-10), (9,10)~~>(7,8), (13)-->(14-16), (15,16)~~>(13,14), (7)==(13), (13)==(7) + │ ├── fd: (7)-->(8-10), (9,10)~~>(7,8), (13)-->(14-16), (15,16)~~>(13,14), (7)==(13), (13)==(7), (8)==(14), (14)==(8), (9)==(15), (15)==(9), (10)==(16), (16)==(10) │ ├── prune: (8-10,14-16) │ ├── interesting orderings: (+7) (-9,+10,+7) (+13) (-15,+16,+13) │ ├── unfiltered-cols: (7-18) @@ -2883,3 +2887,168 @@ left-join (hash) └── eq [type=bool, outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)] ├── variable: r_b:2 [type=int] └── variable: b:7 [type=int] + +# It is possible to infer equality filters for a self-join on key columns. +norm disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) +SELECT * FROM xyz INNER JOIN xyz foo ON xyz.x = foo.x AND xyz.y = foo.y +---- +inner-join (hash) + ├── columns: x:1(int!null) y:2(int!null) z:3(int) x:6(int!null) y:7(int!null) z:8(int) + ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) + ├── key: (6,7) + ├── fd: (1,2)-->(3), (6,7)-->(8), (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3) + ├── prune: (3,8) + ├── interesting orderings: (+1,+2) (+6,+7) + ├── scan xyz + │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ ├── key: (1,2) + │ ├── fd: (1,2)-->(3) + │ ├── prune: (1-3) + │ ├── interesting orderings: (+1,+2) + │ └── unfiltered-cols: (1-5) + ├── scan xyz [as=foo] + │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ ├── key: (6,7) + │ ├── fd: (6,7)-->(8) + │ ├── prune: (6-8) + │ ├── interesting orderings: (+6,+7) + │ └── unfiltered-cols: (6-10) + └── filters + ├── eq [type=bool, outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + │ ├── variable: xyz.x:1 [type=int] + │ └── variable: foo.x:6 [type=int] + └── eq [type=bool, outer=(2,7), constraints=(/2: (/NULL - ]; /7: (/NULL - ]), fd=(2)==(7), (7)==(2)] + ├── variable: xyz.y:2 [type=int] + └── variable: foo.y:7 [type=int] + +# Self-join filters cannot be inferred if not joining on a key. +norm +SELECT * FROM xyz INNER JOIN xyz foo ON xyz.x = foo.x +---- +inner-join (hash) + ├── columns: x:1(int!null) y:2(int!null) z:3(int) x:6(int!null) y:7(int!null) z:8(int) + ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more) + ├── key: (2,6,7) + ├── fd: (1,2)-->(3), (6,7)-->(8), (1)==(6), (6)==(1) + ├── prune: (2,3,7,8) + ├── interesting orderings: (+1,+2) (+6,+7) + ├── scan xyz + │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ ├── key: (1,2) + │ ├── fd: (1,2)-->(3) + │ ├── prune: (1-3) + │ ├── interesting orderings: (+1,+2) + │ └── unfiltered-cols: (1-5) + ├── scan xyz [as=foo] + │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ ├── key: (6,7) + │ ├── fd: (6,7)-->(8) + │ ├── prune: (6-8) + │ ├── interesting orderings: (+6,+7) + │ └── unfiltered-cols: (6-10) + └── filters + └── eq [type=bool, outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + ├── variable: xyz.x:1 [type=int] + └── variable: foo.x:6 [type=int] + +# Self-join filters can be inferred even if the join key wasn't a key in the +# base table. +norm +SELECT * FROM xyz INNER JOIN xyz foo ON xyz.x = foo.x AND xyz.y = 1 AND foo.y = 1 +---- +inner-join (hash) + ├── columns: x:1(int!null) y:2(int!null) z:3(int) x:6(int!null) y:7(int!null) z:8(int) + ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one) + ├── key: (6) + ├── fd: ()-->(2,7), (1)-->(3), (6)-->(8), (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3) + ├── prune: (3,8) + ├── interesting orderings: (+1 opt(2)) (+6 opt(7)) + ├── select + │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ ├── key: (1) + │ ├── fd: ()-->(2), (1)-->(3) + │ ├── prune: (1,3) + │ ├── interesting orderings: (+1 opt(2)) + │ ├── scan xyz + │ │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ │ ├── key: (1,2) + │ │ ├── fd: (1,2)-->(3) + │ │ ├── prune: (1-3) + │ │ ├── interesting orderings: (+1,+2) + │ │ └── unfiltered-cols: (1-5) + │ └── filters + │ └── eq [type=bool, outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)] + │ ├── variable: xyz.y:2 [type=int] + │ └── const: 1 [type=int] + ├── select + │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ ├── key: (6) + │ ├── fd: ()-->(7), (6)-->(8) + │ ├── prune: (6,8) + │ ├── interesting orderings: (+6 opt(7)) + │ ├── scan xyz [as=foo] + │ │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ │ ├── key: (6,7) + │ │ ├── fd: (6,7)-->(8) + │ │ ├── prune: (6-8) + │ │ ├── interesting orderings: (+6,+7) + │ │ └── unfiltered-cols: (6-10) + │ └── filters + │ └── eq [type=bool, outer=(7), constraints=(/7: [/1 - /1]; tight), fd=()-->(7)] + │ ├── variable: foo.y:7 [type=int] + │ └── const: 1 [type=int] + └── filters + └── eq [type=bool, outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + ├── variable: xyz.x:1 [type=int] + └── variable: foo.x:6 [type=int] + +# The optimizer doesn't detect the contradiction here because the "y" filters +# pushed down before the join properties are calculated. +norm +SELECT * FROM xyz INNER JOIN xyz foo ON xyz.x = foo.x AND xyz.y = 1 AND foo.y = 2 +---- +inner-join (hash) + ├── columns: x:1(int!null) y:2(int!null) z:3(int) x:6(int!null) y:7(int!null) z:8(int) + ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-one) + ├── key: (6) + ├── fd: ()-->(2,7), (1)-->(3), (6)-->(8), (1)==(6), (6)==(1), (2)==(7), (7)==(2), (3)==(8), (8)==(3) + ├── prune: (3,8) + ├── interesting orderings: (+1 opt(2)) (+6 opt(7)) + ├── select + │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ ├── key: (1) + │ ├── fd: ()-->(2), (1)-->(3) + │ ├── prune: (1,3) + │ ├── interesting orderings: (+1 opt(2)) + │ ├── scan xyz + │ │ ├── columns: xyz.x:1(int!null) xyz.y:2(int!null) xyz.z:3(int) + │ │ ├── key: (1,2) + │ │ ├── fd: (1,2)-->(3) + │ │ ├── prune: (1-3) + │ │ ├── interesting orderings: (+1,+2) + │ │ └── unfiltered-cols: (1-5) + │ └── filters + │ └── eq [type=bool, outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)] + │ ├── variable: xyz.y:2 [type=int] + │ └── const: 1 [type=int] + ├── select + │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ ├── key: (6) + │ ├── fd: ()-->(7), (6)-->(8) + │ ├── prune: (6,8) + │ ├── interesting orderings: (+6 opt(7)) + │ ├── scan xyz [as=foo] + │ │ ├── columns: foo.x:6(int!null) foo.y:7(int!null) foo.z:8(int) + │ │ ├── key: (6,7) + │ │ ├── fd: (6,7)-->(8) + │ │ ├── prune: (6-8) + │ │ ├── interesting orderings: (+6,+7) + │ │ └── unfiltered-cols: (6-10) + │ └── filters + │ └── eq [type=bool, outer=(7), constraints=(/7: [/2 - /2]; tight), fd=()-->(7)] + │ ├── variable: foo.y:7 [type=int] + │ └── const: 2 [type=int] + └── filters + └── eq [type=bool, outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + ├── variable: xyz.x:1 [type=int] + └── variable: foo.x:6 [type=int] diff --git a/pkg/sql/opt/norm/testdata/rules/combo b/pkg/sql/opt/norm/testdata/rules/combo index 1436f2299dc4..e4ab57e43110 100644 --- a/pkg/sql/opt/norm/testdata/rules/combo +++ b/pkg/sql/opt/norm/testdata/rules/combo @@ -489,7 +489,7 @@ INNER JOIN a a3 ON a2.k = a3.k INNER JOIN a a4 ON a3.k = a4.k INNER JOIN a a5 ON a4.k = a5.k ---- -https://raduberinde.github.io/optsteps.html#eJzsvd9y3MiV53__ewr8dCUu_7iBYklFOhRhr90z0Q5Z7e327o1a4SirSzZbFNlLUrPTu70TE_MMfTlP5yfZqGKRLKDyzzmJTCABfHwjN6uAAjLPyczzOd88-X-effvfXj87f_btl6-__N2fi_9S_NM3X_-xWBbL8rurr968-fKb4g9ff_Vm_Yeq-PpNsSxPPhavNv80P59tPq_uP5_tf366-Xx2__np_ufzzeen95_PTz4-O3r25vrm0-8vPnx4dv7s-Pi4WP7qq6uLu4vl5XdXh4eHxV-f_vM3vymOy-KwPJpXxW9-893V4Y831z-s3t99d3VY_OOX__zHL__-j1_-vXh_ffn509XtefHxvPz_rz5fXhYX51Xx4XxW3J6fFj-cz4uP54uHD86KD-flF8XteVkWP5yX1fqi-cOH5Yv1py_Xny7Wn54VH8-r6vGes-LDeXVa3J5X8-KH8-rF-tOzh09nX6x_sixuz2dV8cP5bFZ_yI-rn86L5-VRdXZQ_-DD9-u_Hxwff_f5iy9mq-fV8fzgqHg-Ozo9-Ld_2_6tPKqONn9dPH3v7Lis1n8qvzgqy6evLo7OjsrqqJwfl2dHVXVcvTiqzo5ns81X50-Xly-Oy7PNH18elYud6483Vx-VL472b7A4ePXqeTk_qqr1e9zf8dWr54unP1TVzpvMjqsXmz-eHlXzvZ-4f8Cjana0-xNVdX_H9a_c3_Hs6Y6zL7bfmpVHs8pyx_snPpp9cbS949nTHatt4_-ybfyLq6vVzfEP1xdXxfO_L2__vvl48z-Dea2948HCluXJ2siW5cnazpblydrUluXJ2tqW5cn7m-__-peLq7vVzdXy8i-f_uX9-7_cXXxa3d4tP_14_mL9jbvlXy9X1xffn7_cONeDgS6rk7WNLquTjZkuq5ONpS6rk42xLiv3rcvZ-iuP9y5PN575aOHL2cnGyJezk42dL2cnG1Nfzk421r6cuW9ffbH-yuPtq3Lj2I8usjw92XjJ8vRk4yjL05ONryxPTzbusjz13P7l-itPt19sBoxHH1vOTzZutpyfbDxtOT_ZONtyfrL2t_W_ztvPTtdfebz9bG7o7E-fL-8ufry8eH9x99N5cbn6cHd8c_2_bp-v_nX5_u7yp-Prq9XBUXFz8be_bz_436ub6-Prm-NP1zcrk_U0_L7x6b7zvzQ7__3f6-5_anX_h8_q_l6VBn9_cPXtp3oPXxg8_MGtHz71-fTc4NMPLrz9dN-LG03pcuX_wKMz8Wh7p0TzvP-wOGDl-FLGflh3wtYe6GgEgQfhSGJH8jZebIO32L2xywdi_ju273-JXfN9f3N9e2u_ZExmLH3JB3twtGR3ViF9hNv3y6tiWbxd3r5alu98V3XcrcrH2XaA9923__wS0AshzVqFN2uPVm1sW79pm9o2pt0-3vji8m51c-v-elG_5t5KtxTmvCzeXn--W928er4eEN9fX93e3Swvru5uXz3_VXlePP_Vm__--nVxXLw7sPefqbtn3u7e7-d-5kmz-zhmM6PjKOce681lPdvs0o2HbMHZeTl_7NPNSqzZrYvdbv118atyXuvno-LD969MM6OhR009f-o3lKcu7zgIMPa1ccVu6mTtgnj_rp7ebXbrtjdfPbTTk69uV-wNf503eraqDD17352PS_z7bn5niHR3O3Xu6Pyn3uyKopi78czV4N-f63HAzu1sHdfssW0_vXpoi8cee-Aa9R6rd9C6x84MPXbfS49I47733n13dQ-1v7349OPlxYefvl1-Wv2P5c2X__Pz8vLi7mJ1-4C5HV_YgO-jebXDvotH9m3s49zYtzEEX1uCe4UJ-07Kvgs3-y6aQ82gI6Nhk7Lko_ZeZ7cFEns3bPh949NcoUP-7HuvKV2uPCbWMWyPtndKNM9zsW_blzL2w_js29YIAg_CkRQxvafxYhu8i317vpur-dfZt-clTOzbcsmYzFj6krvsW3ZFUvYte4Qm-3Zf1Tn7Vj3OE_sWXRbGvvXNWoU3a49WbWXf-raNabd7KObY8fWifk189m36lefbu3_1bfH7r77981dvfvfne13k73777Z-fb27622-Lr978eXFwUHz9Tf3P__Xrr18fSB_s18Xdeu46sNuXjc1rR-1-5nE7mxfaSBibt9x8z_KM32uaXFdsvvkwNjbvMZTO2LznOXbYvK87wth8866e3m12a6ds_vHnbWze1vmdsXnbA-yweWuDh7H5vS5pdlyzx3pk83-4vrj6p8fUQY3K1z6Cx8Pj4fF5Qwd4PDweHo9Hw-Ph8TgSPB4eD4-Hx8Pj4fF6Ht8LKddr5QWPB7F3DQAQe4g9xB5iX7g7rtljPRD7P32-_fs9jv_q6u76D9cXV69XH-4eoL3t0xq3fwm3h9vD7XODE3B7uD3cHo-G28PtcSS4Pdwebi_jny6gGVpD5vGq1eU2SHR9eyiU391SrSi_qk5LwlsHlQrqvP9CnkpZOCh9b6qq3OQD8Ek82Q0k5sAbYSNIGrtQ1W4qnt_dfBaskUgHkQ4iHUQ6iHRQl-mgb1afrv9l9eb67s3ny8vfXV99f3F3cX31kA6yfbpNB73cpINekA4iHUQ6KDfmRTqIdBDpIDyadBDpIByJdBDpINJBUbdxjCqxo7os5vYNT_ZFdHGejdyurVtlX3RpOglez46yh8F2Ujd260q4Z4gciN-3yYGQAyEHQg6k-xzIl5cXny6ulnerb7dL2vvkx96ft1mPF5usR0nWg6wHWY_c0A5ZD7IeZD3waLIeZD1wJLIeZD3IekTdBPMQIsq-PcpSVyK6HpYrUd262bqiiztv5JCnUrZ157mSR56d8ARu2ZBABoMMBhkMMhhkMMhgTDeD8Xr14e63t7fX7y-Wd6s_XF9c3X6zjtIeMhnWj7cZjZKMBhkNMhp5YhsyGmQ0yGjg0WQ0BpTRaEIVhwc1EUoAPcbpJLG_BXSR_QjMfnja02S_MsY-aDOWvuRu9kN2RQ7ZjxDinhlqz6GoV7uSan1X19LW00rSdIKZtEfon3wyk72rek7b_UBpBnPxBepcRfqUPVklu8cmLGn2mFXyrCWaWSXtOqKflaXdT4SNFJZVstxctt2tqH-5s6ySXk8xk26R69wcVE8lHj3jGIeqdJ_uKKmeTUWasCZfSb6SfCX5SvKV5CvJVw4uu0G-knylJV953GxKFyPSpFlw-97d3p5SI6mZU1LTk-nBkyJkKmupRM93czVQVSpRl3bKJd-kTTTFSZMoU0u955TEyaS4zSNPH40pb5R8kva0YZp8lAGNeb7ZbQaqkz3Tfvu2XDFCM5e-a0tzlP2IeDMR6dEJpEf1DezfRddXniugiQVuECfP5f4N2Q67nvNcsjyuNYVuy3OJI7OO43n7ZO5riLA8V_Ounvx40wo6zXMpZGSnATKyfhaF6tVbnH6XpY8t38vKCrwZb7KiZEWN_UtWlKyoPyt67M6KHlo-3suiAH17JzPJR_S93BtZ0zFkTW3pHFy6d5e2d0otc2n7UsYO013mUphY6j2jJE4ltUySSJNH_WWN_OmiSE2g2Kk7BmQ-9PWHrW8iZ5wMgartK53nmDJfZ9hi_jZFfvG30G0EaVJf8k0KZGIVfHa4Zk4mlkys6pnIxJKJtX69qF_TW8lTT_pNO7z3sxJQT9lx0m-Wm4fVvO00_eZJGO8n4m3pNz9M6wrW2qMa68uGpd-Om13XzLI3e7bT9JtEQzvXaGg7DnblwWmc3vQkoptfyKNvre66yb3-6ebz1Sap-nr14e5315e3D1lXwwfkW8m3drkLdS8JQ_ald8DS3Rgf3tl0XjZ7iGuD7J4fZ5yLyxwq73lJi0lr0g25Z7G-1Gj_KUDtL3u3-vrCnuBNZ_a72IwzVvFtpoDep4C0pFugnCUhSUKyd38jIUlCMgczJyFJQlL1TCQkSUhav17UryEhSUKyj-qnnrSVx1p62_UnD2HipK2ad1VWtu00bbX385601WYroDFvtfNJLXE1J3FF4mowiavhRhxDB1ndJqJUXaFqWEUzRUjgkHXoOn3D0B02dBf2xWMpqhURL_tiXaAp8iaSIsD-XBbTTe8byeMCub2R5nHdG3VXX27Tl1zgSZbIsz3eT9HFu4cZXUKrgrUcBVrXHBtRlkhxfKvbvkdo5tJ3TWOOdjyuOCg2r0renixR4DmyPR7ja80S6Q-TTX1Qr__AxL6yRCHn9frdIE6WSHWOpfvrRf2avguI2rJE2uG9n5WAesqOkyWKeqhmr9vWpLvS-soSydZophAmTpaoZV3YLDauGeYV8FgIHrMpeb3VgQqb9wx3UTx0suTsoKh1ggrrkOX8HujF5XyNxpJ64Aij0-RLL0lDpnAY-9pO8vURMBnJayoNf4T2r3rhhHZqD1AVV_UJaxSP2SQ2gkv7wTb6B3tiN_JrWwCcwEaftWz0_j2ylbvEgTqCH5LVssgB70gmC_fJIE7j6-l4EOczCVcEcZCP6xcUVtI0jx6PDCnsxrFf3sZsUD3VuDE_jC-wigOEnMVsXAbQ7PmsBcTyujfzjXz4bFTy4WM0aH1Ctg4bv0WzJ23wPJTae3MEYt-YYl_Gh1YQnkECTXCUWio7qxbfmwx4OJNnTskbJs4btm5gRiTTIOAVKVpXM8Ol36NOww533G2_IbrRFFErkBgXuIapwLTQIiNt2QzQaCzpYDTCjFzHYkBzQ6aUcExz3o6t2bDsuRrAEtwyntrTExKDHYFgQfKaynFxhMOj6oWj7ilx_pJVUe-8Kp_dJc7HdG8xMV7aj2BB_2C2zSaua6PtOBE3-v62E12j9--RrdwljmBB8EMyfXoOggXJZOHelOI0vp52pjifSbgiiCNYcP2Cwkqa5tG_YCE7iOZfSAUtw4U6BpUCtLA5yXCXUz0qkhqtmUxcWliHEfE1I4guxO8a4hGjjDO0b53agO1LKO2leeqkzc8qF0vXr-8nAAl8Ools2niDJNppf0e4BdTCjsjEfdu7VZzwRPprerlsr4GKeN6Ry6vN9pmBxtr8YJoFR5zgRSygltpQ03h6LM3ssJr9-sw-S-kjJ2m2jv1KzS6LaFmu2WYFgp4fie76rDgsj17M0F2j6stYZR3UyCNoXhugQVntcP7Gp4wAo9FRT3cYcCz20E4PZsCyr3ija6cZlCI15-THHH-eyBhTDd9xpyK-ncCAEltzOJ1xpPURuLKJfgCTfldqzUmoDvp1pHTv7DIR7f4ilBaplRZZjS8hb2o3t8abplh6eDQlYpsegaZE_K4hvj9KTYn2rbsSReU6ISVTOQm2z0S0Q4VVCUYdu2JBa1557j8wP6t8E0L9-n6EQIFPJ9mOYLxBkj0J_o5wb0wQdkQmY257t4ojBJL-ml6GnrcQqP8Y6SBIqdhGSj7WYCfw1TuR0DZ-Uy34K7x2mpmYtvHAYYraus1nKas1P6JWW1u_S3KBraNz5CpbX-fk5Ov2abZ1_6QS3RaFecJVXN24RWezrvelbPLbcGPrG_4YDcyO6H1GpZff6g1JaTzIb5HfDll--2KG_BYxWGaS26k2rC2ERWzrcPXGp_j7QAW2U21d50IOae1ghif7ajaitHaqThJFTjvVxrM5NULaqQlpB-oBvYpnp9NmTqtENpuB8x0MRyqb9du6DEI7Ow5gpkTWmZWPJlPS5eeg2lcV24NgOjIPYmMXDeYpzOnFQuuPKu2WNpsk0BB1PA3qX0RmBo0XST3LecRQQXY3ODFU27Eh-GmDuidMA737_wcl3Qp4RGXDGmdFufy6fpeutGL-n3OYcATLm4nlQb5uzWnYtS-QQm7TUpem-kmtnMglMEGXhi4NXRq6tDHp0r5ZL5GNwrSdT1CmoaIYrCZtRG1qoyHI0Rz-3fgUJx-tEG1EDetctKFBG8yYZF-5RtGgTX7c6aLxRtRuNk9GeDYd4dlAx4xBSM4G3VxOU0Rt1qvHHYxPZ4bEDImZ-3eSS8w6ccxhicvQlQ1fV9bWrA_yU5SNS0w2RWnYtIVe3eimelF5pfTXxqM2JV6qm7SfHmLpp0Lu0qugKY6ULqdRL8ZDRtNQtdbTJa7tJfhJ9yCAZgvNll-hg2ZLZjVotqap2fry8uLTxdXybvWnx_rsG8XW_t939Vrzs1HptfZWrogNkBV1Lyva-xRbRPiC8EUifLF9ZeQJWbQbk9VuOL83crNHhxCsQ5B8PascIJn5CWbmxddkZarJTvOTn532ny0su-NxTp8E7iRt12JDct_pL-05VkNJVwc-VktzCfrNvrO8KQ2n8ag9l8Zo94PJ6k4486at27mXvGmMlk6SkvTlB4OO5ws53K9xi2xP-As7iCyOtwb-lvYwsdY-2qZN5eeHxfLMVq2qOAMsij9qz3cMOfaLLD1ZeqMNea5oGg9ZerL0-WTpQyqrzM9GmKnPprJKPjCcoir2NkXykE_ydgCVVEbl1ONuWAQklpAMAUn-lVPGOM4Ms91Q26C2sd6710op-YwREy2SgiJp5JVRKIoS8KKx9VZiDUxWRh4C1cXanazeNG-dWL8VXCje0sHo0XjFQQjbtE-EsC1OHZZ2JR3i6BwCf0tbyKG1zqFNm8rPo4mlc2jVqipLj6BzkP5Um5IEQ9I5aOtSdaZz0Gp9gh6slZxCK-kNlFOolTmmn3FO-Eg2xLaFZGMMkg1tYYWtXGM-KrnG3tKRBCTSAqopDNIAHXMcGfDBuIp0oh93uoZM7mQzuc7vjdHWA1KTzmy3ezNyEKfN1B9kKbtk8Ha89RusaQBXLnh0SUTFc6feyjya1Foka_MPZuFMs9898h1vj2-xD7jvpJP2KcP2w8dJOgX-lnYXfOukU5s2le99j5V0atWqKkuPkHRSZwoC9rn3kHQSj9G9JJ1Ch8F4uR1P0sX7G1pzaNpBp0kXyXLcMFe7Lut8mlY8TMsZWvFLonoAmczLigeU17uIOhvrf0ZS5SLWHBzYfu7aFpFn3tAWlFpwhPlW8CuKOhY9z7KSgbU5wQaNqtHoi31a1XRMwIzqur2-cAniBcQLGYoXgupNzEcoYBhxvQmKTOTSkAg_qCmRsePSmi2817G4QjUzmHHGvsIceN0IikVkN1Q0vFOaexy-v4XEg04NynCbIg-p1BiKXlDpIqtxrdFA41CJSR4ZlVg3pSYCLavzNKDiYRKajj2Jorgqtz3h3jSg4NIYaUD9z0g2gcdKAwa2n3vrd-Q0YGgLSi04QhpQ8CuKfa09pwFdD2ZLAyrsKPr26dApMEIa0HV7_b7-AaQBJWW6OksDSvLVQQ-jzjBKFOuBGUZRptl0a-cSfeSZS_W26_usZTmqrOXe6gxcT7KNXdYZ2ptj0h1iemgv9bq_WVAcoGeKe5TeY0t5RA3q2c-tdLxm1m56yRbPi6XYHEaGoa2h-odQXZSWgdHKHjnZXkXlxpwx0HPJ2yktq3N6rniYhKZjZ4-Kq3Lb3Oql54JLY9Bz_c9IdrPGoueB7efewxqZnoe2oNSCI9Bzwa8oNij2TM8lA6t8l2oseq4ewLxTYAR6LqKm4g3K_dBz59KsM3quWty0AtYeqmy8r6Qvm53YKVW2hefeXcS5r6o9Txt1a7FrIPF8c7jLaM-LyQ2o88Wz7DnSWIh9wSG7IIct59KFsvuqGGtk1S_Y9phHXhnr22p_Z3ma9XBAawksM8Iq2P0Dnk3Feax9PaOhe-945BWvZuhxzVER1rmWO8u6tNmXPZZasS9sxauiVlkT-3LW18IBK9nmLWW7-seliiiLw_Lo9OVGFXH8lMQzJ8WRSkwxMeGWSuxlgqcgldh7afYlR0-E2aQSto2YeqmE7U5PUglb-riVVMKbc8mUNbTCVYnyNGzVbSmIUBhlzrbpfdrIVicF6iPiYJ4XkxtQ5xxM9hxpLMROG2QX5JA1lnIw91UxOJjqF2xp4sgcTN9W-8nhNBwsoLUElhmBg7l_wJNLyoODeUZDd_o3MgfTDD2uOSoCB7PcWdalzb7sMcsrTfDG4mCy9YXDd-JldKWJ-X442HHz55sc7NAROh7GiBoNN3kKGA_bxYqHvsRi3qtwNfyNvQiz9s0WIRMSWio2-hfzh0MJBO0PGs-4XCmmvlfyKRQQiqBv_8udx3veR4huCPa1tPe7fQkdlLGd9YIYYZ305rvKhjTBnKpdZkHtEseUAywuQuBmvbchdZpVuGYfwvYVC2mCNOGgYZlEIoRm-zf1dlqvAZlhWbKvSYgchnmneLMTtBMhCPqj15Dr0BFtPTt69uW__nh5fbP6_cWHD8_On93LEL5ZXd98v7r5w_XF1WNB-frf7uUHpy935AeWnfiIDqYXktic_n6rfIQg3j6FHtQ-DCud7Nn8nncEpa4aEXt57R59-42ssvYYf3DWX1SmLgWWKHYTrb_6j9ZS1NJUBPEpwjx1ScC0QbwmpPJ-dwBB_PSCVe9z7JcLTcg4pDffLQ2ahnEca83GekEcs5Hevpah7sFsDJXzsmIcsg49dRl6S8Yh7ELLHBuBcQR0Wq-Mw9NXc2evBte9FDAOX7doC1EK-qNXxmF4tMbOin9eXa1ulnerP65u_raqgQ3TJzW8cQbeAG-AN_KILcEbQ_UY8AZ4w6FOV-ANmaDh03o-zwOGeEO21IqG9R2LTf7m4upv58XhQnDN5td2Lyrno8Au3sfKnCuogucY2olWvSZnRHXzVPXSLKiX4vDMfiiH9d4G8W9WlENlZbHkLXZLK-dyVcxjRCkzSkgNpAZSE05qXl9ff_z8oxHV1D6qsZoZrAZWA6vJI1CG1QzVY2A1sJouWc3lZkJ_CufANg-G9PSabxfvilfF29KsnG4Cn_sG3V5cLG9W65vBbyIbFLqQwehCxkdMwBPgCfAEQhLgxJBDLeAEcAI4AZwATuQKJxCS2IUkzvypVUkiye-PDERMT6KQYaZf1WMZoqNU0p_xsZFWA0SoZglSA6mB1GRHahCSwGpgNbAaWA2sBlYzblZTF5JUCElMQpJyvlGSLBCSDJ3f-C5JIpiwwRK9nkR1dAiwBCEJeAI8MRY8IS6yekaRVYAEQCKnaBAgMVSPAUgAJFoCiQc9iDdAygAsGAp_eK8xFv7wXiVRFfQb7489oA1NdksDZV-y29aMKY4UEh_I4Sy0G69UigpcxsBFsY8v6eZMJffxKOGKoICB3xhBjwIKqsaJWFDQPlY4Zw9riCxGT4pZWHkqq9yi_a8X5cAfx71j6edsrawvPR5neAoYRVI2feg5WX0xQNtjNw221-f1HVvVQwFu2GmjD2CnsNORkSDYKewUdgo7TWZc9rWrJyaDncJOe7cQ2Gkf7BR-2CLyhx_CD-GH8MN2o0jKph8EP5xyZk_QZbBT2CnsFHY6ZRIEO4Wdwk5hp8mMy75u98SjsNNxsVNZMOI8HT8VQRkOvZWFSa0ZQByIqbDZkSNMjZ1ox59FM54cQ2n9vCj_BAryg4LHh4JjlMkDAyfBwI80r0EWFyaw2KR5Rq5omAZQkEJBoaBQUCgoFDRPBAUFHarH-EFB1BgSCgoFHQUFHYGCFFwILrRe0L53wYWC1gQXWiNXcCG4sM1InLLZc8eF_edohZPpZpBKkgwVdBrEFGI6IWLKYbcQU4hpJrgKYjpUj_GzEsn8mIEJQkzjE1PZwtsZSaUKoTSxU9wC_S5orIgkU0Fj7yMYoLH3GiM0Fsa1ozmiJUPCKL2i_TZFLwyTmFH-e9NVDdo6hvc2qtPNIIwcqwKm6OvU1z-up3zjoa-7n-wii3kJsgBZgCzyiBdBFkP0mGMRsjgMjdaytFTXii66SMQWJbr3Su7Fh5UxnJkINrG_E8xg-MzAi6R2Xvn54udyflC8Xb6_-7y8XDec5DBLoAPQYWjQQdb-uZzlmgQ67N90WBoNs73FFhk1Fgn73xdYGIxkGIzk9eZwdyMkqX1UE3bMoSRQEihJHrEnlGSIHhOFklxuRuinlRvAxAlMPq5-emqFt-X8XfGqeFuZq9g-htX3Tby9rFjerNa3gZwY3glyMnxy4n0swAfgY2jgA7XFCMAHzGE0zEGly3jJVhKIA8Qhp-AN4jBUj_ETh_4iNbaSZBvZi4O-NNGeIswLrUsgKzxkLU0TCYF0SdviOo03USkTK_koi1mzpHHT1mEr5wQqqr0kOWAuAnmQ1vDQAj6xkCIOVtD1mJsuJIEMUtaQJ3Jw2Fuicjpe5YUchnhhgaXNKfsBt4mjFbknNzPIDeQGcpNH2Ay5GarHQG4gN5CbbsiNSFY1RYijEVA1-Y9YRwXNiWBZSbQebWiOqrSxU4oRB53oShuDTsLQCZwCToG-BEoxzJgLSgGlgFJAKaAUUIr8KAX6ksKhL3GmT2Ps4hkZkZieWiHDfL-qx_JkSCkVQSOFJK2HjGA9E9wGbpMRt0FfArmB3EBuIDeQG8gN5Gas5KauL5mhLzHrS6pqoy8p5-hLxkFzfJckEVDY0Mnwjs4GncAp4BQZcAr0JVAKKAWUAkoBpYBSDIxSONOmqfKlGrF93GV-HzIThX8Ni08YKsXmUsNE47FaL1s0o7IxVIpVRdsKXqhK80e8b4QKtHlSo0Ftu3mkBg0qsjBBkSY1MDIRg_NRrARIA6RBTAKmAdOAacA02XkMmAZMA6bpFNO0q1kyWmJTV5QssixYArpRohvfFfGkXh5YQo2SYcMSyARkAvkIXGKYURZcAi4Bl4BLwCXgEtlyCeQjdvlIWI0SSVZ_ZAxiesKEDPP7qh7LkBqlEvyMD4u0GiBClUpAGiBNRpAG-QiYBkwDpgHTgGnANGCakWOaunykQj5iko9sC5IskI8MHd34LumsEkkVpCJRcBKdhgROgnwEMpE3mVDJR86Kw_JoXsIl4BJwiTyCQrjEED3mWBts6ZLhWVqqK9KMHnnZkuTG1JA1OW4-wAM0kluZVn8wECvpGD2LKS20EEkN0yWb9Lii6HScjuqd9FSF1WtmOy_-vJz_XFUHxdvl-7vPy8tNA0p8L_9DXqSXhMBN370tR5moWnWYUKqN5iegVZ1-aw1V9XSnL_Sx78oCngMkAZIASaYR8tn8HUgCJAGSAEkygiTmw_x0WwgmD0kI1AnUCdQJ1CcRqMvaf5yseNCQotVErMbaQJIRQxLlHpcNJjmdg0nAJGCSPIJPMMkQPSYKJhEp9rM02l6ISV3JX84dSn69hB90AjoZDzrxPhzcA-4xVO5hn3rE23LS7MeBeyDOmAh3QJwBdYA6QB2gDoOjDogzEGf0voMll-IesgW6MryUZfxi3VQcrEYiPakqhXgfwbCtzHvNWI_uVWljFg1pzEKrjMmwFKj0Cm2A7bvvaM_oVTXoYMgQdAQ6Ah2Bjgwv1rP5O3QEOgIdgY5kREeo79G__oKonKicqJyoPMOoXNb-oybCgwYTJlNLDK-hIyOmI-xZgY_AR-Aj8JEB8pH6npUZe1Y0e1YWmy0rm8oE7FmBmcBMtFtVQB4gjyEgD9kWlZlui4oKeQg3qYA80GKMmzagxYA1wBpgDbCGwbEGtBh6LcZ6uYoYI-ZWFWeUlCo8MvSAbJWuDIhk2b5YNxWHV90Dnrj55CiAR1RyQlIxo2tP3Q2v1KKYco4oJhtCRGkYQaM6_TThVpUtFdk_PfdhrqwH3HUgYjs_11hQCEYyTkbyzWpjxjU6Uv_bPReZlxsNxhlcBC4CF8kj2oSLDNFjPFzkIcaxg4SsbND1oKYSGvZvG7Pb9q87w-LhgwkDF2vGOHbGExTVOBUs2zjGsQQMr2Avo4MxQo2Uo6v9UVMqix7GWvuXOqlBqh4Tqwe_2VmyNtzofsTWoqEU9R5j590NaMh7jRENRcnw94uGvI_lQ0PeG4grgsYYRKU3F9QHbYmGfPeVnhTS-bgreAhZbS7J-sJ71c4yI8G2GMo95830vF3ezBJZL7C2ahzPCLBaU6vGyvLYWzWhlu5Up6VrGeRowpEITe1N_XkDJ0Hj2mCtJBZVLZa9YZ1HPhgAa-0r5ObbKubAlhGCZj2fpAn2sHXzK0X9e93Ta9uDHmfxhIe2p-hKt-p0bAA_gB_AD-AH8AP4Afz5A34AOAAcAC4dPQDgAHAAOAAcAA4AD7JaU6sCwAHgAHC5DQHAO8HLIxWBuToCwA_gB_AD-AH8AP7MPAbArwAMAH47gohM9_3Lt-aDGUfe4LreLfMYaWoxjimLkbIyY1gGY1FLYDz4zWLQ6QtF-JtT-VAyKLbH8mVQciHnFF4V3TzXshqqRJyz8GecxYAuFef2cFODtq4G6m1UMhBkIMhAkIGI9tJkIJDgQ-gh9MPijbaREkIPoe8vCM_ZYyD0CpABoQddg65B16Br0DXoGnTta03QtaBRQdega9A16DraS4Ou44rnx6K_cvUHhB5CD6GH0EPoIfQZekyjWSD0EPoGobe_U6YAtrvSJ5HTEpGWuYbXUx5FGbiE7pfjysJD837d9r0nDg5FZ_G1OMnByxac5uCPzuIEaVaANKG8AUWDYhYNAp-nwOdeaNmM5RPUJ9INb-b5S_C4LeUHE9UUdKMTaAAtlypgUGdGk0_IKp9gvGnbhBpJisyTFK4HzZzed3r2sHPIAOGD8EH4IHwQPiJ7ED4IfygIHwEzIBIQOW4QCdmD7I2Z7AGyAFkWta1pfYdqIaPFG6oF8cKqf9WCsLEshhZh8aRNhMqXSxPJ-bjGclg_rB_WD-uH9cP6c_QYWL8wGGjExDZ-EJn9uNDFyPMLnt2U4eShJblLZebjgXwpzU073j0a5Fl99KuZrr4k0P63O6ce3kcwwA_vNUYG4r1KctTlwNNZ3veiLI2CuqhakzNdg8GMnM94ljUUUEHwTAGVdCmdx4oZDRi4MLHAZsUMIwpcNEngAlkyqBJUmQt4sY2UoEpQZa8YJ1uPAVUKY_ZJy5IBfYA-QJ_3GkAfoA_Q18rQTQ0K6AP0AfrkNgToSwT6-pAqeJeUZn1um7S-qyvgmfBMeCY8E54Jz8zVYwQEYqQ8s7_T95QbxuzLTSN4DYxwhg9eqQdh5WrUg6AeBPUgpIZuatD860G09dIohyey8VfsQTLXyaUkhMxLwuGJt8XMS5uckGi8xO8As7leI-qSB-tH7OB87y5v85RiGNR-_jYHxwHqY4D6JzzbgMQP8b8Pz5ogsdFohy3L5aw_CPZ4eBwEG4INwZZ6zKSX3DEElCySWSSzSNYvksmXkS8jX9apcZMvI1_mvzn5MvJl5MvIlwmbinzZpLYQuEINMDeYG8wN5gZzg7kz9JgxgafoQD5SQVsT9rJ_G5oETRLRpNg5GGU3Gbfk5ZCDiQIfoGwRcQCULa6FQ9niPXz7U4LgRYPmRb0U1spD6uE1oc7V1SpSrgx_DAUlxkHKf4lGytGK5Fv5xPWMQGYgM5AZyAxkBjJn5jETXl53XYqWBTEL4mkuiElfkb4ifUX6ivRVT55D-or0Fekr44KJ9BXpK-TOkOhWJHpeQqIh0ZDoPGJ1SPQQPcZzdIQZ60XneSKQF2l2drGGqDHSuMFU_-F1fDBlfydOi1PEpbIApXMA430sTosLAzC--3JaXManxcnaH_YB-whjH0UW7MPZSRv08M-rq9XN8m71x_XsXgMQpk9qGGIGhgBDjAZDHPtFScJVcZaxU_hoGl8AZVulV5Xxa_vB2hnUI-Um76grdnDA0HCAnVM1I8Wfq8ofLMISYAmwBFgCLAGWEMASPK06d845gSzB247xWcKxgyVkpqOwGEwsbXljoX8oMxEZ5Xh9ff3x849GzFH7qLbx7yWcA84xdc5xuXGPp1EX5BGEPD6ufnpqpLdV9a54Vbytzt6Z-ch9m2-_XyxvVuvrYR-wD9gHUgjwBfgCfAG-AF_kjC8mIoUYFL5ozxE0aonTl5QPgiKMjCLYVsZs2mDTRpchvH-9HXmFLVlTh5acMfyM27diLHs78ptk7iPyotar8RDz11p79eCBtVNBa4zj3helS3ZnyJQqVtIESW0Ot5VVU_GWFGuJdTrB0UlcyFuBwKPOE5KjSm4rFstuHd4F-2IjiFACC2fAnDhutofP0gIXIVG01ToiROi-e4sDdU8wru-xljNiQKCeJGx34gzrN3MJ6QX21laf4Lc315nvcvhm30piafOnA8q3rb97QHklOKC8sh9QXi-J5ak8bX3CfCpthAlBtgiHDS8gHBAOCAeEA8IB4YBwQDjZIRy3THHCNEckPLSAIL8MEawTz7KCsI7vkjZYR1Uy1ixoiMpQdCVjYShhDAVgkRWwQHECrgBXgCvAFeAKz_oFXAGuAFcMAlegOClcihNXQlW5f20KaGJ6-oUMFQCqHssTJqXUCI2UlrQeMoIVTgAc0xPmB3BQnIBwQDggHBAOCAeEA8IB4dinmiEhnLri5BTFiUVxcnavOKlQnIwD6_guSSKpsDEUveJExVCUihMYCoqT4QMLFCfgCnAFuAJcAa7wrF_AFeCKnnBF9mUJ4y70-1CcqBxtWIzCVCo5lxonat9V-uLW22qOqEUTeRZlVcXeSoKoUgBEvre24OtwYNKg9uc8wYQGLXmY13wwwURLjN5IjRMADooTEE7fASkIB4QDwgHhgHBAOCCcvhFOuxonI6Y5dcVJOc-yxglYJ9VZO3GFYR6GQo2TETAUgEVWwALFCbgCXAGuAFeAKzzrF3AFuAJcMQhcgeKkcChOwmqcKM5jHhmamJ5-IUMFgKrH8oRJKTVCI6UlrYeMYIUTAMf0hPkBHBQnIBwQDggHhAPCAeGAcEA49o4YEsKpK05mKE7MipNtjZNyjuJkHFjHd0lnNU5mQYoTFUPRKU5gKChORgAsVIqTs-KwPJqX4ApwBbgCXAGuAFeYccWxNvTSpc2zpRr2yDN6HGZLp5uzRdY8uvmsEJCJMoubKn2riTmSFHVIkusUl3GIpKbpEmN63FN0Nk9H9VV6Kv3qtbWdF39eVT9XZwfF2-X7u8_Ly00DSvww_yNmpJeEAFDfvS0HqahadZi8qo1UKKRVXX5rDdD1xKdXErLvzwLGAzOBmUw9AoSZwExgJjATmEnPzMR8iqBy7wHMhFidWJ1YnVh9QrG6rP1HjY6Hjytaz8161D05ZqLcGLOhJqdzqAnUBGoCNYGaQE0SUhORuh-AYlX9Vy7Vv17uD0mBpIyOpHgfDgwCBhkqBrFPQeI9PKk274BBUG2g2oA_wB88MZLz-eEP8Af4A6oNVBsjZQ05FgeRrdOVgaYs_RfrpuKwNRLzSVdpxPsQpo1o3ovGfZKwSj1TzhvqmdLJK9Q6gzhxtvT2ilNr2uTdffce74HBqlYdDDaCmcBMYCajiwBhJjATmAnMBGbSMzOhOkg--gxidWJ1YnVi9WHE6rL2HzU6Hj6uMNpbWtQ9OWbCTheoCdQEagI1gZpkR03qO11O2emi2-lSzjc7XTZ1DdjpAkmBpLQ99gQMAgYZEgaR7XQ51e10UWEQ4U4XMAiqDVQb8Af4A_wB_gB_yIM_oNroYKeLeWELa-ieNcTNb0ZiDZKyCZKqD53ainFxH6DaqFBt5IcrKHciwRUuv22n2hC5QWxOZJiPhLii7THDalyxhRT7J-c-rD7rQX6dTthOzjXWCZKrNnSNoFJtnE2QmaDagJpATaAmUBOoSXbUpK7amKPaCKpPWp2h2oCkQFLMJMX7cGAQMMhQMYhMtTHXqTZaToJgECUGGTmBQLUBf4A_wB_gD_AHz9SPaiNH6LCn2jDmUZTRJKxhYunKLNJ_9l-H-YyI-aCeARuBjZLWJ4WZZMNMXm40G2cwE5gJzARmAjNJxEzMq7zo6zrRSq7NiW6e6GffxWJ4WpeHdsb3oWBoofGoboDFosYrFltcsRjuaSr-1W-UveAyyhiXKij8K27SNjpL9DCLheCajoqLqDxW6YGLZnhjZuIOpzNz-2h2pI2IFy5aaHJDBeG3GkSrXvPf1xBkL5S9lGeZElVHtVZhSMGCLyZv4IWFiS40Y3IjXDA4n2tXihHnRKLTDpxjzK3IoYeXfeSAc0QPajWIbCCNcmvNPaaZgWnANGAaMA2YBkwDpgHTgGnywjSiPWnTIzb1nWcL18azJusR7z8D3XSMbnxXRBBnymCJD02k2qQCLIkDSyATWZEJ5CNwCbgEXAIuAZeAS8Al4BLD5xLIR7Q70hoX7elHJFn9kTGI6QkTMszvq3osQ2qUSvAzPizSaoAIVSoBaZy2kA2kQT4CpgHTgGnANGAaMA2YBkwzBkxTl49UyEdM8pHqbKMfWSAfGTq68V2SRCZh4yR6FYmCk-g0JHAS5CODJRNUbIVLwCXgEnAJuER0LtF54dYs8EUGZVuNqSFrcjxW1dYRoxFnajZNTlYRZEQvuxA9iykttBBJDdMlm_S4oqjYakf1TrpxukaspCzRWlU_V2e7JVorke9RTFSCH7R2OXwo1UbzE9KqLr9NWKL1ESxsGcgO1qn8WKeyYp2dassWrEN1VvgIfAQ-Ah-Bj8BH4COD4CPrxa8CkDhD1cnyEWJ0YnRidGL0ScTosvYfJyYeNJ9oNRGrifbk-IhyZ8uGkJzOISQQEggJhARCAiFJT0hEOn1giUW_Xzn0-3rhPtQEajIeauJ9OJAHyGOoyMM-9Yg346TZhQPyQJKBJAPgAHAAOAAcAA7ZAQckGcklGeYV7eTpQo7lPGSLc2VoKUv0xbqpOFCNRHlS1QbxPoJhI5n3mrEe1quSxCwaipiFVhCTYfFP6RXa4Np339Geyqtq0MFQITUZ6bROB3gEPAIeAY-AR8Aj4JF9nwKPDAOPGI-9sIdglPQgLCcsJywnLJ9GWC5r_1Ej4eGTCaO9pUXYk8MjbFgBkABIACQAEgBJroCkvmHllA0rqg0r9wdObEoSsGMFaAI00W5UgXnAPIbAPGQbVE51G1RUzEO4RQXmgRoDNQawAdgAbAA2ABuygg2oMdisMnawEDeRGQcsyIv6yUpNJLcQw_ZzvRqjQo2RDZmgIImETLisop0aw0mGUiEhw6wjJBNtT_QdGpkw2puuEVRqDKPCc-R4BDUGgARAAiABkABIcgUkdTXGHDVGQPnQ6gw1BtAEaIIaA-YxUuYhU2PMdWqMlpMdzAM1BmoMYAOwAdgAbAA25A8bUGNQG6P_0qGjzkxmkemz_zqAZ_iAB1UMhAhCROlQ8Ah4ZILBHngEPAIeiYRHzOuR6CsQ0Zoj0lbV7PCIyiGHhkeMx9JbI7By3j0d0Tq11uMeff6s7n-1AUJOR5zxVppAaz_Cin7koQyLxLihNEiLRETSObf3IZquaHYuLxRJUoOkG6fbdbkNc1ehkXLeqN9aisikP7sQ0Wa04bwl8xCrp4LCeZFdUtREC0l6Ol-lK7CQmI9sn37_BN37hXPzReoPbjtDdzsq7fDacg4kAZIASYAkQJK8PQZIAiRJBUnWK2AFJXHGq5OFJATqBOoE6gTqkwjUZe0_TlY8aEjRaiJWY-1pQhLqeoBJwCRgEjAJmCRXTFKv61ElqOsxKmJSr-tRzjd1PRaDKOsBOgGddIlOvA8H94B7DJV7yOp8VLo6HwruIazyAfeYIndAnAF1gDpAHaAOUIc8qQPiDMQZve9gccacqYJNQ5QpW6Arw0tZxi_WTcXBaiTSw3G67Tw1ynG62voeZ9T3kAXYvvtyHu-gyBB0BDoCHRldrAcdgY5AR6Aj0JEc6Aj1PfrXXxCVE5UTlROVZxiVy9p_1ER40GDCZGqJ4fU06Qh7VuAj8BH4CHwEPpIrH6nvWZmxZ0WzZ2Wx2bKyqUzAnhWYCcxEu1UF5AHyGALykG1Rmem2qKiQh3CTCsgDLQZaDFgDrAHWAGuANWTFGtBipNZiuA8ChSsk5wpx05hRuIKo0oGkUEPXw_7uql6txSjnaDGyARNUJBE0qtNP22kxRn36dkww0TgDdmE6ArYJJownwBrQqFiLkfig8GnSEbQY8BH4CHwEPgIfyZWP1LUYc7QYei1GdYYWA2YCM0GLAfIYKfKQaTHmOi1Gy7kO5CFHHlOgDWgxYA2wBlgDrAHWkCdrQIuh12KYsyfWIJKqoZJFuiI6GlZeMotkn_3X4TvD5ztoYgBEAKKUVUOhIz3TkXm5oSMz6Ah0ZDR05NgfugmXaWOgJ4Z4Nl5IJ9PVSxeKsJhILMa8loy8etxfLzqWBq0PbQ9da9rXANbVZVugk4njy4M8d43R_W0wAnOQoBc1TtF6ndbJqifGsnNA6P1a7nFhdr-wsBvoblBYzn-uqkbhytKMZdRBTKroJTg-jxWvdH1CS5ce6HFEs2d5_TFJaZNuvG3X1Sww3-Ffe85VOSmKPG0R0Wa0oMDdCq17KggUiOySWinaVo1THlYAauyExopmuisi4npTiAfEA-IB8cifeDTqMAvT1hCPFsSD-Jz4nPic-Jz4nPic-FzVqsTn_cfnni4afA5nKPyh1WJbnmCKSjx09TbumcfpS5gHzGPqzKNeBaB6B_4Iwh_16gDlfFMeYPHOjEr8ZQHgIHAQOEgCDiILBwAZgAxABiAjZk8BMqYCMgwLzibIsG8vDQIZ9ttNGWQgpAAq5BGgTA0qIKRASDEk_1SHgtFjQEPw55mwW5-h3iaGFEF-y26mDuqC5ABnnPU4krEZ1QGcnQSazfId91RmoRenLPa1KeaiAvLsYYy1tzAOEhXIjFY4QFoxAJIDyYHkQHIgOT6SA-mAdEA6IB2QDopkDNg_icWJxYnFicWJxYnFicWJxYcTi3u6aCx5mqEgB5ONpUoisVUE1pFhLDU11lHfKjJjq0iMrSL3B0luNtmzVQT-Af_omX_IAgIQBggDhAHCAGGAMOTf9GwMmQk3hqgQhm9ryIQRBqoJSEIeUcnUSAKqie73hzRWDGCDvLFBpDxFOxLg3v7vLFTQtQUdeI4qNKsYJGcUEv-r439qaYhraSjSnaGjTg4E0s0UhQBFjyB7Qiay4UfKStSQJBUd0WCRJDxEDELUBCQV-giHfZFgh5hyRN0D0elRoIT0hPTZRQyE9IT0oSH9ek5jJ8QwQnoW1yyuWVyzuA5bXEcciGCLsEXYorOpYIuE9IT0hPSE9IT0hPTjc1B1SBc9ljMEcR5tnTJ68-zAa303fxCYZ5ySydYJSVATe-uE-jgOpac9-vpZ3e9qA4OmdEQ5p3RElvsuuvQ3j9uZHcnnfmn2XXTjYPXDa7T7LvZ8ynJsb2gwnTimDgitU0bY2kDb0arsu5AsMTIs4wjvgHdMO5yCd8A72JWQsYMSkRORE5ETkRORE5ETkRORDyci93TRWLI1gwIPRkNLlE-ioiPEI8OAamrEo17R8ZSKjjEqOpbzTUnHjd6cko5QEChIzxREFhYAMgAZgAxABiADkCH_pqek46mwpKMKZPhKOk4dZKCggCfkEZpMjSegoOA0zCH5Z_foIFLGooMDHszRY7AFaft70ZzmNVuvqwoxQ-Kt19TIcDpTBwUfcqCQbq6YrPJMT9hENvxIeYkalKQiJBo0koSJiGGImoKkwh_BwC-byjMc0kAwTzBPME8wn9N2iAXBfN_BPMtqltUsq1lWD6mgI1QRquhodajiyKkiwTzBPME8wTzBPMH8OPyzj2A-0SpaG8xHWkarg3lltxuFeJpgviKY7yqYZ1ndd7JeMbVmsWEoFlJUBfPKeWPRpImqYJ56J6mDeRAimXmCeYJ5gnmC-WyDeWT2gwnmWUOzhmYNzRpasIaOOOqAEEGIIEQQIpl5gnmCeYJ5gvl8g_n1IpFTB_o7ZdG9AosWphj6y1fuRheY-Kritr2bP77pkJVMOMDpxpvrrtzOj5WHOigUyy0L_KUxOMEjiBidz6BEtdfUgVsH5rW7ohc8VtNC_ObhGO2dGK-9ochvLoB6rbrHf18p4gtlEonRRIChpwQVWl4hiGEBHDkBjm09wFcPK8FHwvGw0q0jjnohwF8Xv6rODIzjfpLczWVUVTTK8c1q00I1vlH_25ZszCAbkI39wOnYQTYMobKebBhu8kQ2TE6qIBuKZWKWkUh4wGkFDWaA4EsBAxrylg0QCg8rFJYHOoTChMKEwoTChMKEwqlCYU-r9pcc8TVgW9WWP51ha7JDR1hkSOTpwyLzaV3bsMhEBpRhke3Fnv3f_-__BQAA__8_L8GV +https://raduberinde.github.io/optsteps.html#eJzsXV1z20Z3vu-v2OpKKgFaAEiJ4juaceo4U3lU2bX89ib2ZBCJrGFTpEtSadxOM5n8Bl_m1-WXvINvENiPs4sFsIBObmIB2OXifOyefc5zFv93dPsf10fzo9uX1y9fvCP_Qn54-_rfiU985_366ubm5Vvy6vXVTXjBJa9viO-MP5PL6H_l-150343ve9X7k-i-F9-fVO9Po_uT-P50_PnIOrrZbB--D5bLo_mRbdvEf3a1DvaBv3q_Ho1G5Of8z-fPie2QkWNNXfL8-fv16Mt282lxt3-_HpG_vv3517ff__r2O7nbrB4f1rs5-Tx3_nn9uFqRYO6S5dwju_mEfJpPyef5LL1xQZZz55Ts5o5DPs0dN2w0TW86Z-Hd8_DuLLx7QT7PXTfr0yPLuTshu7k7JZ_m7ll49yK9652GP-mQ3dxzyae55x0O8vPi65wcO5Z7cXJ4Y3kfXj-x7fePp6fe4ti1pycWOfasyclvvyXXHMu1oquz_LkL23HDS86p5Tj5ozPrwnJcy5nazoXlurZ7ZrkXtudFj07z5s6Z7VxEF88tZ1Zob0etLefMqnYwO7m8PHamluuG7xH3eHl5PMsvuG7hTTzbPYsuTix3WvmJeICW61nFn3DduMfwV-IeL_IevdPkKc-xPJfRYzxiyzu1kh4v8h7dRPjfEuEH6_Via3_aBGty_NHffYxuR_9RzCv0jtTCfGccGpnvjEM7851xaGq-Mw6tzXfGd9v7n38K1vvFdu2vfnr45e7up33wsNjt_Ycv87Pwib3_82qxCe7n55FzpQbqu-PQRn13HJmp744jS_XdcWSsvsvv2vHCR7K-nUnkmZmF-944MnLfG0d27nvjyNR9bxxZu-_xu3dPw0ey7l0ncuzMRfzJOPISfzKOHMWfjCNf8SfjyF38iaD78_CRvPtZNGFkPuZPx5Gb-dNx5Gn-dBw5mz8dh_4W_p_bvTcJH8m696YUZT88rvbBl1VwF-y_zslqsdzb283_7I4Xv_p3-9VXe7NenFhkG_zXx-TG_y62G3uztR822wXNekp-X7pbdf5zuvPH1w_df8J0__Teob-7DsXfU1dP7sp7-Izi4albp3dFPj2l-HTqwsndqheXRMlz5T_Qow3xaLZStHneHwwHdDkPGeyHh05Y2wM5QgB4EDoS2JGEwtNt8Ay7p6q8J-ZfsH3xSxTN92672e3YTYZkxtCXTO2BI8n2rAI6hN2dvyY--dHfXfrOB1GrltUqOZxEAcJ3T_73TUELKmJ11cXaoVVTZSs2bZpsddpt1nGw2i-2O_7j5LBNbKUJCjN3yI-bx_1ie3kcToh3m_Vuv_WD9X53efzMmZPjZzd_v74mNvlwwtYfTd2eUN1VPXezTtLdh7OaUR1Hcu1hdg7TbFmlkYckwNncmWY6jSKxslpnRbX-jTxzpgd6tsjy_pK2MlI0StP8RGwoucpb3gRQdU2N2GlKlg2Iq70KtFtWa6LNy1ROua8mEXvJX6clzbouRbOxOrMQP1bzB8pOt6jUKUf5uTbbQlHoarzgCfx-Lg8HFLpjKa6ssURPl6ksMo2luMahxg4VFGrsgqKxWEsZpBFr78P7dQxq3wYPX1bB8uut_7D4T3_78r8f_VWwDxa7FObmPBAB39bULWDfJMO-qTo2DfumbsFDS-BHmIh9N4p9Ez72TcpTTa93Rv1GyhqftSvKrgtIVDos-X3prqmgg_nYd0WUPFceEtbRb49mK0Wb5_Gwb9ZDBvuhfuybJQSAB6EjSezpBcLTbfA87FvwrKnmf4h9C16Chn0zmgzJjKEvWcS-YS0axb5hQyhj3_xWrWPfUsPJsW9QMzXsW16srrpYO7RqJvYtL1uddluBYmzO4-SwjX7sm_Yrx0nvV7fk-6vbd1c3L97FvMgX392-O446_e6WXN28m52ckNdvDy__6-vX1yfQgf2N7MO164RtXyxsXnbW7mYdZ2PzQBtRw-YZnVcsj_pc2eTawubLg2Fh8wJDaQ2bF4yjgM2L1KGGzZd7FWi3rNZWsfns51nYPEv5rWHzrAEUsHmmwNWw-YpKyoora6xDbP7VJlj_kKUODlD5g1uIxyMej3i82aAD4vGIxyMejx6NeDzi8ehIiMcjHo94POLxiMcjHi-Px3eClMtz5QHDQ8SeNwEgYo-IPSL2iNgTvuLKGusAsX_zuPsYw_FX6_3m1SZYXy-W-xS0Z909wO3PEbdH3B5xe9PACcTtEbdH3B49GnF7xO3RkRC3R9wecXsY_skDNFXPkMlaLVbJJpH3dF9Qfr6kaqH8Uue0NNi10lFBretPZVSSBwc1r02pU27MAfAx8cQ2EJ0Tr4ZCkGbsQursJnK83z4CYiRMB2E6CNNBmA7CdFCb6aC3i4fNL4ubzf7mcbV6sVnfB_tgs07TQay7STroPEoHnWE6CNNBmA4yDfPCdBCmgzAdhB6N6SBMB6EjYToI00GYDtJaxjGoxI5UM53lG4LsC6ixmUKuJ-ta2Re5NB0EXjcOZVcD2zF1w7auBmuGMAci9m3MgWAOBHMgmANpPwfychU8BGt_v7hNQto4-VG5nGQ9zqKsh4NZD8x6YNbDNGgHsx6Y9cCsB3o0Zj0w64GOhFkPzHpg1kNrEUy6RYQ9PcijrkDoulquRKrrsnRBjVsXssqoJGXdeq4kw7Mb_AI3bErADAZmMDCDgRkMzGBgBuPpZjCuF8v9d7vd5i7w94tXm2C9exvu0tJMBvN2ktFwBpjRsDGj0V1GY6RJ-FYuQjUVlDrRqofonZ1I-s5Z-O9koNHl8_j3on-fRv92on9H64UTdx4HCPH0G7eNu6RiagZqmWDeqi8o99DyVnb5rqmAofl5q9EARNnsVOx4UbwVtXXjPuOFZhJdj9q6cZ8TgxRb8SBMSPZhqu44IVmGA3swK-hPSLKEQPGgMgLaS3kNaP7kqEQiH4fzIARNZaQOusgnC4Ziqjce5pMFL0GzX1jWstdmDH3JYj4Z1sKEfLJKDtOw5KUJxyT2ydHNWnCFIlU797PrIyBlD31sxBoB8WKHmenG4wPYu0qHCcUbkmZACw85EwlOD0jEaJ6IIXUKaEbEEASLZSKGbKDYzdaB7bVAIakRMRidwyrEyeHDrREx5CmIHrSqvHVzkBoVeC7XYxxSp93KfX2xY1OBcryQ4oMUH6T4IMVnoBQfZJmYzDLRyyWysresretCV01o3HK9gtLTvxK9W-6kqPr0z0T7ofoKBpD-mdiAlQw6MYPkz6Il0N6xnlF48Us4SVfJqGP7dafJMGOzdc-ScTnIWjId4kHW0pNjLRkETZlDtakolgf89l_5LS9NqVFY7nnRLpI_U9OwEvUm1mFl2q5vIDVWr_N0rLFJz9LBOZosr2JVEJIDLm2dL21sjhGyvJ4mawnCOROwPtCvNbCWhkYrMstRpPJWYkKMKUwYWQqMHraB6WbIm7WfSgzZXADJtgcgY6dzqg6Yo6PXXeCsnCHRcRoPaAUybIbmQ8nxQRiCOAUOZAos73bErs5oMUCPh75rTc-E_Qj4TBUk4CEBr2UCnryAxUcbdcWkUhAxwCn1MKn4vwE79qhjJhWMKcgkabKYVGBAp2VQkh1liQShxqQq9ypgYJatoFUmlUTZxEShbKKbaF06rNajdxhBkfGcUVYg5FQi7w55d1T9PlHeHTKynjojy-YzspogAFrZwDUZXaHDhkzP8k4PrS-9kBug5TklG0yv5GYYqunQEtMruTEmeirYo5WZEUACmgwzeb-CbSZvUzDPZOwFC81tDDBSdasNG59mI3WT3vKRxu7vuflIY5f3vHyk7lli3TTjr3A3MLnbOcbdeAhW4ZEhH9HMtRkpaYpkSBaDBWe3zmc3tlIGSx17KrNGc1MGDOLkEHk6Z_CAqTs1SQg98JN-bEIKXmR5k5IjJVcKvmQlq1TuTukVretwvd1K7mDpK-U-ZmXLbL2VWdOWJbwyyYcbz13eNB_uIaAL5SR1R0YSs5A0eb7E6ZFDoB_0fTPG0o1mIhMFZufyPHFBwAWhhwsCK7tU5xufODeqHonSDOULfuAKkjGHuBUUKBvJmIB3RTImkjGRjCklYCRjPlEypmC9AR5X1xUDT2aq5sVSehh4jM7VvkXaKgNPwBmtcnFZDDxxTqqt9C8bGmC-rBoDzy6rrky0LWu2VQYepL5xKlPf2DJiBEd49GhTwEUtP2CGbpnuGtEv32wf1xGv8nqx3L_YrHYp8ZJyY8CUy0osjhw45MANhgNHBBy4ysqNLJEnkHZRVzYqz5gDFQfMNzRhMcLUVw9SXwAKEDiCQ6NHo--F0VdWAhEfsHsCkOwvC89aFOE1yicZsXthTSS6PgeNoVXnoVWzCUNA1S_SkXB5GszyhHQkpCMhHQnpSEhHatEzkY6EdCSkIyEdCelIEsaBdKRe0JHKI4Ke99UVaUUwDg4OoIe0Uu5V8uOprZJWKj8vIK1EZ4FRWSuFOwe0lSnSVqpJD6StIG3ladJW-rvB7Dvc3i4NRUoVUoKVEJMG-gbmsTFRMKxEgU7yBsYxGMcYGccQ9lbVAR1NrY8wwdwOSlAdIJ_jFdNPMPbq_BhMvcmIylSe7bK1HsNjWiwHLyZDYgfGa32P1yCf0WUsAbgSaP9gTM0Zu_bnaJDYMURih0DZcFcfoMdD37UZz2TnkGENkNhxyf9MtoDYwW_VDbFDakw5sQPUrAaxQ17AnrqAO3V5VafUQ-wAfgid_oE3M4gdEp-s53zMrytih8xUzYul9BA7YB9QZDxXNoBOz5mBHiPTFbEDFjzTcAA9xI6a33I04qQZyrqCID6C-AaC-KwSQeGh44Q1PfV3D9R3_JurIK3HjxPmmsB9DgFiBIj7ChBzDRs6Ww4QOGp8HwIRZBOTG3ujA3kckeMhIccQjUvOAQOcCqReuEGXZQNXEq0QUk4hZQmhlXFlQNNuwGX5geUIM7xtDZhZUeheTaF3Pz_Ucl490DPgh2BH5JoAQkOWrjISrbRudRgGqoVqeoBp3i9IWEnZPFqFqLl7G_6p2XSD6ujobPpgROiEHtiae0Y2zwDKmjexMvHlKngI1v4-qkH8-_p-sX0TcyCvF8t9WqQoeCipV5xG9YqnfaxXtNkVLsjzNwRLwxRBAykCitm7pRto9mj2ULPvoqi2EtY8hbrMSkFLY3WZ6MNPzIf7snSxHMDFGk10APMWMc7eu48VlZUXyjZ8WusoTVtu4ZQ5JIzhjNKvJZVFzxRWzzDj7_4mPPtOGOMqSGulDzUMo8yItIAMCWPlaQ0JYz0gjHENGzpbDpAl0nLhCl2QTUxu0MoYcASIhLHeEsYgGpecAwY4FUi9cIMuy-acSLRCwtgltQaZKzR-ITK1aTeEMfmBsUqSeW211SWDhV4tTpYTevfzQy3n1UMYA_wQrIrRBMIYZOnily5zja-j-mXumIChmh7CGO8XJKykbB4d1jQTtnFUC5vpBtVRdTN9MCJ0Qg9hjFvGzDOAsuaNqHhWVKABID_7nZg8UsTtEbdvtywbXGQ0yH1iy-Ry3VtPQe-caQYxogFjRAJlw119gB4PfddmPJO9pYQ1QCjoklo7CEWB-K26AYCkxsQqFtQP-8gLuFoiaArYoyBigFPqgXj4vyGo8zED2BGsN_wiwM7hHJmpmhdL6QFxGJ3D7KBsAB3W-uWjSJh5wWbNqlP889BgY3wn2uxbJDVZYTVhMjEnjYO5d5o1voA0jqbzpPVy7jlZa-cU0jxaBZLmu7nn5s0dZvOCo0aLR9L809zz8uYdVdOdkpFjTSaDqaZzsZoOcRijcJiqkSYxBxqpKUbaEMerEVPFSjXJSjUXK9XM8zhcFhjmergfRXM1w1z7tkAQ9s78qVWB9S-bLKgCo52Uj9lkXK66yCYruplUGNiMT4pIG1RcGN0Ml1nKMivOu-IX_xqnWAxBhlWb0putxs8aniCP5ERQa1RjPhukS7byWUMQw4uXq0bmCJA5okYZMYorIksS0c8OUaOFtM4HYaymbOfCj4Xix0KlHA8_FoofCzWEnYUfC2WZgzZ2lh5bY6xKPfgSKYiM1pa9l8VI5aEdPkQOrUnEQxM15vLQRI0FPDRRcwEPjf-JUyEPTcSCq9k8k3pss4rsv3gSUmX_xVOXMvsvnvGU2X_xRNk1-28yISPH8mZDYf8l4QLmzRHQVwH0aVy9mUlUvYr-KwKu6L8i8ar-qzqo6r-qlar-W9ITzeyrVl616aoFH9orEuvkiHWHezMj_ANn3N7NuAzjmhnHgsO5t-G5t6Rs5Kz1iUzD56xRYTwk0-BKkK8ELOt4mmQa4Uu2QQThge-YCgOmwtRyYEYlv2SzXvrTXWp5LsMSXNKZrWZSWmpphq6OGGgzNpKyDrA_iojGtHRTo6ERbmQa3ciwdAnkG5lBNAIzjGrM9aTUI-bvCpkkUXPoORIqCTiw2GgJOLjYqAk4uNioCTh42pOagIOnPZWaZ1KPVxrFtGe8MqmmPeP1TDntGS-DymnPePVUS3u-2T6uF7d3_vrFZrVL05yli3Fa05v1N63JAkONyWpi-NBw-KBHz0ytog4xj9hFHrHy0oezW-kmTnHDdQ-teT2c5zBn10LOTvwm_cOl-DKl4byIS_XXWWUwRpxUDZhU1RJIZuGHCqMQ1sAL01D6yx05PXJ8BSFOhDj1Q5xwsfFrDFQwSrDYaBglXGxUjBIuNipGSW2uHaPMqi7epIFjqRoju36AVMbHL9tZtDmiowY9hi9xu9f_yMQWwFoVEOcpwFpMerx2WAtNv0vTLymyjHSIOb1QpEPApKWEvSpIR-WFjGAnN4V08GUqi3SgIxrgiK3vjoGkWbV9KXcXyfL3g10k94O6ol2kqDF3FylqLNhFipqrFLrDd5FgsdF2kWCx0XaRcLFRd5FwscGJMvBdJFhstF0kWGy0XSRcbNRdJFxs1F0knF9E3UXa5SblXeSIE2KMdEQXlE7ywGJUL6YY0cIJ7qj7F0kw5Tc7od7E-MGs-IGioioxV2vUwO6Oajg1YwWKC5e-y6YWIXDaCb7GphoXcFoKv8GmGg1ApEM_skYtBgBJh3FQjerKD5IOg96qymxVJbWq81nVqayqa_uIs6wfWUcvf_2y2mwX3wfL5dH8KEaL3y422_vF9tUmWGeE1sNrCUo8KaDEDEYZYsO4rnaMDbOxMS2wGBcRq0n7oTJ-BhW4cuMPBL6MdzAA5qUZ7oIgXfpALhG-pQptqaJa6oCWOpalDmOpIliq4JU6bqUOWamjVapAlSpGpQ5PqSNT6qBUrzEnHpgWB9n_tgl2-4R28cN283AVhs9heJ1G3JwHKOG3TQu_R8OoMcNAvP9xQq8DcZuekB5UIM6U3-yEehMDcbMcrLhcUteC-k6mJyORn11LsyjtU7sQW3eA2LpCWAGJLmoGGUPee1Fm7RK3QJVWoMooUCcTqPMI1CkEquwBVeKAOmdAnS6gzhRQJQmo8gPUqQHqrAB1QgCfC1B2bMojjDSfpKvSGggSe9LOSWsiTOVJuyP31bnfm4Bn7aQ9j__q_G9JyCToJJ2M--rcc2ZkcnHS_jRScKQaCTXcvOPewoy9BRHsLTTEzXqSRrnJmbG36D1vR7DFcIBbDPhxCuK9k2B_IQHpCBRn61UcuzsqzqI_b1WO0pj7Km5QzK32hWexpKM0fvzLr-SVSVhJRmncV-dW6crkpqSjNP6r8ytw4WkoySiN_-r86lqZjJN0lHb0___0jwAAAP__VSsNvg== # Exploration patterns with varying costs. optsteps diff --git a/pkg/sql/opt/norm/testdata/rules/join b/pkg/sql/opt/norm/testdata/rules/join index 28f72b47c360..22b83f11727b 100644 --- a/pkg/sql/opt/norm/testdata/rules/join +++ b/pkg/sql/opt/norm/testdata/rules/join @@ -1844,21 +1844,20 @@ semi-join-apply norm expect=SimplifyLeftJoin SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k ---- -inner-join (hash) +project ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) ├── key: (8) - ├── fd: (8)-->(9-12), (1)-->(2-5), (1)==(8), (8)==(1) + ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5) ├── scan a [as=a2] │ ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12 │ ├── key: (8) │ └── fd: (8)-->(9-12) - ├── scan a - │ ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 - │ ├── key: (1) - │ └── fd: (1)-->(2-5) - └── filters - └── a.k:1 = a2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + └── projections + ├── a2.k:8 [as=a.k:1, outer=(8)] + ├── a2.i:9 [as=a.i:2, outer=(9)] + ├── a2.f:10 [as=a.f:3, outer=(10)] + ├── a2.s:11 [as=a.s:4, outer=(11)] + └── a2.j:12 [as=a.j:5, outer=(12)] # Right side has partial rows, so only right-join can be simplified. norm expect=SimplifyRightJoin @@ -1890,22 +1889,20 @@ left-join (hash) norm expect=SimplifyLeftJoin SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k AND a.k=a2.k AND a2.f=a.f ---- -inner-join (hash) +project ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) ├── key: (8) - ├── fd: (8)-->(9-12), (1)-->(2-5), (3)==(10), (10)==(3), (1)==(8), (8)==(1) + ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5) ├── scan a [as=a2] │ ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12 │ ├── key: (8) │ └── fd: (8)-->(9-12) - ├── scan a - │ ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 - │ ├── key: (1) - │ └── fd: (1)-->(2-5) - └── filters - ├── a2.f:10 = a.f:3 [outer=(3,10), constraints=(/3: (/NULL - ]; /10: (/NULL - ]), fd=(3)==(10), (10)==(3)] - └── a2.k:8 = a.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + └── projections + ├── a2.k:8 [as=a.k:1, outer=(8)] + ├── a2.i:9 [as=a.i:2, outer=(9)] + ├── a2.f:10 [as=a.f:3, outer=(10)] + ├── a2.s:11 [as=a.s:4, outer=(11)] + └── a2.j:12 [as=a.j:5, outer=(12)] # Input contains Project operator. norm expect=SimplifyLeftJoin @@ -1937,23 +1934,22 @@ SELECT * FROM a FULL JOIN (SELECT * FROM a INNER JOIN a AS a2 ON a.k=a2.k) AS a2 inner-join (hash) ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 k:15!null i:16 f:17!null s:18 j:19 ├── multiplicity: left-rows(one-or-more), right-rows(one-or-more) - ├── key: (1,15) - ├── fd: (8)-->(9-12), (15)-->(16-19), (8)==(15), (15)==(8), (1)-->(2-5), (3)==(10), (10)==(3) - ├── inner-join (hash) - │ ├── columns: a.k:8!null a.i:9 a.f:10!null a.s:11 a.j:12 a2.k:15!null a2.i:16 a2.f:17!null a2.s:18 a2.j:19 - │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ ├── key: (15) - │ ├── fd: (8)-->(9-12), (15)-->(16-19), (8)==(15), (15)==(8) + ├── key: (1,8) + ├── fd: (8)-->(9-12), (8)==(15), (15)==(8), (9)==(16), (16)==(9), (10)==(3,17), (17)==(3,10), (11)==(18), (18)==(11), (12)==(19), (19)==(12), (1)-->(2-5), (3)==(10,17) + ├── project + │ ├── columns: a2.k:15!null a2.i:16 a2.f:17!null a2.s:18 a2.j:19 a.k:8!null a.i:9 a.f:10!null a.s:11 a.j:12 + │ ├── key: (8) + │ ├── fd: (8)-->(9-12), (8)==(15), (15)==(8), (9)==(16), (16)==(9), (10)==(17), (17)==(10), (11)==(18), (18)==(11), (12)==(19), (19)==(12) │ ├── scan a │ │ ├── columns: a.k:8!null a.i:9 a.f:10!null a.s:11 a.j:12 │ │ ├── key: (8) │ │ └── fd: (8)-->(9-12) - │ ├── scan a [as=a2] - │ │ ├── columns: a2.k:15!null a2.i:16 a2.f:17!null a2.s:18 a2.j:19 - │ │ ├── key: (15) - │ │ └── fd: (15)-->(16-19) - │ └── filters - │ └── a.k:8 = a2.k:15 [outer=(8,15), constraints=(/8: (/NULL - ]; /15: (/NULL - ]), fd=(8)==(15), (15)==(8)] + │ └── projections + │ ├── a.k:8 [as=a2.k:15, outer=(8)] + │ ├── a.i:9 [as=a2.i:16, outer=(9)] + │ ├── a.f:10 [as=a2.f:17, outer=(10)] + │ ├── a.s:11 [as=a2.s:18, outer=(11)] + │ └── a.j:12 [as=a2.j:19, outer=(12)] ├── scan a │ ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 │ ├── key: (1) @@ -3845,41 +3841,39 @@ inner-join (cross) norm expect=RemoveJoinNotNullCondition SELECT * FROM a LEFT JOIN a AS a2 ON a.k=a2.k AND a.f=a.f AND a2.f=a2.f ---- -inner-join (hash) +project ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - ├── key: (8) - ├── fd: (1)-->(2-5), (8)-->(9-12), (1)==(8), (8)==(1) + ├── key: (1) + ├── fd: (1)-->(2-5), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5) ├── scan a │ ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 │ ├── key: (1) │ └── fd: (1)-->(2-5) - ├── scan a [as=a2] - │ ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12 - │ ├── key: (8) - │ └── fd: (8)-->(9-12) - └── filters - └── a.k:1 = a2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + └── projections + ├── a.k:1 [as=a2.k:8, outer=(1)] + ├── a.i:2 [as=a2.i:9, outer=(2)] + ├── a.f:3 [as=a2.f:10, outer=(3)] + ├── a.s:4 [as=a2.s:11, outer=(4)] + └── a.j:5 [as=a2.j:12, outer=(5)] # Full join case. norm expect=RemoveJoinNotNullCondition SELECT * FROM a FULL JOIN a AS a2 ON a.k=a2.k AND a.f=a.f AND a2.f=a2.f ---- -inner-join (hash) +project ├── columns: k:1!null i:2 f:3!null s:4 j:5 k:8!null i:9 f:10!null s:11 j:12 - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) ├── key: (8) - ├── fd: (8)-->(9-12), (1)-->(2-5), (1)==(8), (8)==(1) + ├── fd: (8)-->(9-12), (1)==(8), (8)==(1), (2)==(9), (9)==(2), (3)==(10), (10)==(3), (4)==(11), (11)==(4), (5)==(12), (12)==(5) ├── scan a [as=a2] │ ├── columns: a2.k:8!null a2.i:9 a2.f:10!null a2.s:11 a2.j:12 │ ├── key: (8) │ └── fd: (8)-->(9-12) - ├── scan a - │ ├── columns: a.k:1!null a.i:2 a.f:3!null a.s:4 a.j:5 - │ ├── key: (1) - │ └── fd: (1)-->(2-5) - └── filters - └── a.k:1 = a2.k:8 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + └── projections + ├── a2.k:8 [as=a.k:1, outer=(8)] + ├── a2.i:9 [as=a.i:2, outer=(9)] + ├── a2.f:10 [as=a.f:3, outer=(10)] + ├── a2.s:11 [as=a.s:4, outer=(11)] + └── a2.j:12 [as=a.j:5, outer=(12)] # No-op case because i is nullable. norm expect-not=RemoveJoinNotNullCondition diff --git a/pkg/sql/opt/norm/testdata/rules/project b/pkg/sql/opt/norm/testdata/rules/project index 166298d40ac9..eec07141c019 100644 --- a/pkg/sql/opt/norm/testdata/rules/project +++ b/pkg/sql/opt/norm/testdata/rules/project @@ -270,15 +270,7 @@ project # EliminateJoinUnderProjectRight # -------------------------------------------------- -# InnerJoin case with self join. -norm expect=EliminateJoinUnderProjectRight -SELECT b1.x, b1.z FROM b INNER JOIN b AS b1 ON b.x = b1.x ----- -scan b [as=b1] - ├── columns: x:6!null z:7 - ├── key: (6) - └── fd: (6)-->(7) - +# No self-join case because EliminateJoinUnderProjectLeft can always match. # InnerJoin case with not-null foreign key. norm expect=EliminateJoinUnderProjectRight SELECT k, v FROM a INNER JOIN fks ON r1 = x @@ -290,41 +282,41 @@ scan fks # The left column can be remapped to a right column. norm expect=EliminateJoinUnderProjectRight -SELECT b.x, b1.j FROM b INNER JOIN b AS b1 ON b.x = b1.x +SELECT x, k, v FROM a INNER JOIN fks ON r1 = x ---- project - ├── columns: x:1!null j:8 - ├── key: (1) - ├── fd: (1)-->(8) - ├── scan b [as=b1] - │ ├── columns: b1.x:6!null b1.j:8 - │ ├── key: (6) - │ └── fd: (6)-->(8) + ├── columns: x:1!null k:7!null v:8 + ├── key: (7) + ├── fd: (7)-->(1,8) + ├── scan fks + │ ├── columns: k:7!null v:8 r1:10!null + │ ├── key: (7) + │ └── fd: (7)-->(8,10) └── projections - └── b1.x:6 [as=b.x:1, outer=(6)] + └── r1:10 [as=x:1, outer=(10)] -# No-op case because columns from the right side of a LeftJoin are being -# projected. +# No-op case because the left input of a LeftJoin cannot be removed. norm expect-not=EliminateJoinUnderProjectRight -SELECT b.j, b1.j FROM b LEFT JOIN b AS b1 ON b.x = b1.x +SELECT x, k, v FROM a LEFT JOIN fks ON r1 = x ---- project - ├── columns: j:3 j:8 - └── inner-join (hash) - ├── columns: b.x:1!null b.j:3 b1.x:6!null b1.j:8 - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - ├── key: (6) - ├── fd: (1)-->(3), (6)-->(8), (1)==(6), (6)==(1) - ├── scan b - │ ├── columns: b.x:1!null b.j:3 - │ ├── key: (1) - │ └── fd: (1)-->(3) - ├── scan b [as=b1] - │ ├── columns: b1.x:6!null b1.j:8 - │ ├── key: (6) - │ └── fd: (6)-->(8) + ├── columns: x:1!null k:7 v:8 + ├── key: (1,7) + ├── fd: (7)-->(8) + └── left-join (hash) + ├── columns: x:1!null k:7 v:8 r1:10 + ├── multiplicity: left-rows(one-or-more), right-rows(exactly-one) + ├── key: (1,7) + ├── fd: (7)-->(8,10) + ├── scan a + │ ├── columns: x:1!null + │ └── key: (1) + ├── scan fks + │ ├── columns: k:7!null v:8 r1:10!null + │ ├── key: (7) + │ └── fd: (7)-->(8,10) └── filters - └── b.x:1 = b1.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] + └── r1:10 = x:1 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)] # -------------------------------------------------- # EliminateProject diff --git a/pkg/sql/opt/norm/testdata/rules/prune_cols b/pkg/sql/opt/norm/testdata/rules/prune_cols index d9a3ccd17487..bdd0da683698 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -5313,7 +5313,7 @@ CREATE TABLE c100478 ( # A join which doesn't prune columns which could potentially appear in derived # ON clause conditions should not result in infinite rule recursion. -norm expect=(EliminateGroupByProject,PruneJoinLeftCols,PruneJoinRightCols) disable=(EliminateJoinUnderProjectRight,EliminateJoinUnderGroupByRight) +norm expect=(EliminateGroupByProject,PruneJoinLeftCols,PruneJoinRightCols) disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight,EliminateJoinUnderGroupByLeft,EliminateJoinUnderGroupByRight) SELECT p.id FROM p100478 p JOIN c100478 ON p_id = p.id GROUP BY p.id ---- distinct-on diff --git a/pkg/sql/opt/optbuilder/testdata/update_from b/pkg/sql/opt/optbuilder/testdata/update_from index 47171435775a..d19d31c4d5f6 100644 --- a/pkg/sql/opt/optbuilder/testdata/update_from +++ b/pkg/sql/opt/optbuilder/testdata/update_from @@ -23,17 +23,16 @@ update abc │ └── c_new:17 => abc.c:3 └── project ├── columns: b_new:16 c_new:17 abc.a:6!null abc.b:7 abc.c:8 other.a:11!null other.b:12 other.c:13 other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 - ├── inner-join (merge) - │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 other.a:11!null other.b:12 other.c:13 other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 - │ ├── left ordering: +6 - │ ├── right ordering: +11 + ├── project + │ ├── columns: other.a:11!null other.b:12 other.c:13 other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 abc.a:6!null abc.b:7 abc.c:8 │ ├── scan abc - │ │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 - │ │ └── ordering: +6 - │ ├── scan abc [as=other] - │ │ ├── columns: other.a:11!null other.b:12 other.c:13 other.crdb_internal_mvcc_timestamp:14 other.tableoid:15 - │ │ └── ordering: +11 - │ └── filters (true) + │ │ └── columns: abc.a:6!null abc.b:7 abc.c:8 abc.crdb_internal_mvcc_timestamp:9 abc.tableoid:10 + │ └── projections + │ ├── abc.a:6 [as=other.a:11] + │ ├── abc.b:7 [as=other.b:12] + │ ├── abc.c:8 [as=other.c:13] + │ ├── abc.crdb_internal_mvcc_timestamp:9 [as=other.crdb_internal_mvcc_timestamp:14] + │ └── abc.tableoid:10 [as=other.tableoid:15] └── projections ├── other.b:12 + 1 [as=b_new:16] └── other.c:13 + 1 [as=c_new:17] @@ -103,17 +102,13 @@ update abc │ └── c_new:17 => abc.c:3 └── project ├── columns: b_new:16 c_new:17 abc.a:6!null abc.b:7 abc.c:8 old.b:12 old.c:13 - ├── inner-join (merge) - │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 old.a:11!null old.b:12 old.c:13 - │ ├── left ordering: +6 - │ ├── right ordering: +11 + ├── project + │ ├── columns: old.b:12 old.c:13 abc.a:6!null abc.b:7 abc.c:8 │ ├── scan abc - │ │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 - │ │ └── ordering: +6 - │ ├── scan abc [as=old] - │ │ ├── columns: old.a:11!null old.b:12 old.c:13 - │ │ └── ordering: +11 - │ └── filters (true) + │ │ └── columns: abc.a:6!null abc.b:7 abc.c:8 + │ └── projections + │ ├── abc.b:7 [as=old.b:12] + │ └── abc.c:8 [as=old.c:13] └── projections ├── old.b:12 + 1 [as=b_new:16] └── old.c:13 + 2 [as=c_new:17] @@ -135,17 +130,14 @@ update abc │ └── c_new:17 => abc.c:3 └── project ├── columns: b_new:16 c_new:17 abc.a:6!null abc.b:7 abc.c:8 old.a:11!null old.b:12 old.c:13 - ├── inner-join (merge) - │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 old.a:11!null old.b:12 old.c:13 - │ ├── left ordering: +6 - │ ├── right ordering: +11 + ├── project + │ ├── columns: old.a:11!null old.b:12 old.c:13 abc.a:6!null abc.b:7 abc.c:8 │ ├── scan abc - │ │ ├── columns: abc.a:6!null abc.b:7 abc.c:8 - │ │ └── ordering: +6 - │ ├── scan abc [as=old] - │ │ ├── columns: old.a:11!null old.b:12 old.c:13 - │ │ └── ordering: +11 - │ └── filters (true) + │ │ └── columns: abc.a:6!null abc.b:7 abc.c:8 + │ └── projections + │ ├── abc.a:6 [as=old.a:11] + │ ├── abc.b:7 [as=old.b:12] + │ └── abc.c:8 [as=old.c:13] └── projections ├── old.b:12 + 1 [as=b_new:16] └── old.c:13 + 2 [as=c_new:17] diff --git a/pkg/sql/opt/ordering/lookup_join_test.go b/pkg/sql/opt/ordering/lookup_join_test.go index c4e6386aa225..15ad50c2d152 100644 --- a/pkg/sql/opt/ordering/lookup_join_test.go +++ b/pkg/sql/opt/ordering/lookup_join_test.go @@ -26,6 +26,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/testutils/testexpr" "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/types" ) func TestLookupJoinProvided(t *testing.T) { @@ -46,6 +47,9 @@ func TestLookupJoinProvided(t *testing.T) { md := f.Metadata() tn := tree.NewUnqualifiedTableName("t") tab := md.AddTable(tc.Table(tn), tn) + for i := 0; i < 4; i++ { + md.AddColumn(fmt.Sprintf("input_col%d", i), types.Int) + } if c1 := tab.ColumnID(0); c1 != 1 { t.Fatalf("unexpected ID for column c1: %d\n", c1) @@ -153,10 +157,10 @@ func TestLookupJoinProvided(t *testing.T) { for tcIdx, tc := range testCases { t.Run(fmt.Sprintf("case%d", tcIdx+1), func(t *testing.T) { inputFDs := props.FuncDepSet{} - inputFDs.AddStrictKey(tc.inputKey, c(5, 6)) + inputFDs.AddStrictKey(tc.inputKey, c(5, 6, 7, 8)) input := &testexpr.Instance{ Rel: &props.Relational{ - OutputCols: c(5, 6), + OutputCols: c(5, 6, 7, 8), FuncDeps: inputFDs, }, Provided: &physical.Provided{ diff --git a/pkg/sql/opt/xform/testdata/external/hibernate b/pkg/sql/opt/xform/testdata/external/hibernate index 133ed327f9e8..e8cd7980eff5 100644 --- a/pkg/sql/opt/xform/testdata/external/hibernate +++ b/pkg/sql/opt/xform/testdata/external/hibernate @@ -56,28 +56,21 @@ where phoneregis0_.phone_id=1; project ├── columns: phone_id1_2_0_:1!null person_i2_2_0_:2!null formula159_0_:17 id1_1_1_:5!null number2_1_1_:6 since3_1_1_:7 type4_1_1_:8 ├── key: (5) - ├── fd: ()-->(1), (5)-->(6-8), (2)==(5), (5)==(2), (2)-->(17) - ├── inner-join (lookup phone [as=a10]) - │ ├── columns: phone_id:1!null person_id:2!null unidirecti1_.id:5!null unidirecti1_.number:6 unidirecti1_.since:7 unidirecti1_.type:8 a10.id:11!null a10.since:13 - │ ├── key columns: [2] = [11] + ├── fd: ()-->(1), (5)-->(6-8), (2)==(5), (5)==(2), (7)==(17), (17)==(7) + ├── inner-join (lookup phone [as=unidirecti1_]) + │ ├── columns: phone_id:1!null person_id:2!null unidirecti1_.id:5!null unidirecti1_.number:6 unidirecti1_.since:7 unidirecti1_.type:8 + │ ├── key columns: [2] = [5] │ ├── lookup columns are key - │ ├── key: (11) - │ ├── fd: ()-->(1), (5)-->(6-8), (2)==(5,11), (5)==(2,11), (11)-->(13), (11)==(2,5) - │ ├── inner-join (lookup phone [as=unidirecti1_]) - │ │ ├── columns: phone_id:1!null person_id:2!null unidirecti1_.id:5!null unidirecti1_.number:6 unidirecti1_.since:7 unidirecti1_.type:8 - │ │ ├── key columns: [2] = [5] - │ │ ├── lookup columns are key - │ │ ├── key: (5) - │ │ ├── fd: ()-->(1), (5)-->(6-8), (2)==(5), (5)==(2) - │ │ ├── scan phone_register [as=phoneregis0_] - │ │ │ ├── columns: phone_id:1!null person_id:2!null - │ │ │ ├── constraint: /1/2: [/1 - /1] - │ │ │ ├── key: (2) - │ │ │ └── fd: ()-->(1) - │ │ └── filters (true) + │ ├── key: (5) + │ ├── fd: ()-->(1), (5)-->(6-8), (2)==(5), (5)==(2) + │ ├── scan phone_register [as=phoneregis0_] + │ │ ├── columns: phone_id:1!null person_id:2!null + │ │ ├── constraint: /1/2: [/1 - /1] + │ │ ├── key: (2) + │ │ └── fd: ()-->(1) │ └── filters (true) └── projections - └── a10.since:13 [as=formula159_0_:17, outer=(13)] + └── unidirecti1_.since:7 [as=formula159_0_:17, outer=(7)] exec-ddl drop table phone_register, Person, Phone; diff --git a/pkg/sql/opt/xform/testdata/physprops/ordering b/pkg/sql/opt/xform/testdata/physprops/ordering index 87eb2b50c845..fe58aed7d0c3 100644 --- a/pkg/sql/opt/xform/testdata/physprops/ordering +++ b/pkg/sql/opt/xform/testdata/physprops/ordering @@ -2989,7 +2989,7 @@ CREATE TABLE table87806 ( ); ---- -opt format=hide-all +opt format=hide-all disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT tab_171969.col1_3 FROM table87806 AS tab_171967 JOIN table87806 AS tab_171968 @@ -3016,7 +3016,8 @@ project │ │ │ └── filters │ │ │ └── tab_171969.col1_3 = tab_171969.tableoid │ │ └── filters - │ │ └── tab_171968.col1_3 = tab_171969.col1_3 + │ │ ├── tab_171968.col1_3 = tab_171969.col1_3 + │ │ └── tab_171968.tableoid = tab_171969.col1_3 │ └── projections │ └── true └── filters (true) diff --git a/pkg/sql/opt/xform/testdata/rules/join_order b/pkg/sql/opt/xform/testdata/rules/join_order index b10bbce9ba0b..45913f964797 100644 --- a/pkg/sql/opt/xform/testdata/rules/join_order +++ b/pkg/sql/opt/xform/testdata/rules/join_order @@ -1964,27 +1964,63 @@ full-join (hash) └── filters └── y = z +exec-ddl +CREATE TABLE abc2 ( + a INT PRIMARY KEY, + b INT, + c INT, + d INT +) +---- + +exec-ddl +CREATE TABLE abc3 ( + a INT PRIMARY KEY, + b INT, + c INT, + d INT +) +---- + +exec-ddl +CREATE TABLE abc4 ( + a INT PRIMARY KEY, + b INT, + c INT, + d INT +) +---- + +exec-ddl +CREATE TABLE abc5 ( + a INT PRIMARY KEY, + b INT, + c INT, + d INT +) +---- + # Iteratively reorder subtrees of up to size 2. reorderjoins set=reorder_joins_limit=2 format=hide-all SELECT * FROM abc AS a1 -INNER JOIN abc AS a2 ON a1.a = a2.a -LEFT JOIN abc AS a3 ON a2.b = a3.b -INNER JOIN abc AS a4 ON a3.a = a4.a -WHERE EXISTS (SELECT * FROM abc AS a5 WHERE a2.c = a5.c) +INNER JOIN abc2 AS a2 ON a1.a = a2.a +LEFT JOIN abc3 AS a3 ON a2.b = a3.b +INNER JOIN abc4 AS a4 ON a3.a = a4.a +WHERE EXISTS (SELECT * FROM abc5 AS a5 WHERE a2.c = a5.c) ---- -------------------------------------------------------------------------------- Join Tree #1 -------------------------------------------------------------------------------- semi-join (hash) - ├── scan abc [as=a2] - ├── scan abc [as=a5] + ├── scan abc2 [as=a2] + ├── scan abc5 [as=a5] └── filters └── a2.c = a5.c Vertexes A: - scan abc [as=a2] + scan abc2 [as=a2] B: - scan abc [as=a5] + scan abc5 [as=a5] Edges a2.c = a5.c [semi, ses=AB, tes=AB, rules=()] Joining AB @@ -1994,17 +2030,17 @@ Joins Considered: 1 Join Tree #2 -------------------------------------------------------------------------------- inner-join (hash) - ├── scan abc [as=a2] + ├── scan abc2 [as=a2] ├── distinct-on - │ └── scan abc [as=a5] + │ └── scan abc5 [as=a5] └── filters └── a2.c = a5.c Vertexes A: - scan abc [as=a2] + scan abc2 [as=a2] C: distinct-on - └── scan abc [as=a5] + └── scan abc5 [as=a5] Edges a2.c = a5.c [inner, ses=AC, tes=AC, rules=()] Joining AC @@ -2017,8 +2053,8 @@ Join Tree #3 inner-join (hash) ├── scan abc [as=a1] ├── semi-join (hash) - │ ├── scan abc [as=a2] - │ ├── scan abc [as=a5] + │ ├── scan abc2 [as=a2] + │ ├── scan abc5 [as=a5] │ └── filters │ └── a2.c = a5.c └── filters @@ -2027,9 +2063,9 @@ Vertexes D: scan abc [as=a1] A: - scan abc [as=a2] + scan abc2 [as=a2] B: - scan abc [as=a5] + scan abc5 [as=a5] Edges a2.c = a5.c [semi, ses=AB, tes=AB, rules=()] a1.a = a2.a [inner, ses=DA, tes=DA, rules=()] @@ -2050,13 +2086,13 @@ Join Tree #4 ├── inner-join (hash) │ ├── scan abc [as=a1] │ ├── semi-join (hash) - │ │ ├── scan abc [as=a2] - │ │ ├── scan abc [as=a5] + │ │ ├── scan abc2 [as=a2] + │ │ ├── scan abc5 [as=a5] │ │ └── filters │ │ └── a2.c = a5.c │ └── filters │ └── a1.a = a2.a - ├── scan abc [as=a3] + ├── scan abc3 [as=a3] └── filters └── a2.b = a3.b Vertexes @@ -2064,12 +2100,12 @@ Vertexes scan abc [as=a1] E: semi-join (hash) - ├── scan abc [as=a2] - ├── scan abc [as=a5] + ├── scan abc2 [as=a2] + ├── scan abc5 [as=a5] └── filters └── a2.c = a5.c F: - scan abc [as=a3] + scan abc3 [as=a3] Edges a1.a = a2.a [inner, ses=DE, tes=DE, rules=()] a2.b = a3.b [inner, ses=EF, tes=EF, rules=()] @@ -2093,16 +2129,16 @@ Join Tree #5 │ ├── inner-join (hash) │ │ ├── scan abc [as=a1] │ │ ├── semi-join (hash) - │ │ │ ├── scan abc [as=a2] - │ │ │ ├── scan abc [as=a5] + │ │ │ ├── scan abc2 [as=a2] + │ │ │ ├── scan abc5 [as=a5] │ │ │ └── filters │ │ │ └── a2.c = a5.c │ │ └── filters │ │ └── a1.a = a2.a - │ ├── scan abc [as=a3] + │ ├── scan abc3 [as=a3] │ └── filters │ └── a2.b = a3.b - ├── scan abc [as=a4] + ├── scan abc4 [as=a4] └── filters └── a3.a = a4.a Vertexes @@ -2110,16 +2146,16 @@ Vertexes inner-join (hash) ├── scan abc [as=a1] ├── semi-join (hash) - │ ├── scan abc [as=a2] - │ ├── scan abc [as=a5] + │ ├── scan abc2 [as=a2] + │ ├── scan abc5 [as=a5] │ └── filters │ └── a2.c = a5.c └── filters └── a1.a = a2.a F: - scan abc [as=a3] + scan abc3 [as=a3] H: - scan abc [as=a4] + scan abc4 [as=a4] Edges a2.b = a3.b [inner, ses=GF, tes=GF, rules=()] a3.a = a4.a [inner, ses=FH, tes=FH, rules=()] @@ -2143,15 +2179,15 @@ inner-join (hash) │ └── inner-join (hash) │ ├── inner-join (merge) │ │ ├── scan abc [as=a1] - │ │ ├── scan abc [as=a2] + │ │ ├── scan abc2 [as=a2] │ │ └── filters (true) │ ├── distinct-on - │ │ └── scan abc [as=a5] + │ │ └── scan abc5 [as=a5] │ └── filters │ └── a2.c = a5.c ├── inner-join (merge) - │ ├── scan abc [as=a3] - │ ├── scan abc [as=a4] + │ ├── scan abc3 [as=a3] + │ ├── scan abc4 [as=a4] │ └── filters (true) └── filters └── a2.b = a3.b @@ -2923,7 +2959,7 @@ inner-join (lookup t88659) │ ├── lookup columns are key │ ├── immutable │ ├── key: (1) - │ ├── fd: (1)-->(2,3), (7)-->(9), (7)==(2,8,19), (8)==(2,7,19), (19)-->(20,21), (19)==(2,7,8), (2)==(7,8,19), (3)==(9), (9)==(3) + │ ├── fd: (1)-->(2,3), (7)-->(9), (7)==(2,8,19,20), (8)==(2,7,19,20), (19)-->(21), (19)==(2,7,8,20), (20)==(2,7,8,19), (9)==(3,21), (21)==(3,9), (2)==(7,8,19,20), (3)==(9,21) │ ├── inner-join (lookup t88659) │ │ ├── columns: a:1!null b:2!null c:3!null a:7!null b:8!null c:9!null │ │ ├── key columns: [1] = [1]