diff --git a/pkg/ccl/logictestccl/testdata/logic_test/as_of b/pkg/ccl/logictestccl/testdata/logic_test/as_of index c214e714438c..36c96a94e299 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/as_of +++ b/pkg/ccl/logictestccl/testdata/logic_test/as_of @@ -7,10 +7,17 @@ CREATE TABLE t ( j INT UNIQUE, k INT, UNIQUE (k) STORING (j) -) +); +CREATE TABLE t2 ( + i INT PRIMARY KEY, + j INT UNIQUE, + k INT, + UNIQUE (k) STORING (j) +); statement ok -INSERT INTO t VALUES (2) +INSERT INTO t VALUES (2); +INSERT INTO t2 VALUES (2); statement error pgcode 3D000 pq: database "test" does not exist SELECT * FROM t AS OF SYSTEM TIME follower_read_timestamp() @@ -134,13 +141,13 @@ statement error unimplemented: cannot use bounded staleness for queries that may SELECT * FROM t AS OF SYSTEM TIME with_min_timestamp(statement_timestamp() - '1ms') statement error unimplemented: cannot use bounded staleness for MERGE JOIN -SELECT * FROM t AS t1 JOIN t AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_max_staleness('1ms') +SELECT * FROM t AS t1 JOIN t2 AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_max_staleness('1ms') statement error unimplemented: cannot use bounded staleness for INNER JOIN -SELECT * FROM t AS t1 INNER HASH JOIN t AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_min_timestamp(statement_timestamp() - '1ms') +SELECT * FROM t AS t1 INNER HASH JOIN t2 AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_min_timestamp(statement_timestamp() - '1ms') statement error unimplemented: cannot use bounded staleness for LOOKUP JOIN -SELECT * FROM t AS t1 LEFT LOOKUP JOIN t AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_max_staleness('1ms') +SELECT * FROM t AS t1 LEFT LOOKUP JOIN t2 AS t2 ON t1.i = t2.i AS OF SYSTEM TIME with_max_staleness('1ms') statement error unimplemented: cannot use bounded staleness for UNION SELECT * FROM (SELECT * FROM t UNION SELECT * FROM t) AS OF SYSTEM TIME with_max_staleness('1ms') diff --git a/pkg/ccl/logictestccl/testdata/logic_test/multi_region_remote_access_error b/pkg/ccl/logictestccl/testdata/logic_test/multi_region_remote_access_error index 976c290cba26..c3889ed3b62f 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/multi_region_remote_access_error +++ b/pkg/ccl/logictestccl/testdata/logic_test/multi_region_remote_access_error @@ -189,6 +189,24 @@ CREATE TABLE messages_global ( INDEX msg_idx(message) ) LOCALITY GLOBAL +statement ok +CREATE TABLE messages_global_2 ( + account_id INT NOT NULL, + message_id UUID DEFAULT gen_random_uuid(), + message STRING NOT NULL, + PRIMARY KEY (account_id), + INDEX msg_idx(message) +) LOCALITY GLOBAL + +statement ok +CREATE TABLE messages_global_3 ( + account_id INT NOT NULL, + message_id UUID DEFAULT gen_random_uuid(), + message STRING NOT NULL, + PRIMARY KEY (account_id), + INDEX msg_idx(message) +) LOCALITY GLOBAL + statement ok CREATE TABLE messages_rbt ( account_id INT NOT NULL, @@ -722,13 +740,13 @@ project # A lookup join with a global table as either input should be allowed. query TTTTTT retry -SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global g2 ON g1.account_id = g2.account_id +SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global_2 g2 ON g1.account_id = g2.account_id ---- query T retry -EXPLAIN (OPT) SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global g2 ON g1.account_id = g2.account_id +EXPLAIN (OPT) SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global_2 g2 ON g1.account_id = g2.account_id ---- -inner-join (lookup messages_global [as=g2]) +inner-join (lookup messages_global_2 [as=g2]) ├── flags: force lookup join (into right side) ├── lookup columns are key ├── scan messages_global [as=g1] @@ -736,18 +754,18 @@ inner-join (lookup messages_global [as=g2]) # A join relation with local home region as the left input of lookup join should be allowed. query TTTTTTTTT retry -SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global g2 ON g1.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id +SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global_2 g2 ON g1.account_id = g2.account_id + INNER LOOKUP JOIN messages_global_3 g3 ON g2.account_id = g3.account_id ---- query T retry -EXPLAIN (OPT) SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global g2 ON g1.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id +EXPLAIN (OPT) SELECT * FROM messages_global g1 INNER LOOKUP JOIN messages_global_2 g2 ON g1.account_id = g2.account_id + INNER LOOKUP JOIN messages_global_3 g3 ON g2.account_id = g3.account_id ---- -inner-join (lookup messages_global [as=g3]) +inner-join (lookup messages_global_3 [as=g3]) ├── flags: force lookup join (into right side) ├── lookup columns are key - ├── inner-join (lookup messages_global [as=g2]) + ├── inner-join (lookup messages_global_2 [as=g2]) │ ├── flags: force lookup join (into right side) │ ├── lookup columns are key │ ├── scan messages_global [as=g1] @@ -759,29 +777,29 @@ inner-join (lookup messages_global [as=g3]) retry statement error pq: Query has no home region\. Try adding a filter on rbr\.crdb_region and/or on key column \(rbr\.account_id\)\. For more information, see https://www.cockroachlabs.com/docs/stable/cost-based-optimizer.html#control-whether-queries-are-limited-to-a-single-region SELECT * FROM messages_rbr rbr INNER LOOKUP JOIN messages_global g2 ON rbr.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id + INNER LOOKUP JOIN messages_global_2 g3 ON g2.account_id = g3.account_id # The explicit REGIONAL BY ROW AS column name should be used in the error # message if it differs from the default crdb_region. retry statement error pq: Query has no home region\. Try adding a filter on rbr\.crdb_region_alt and/or on key column \(rbr\.account_id\)\. For more information, see https://www.cockroachlabs.com/docs/stable/cost-based-optimizer.html#control-whether-queries-are-limited-to-a-single-region SELECT * FROM messages_rbr_alt rbr INNER LOOKUP JOIN messages_global g2 ON rbr.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id + INNER LOOKUP JOIN messages_global_2 g3 ON g2.account_id = g3.account_id # A lookup join relation with a left input join relation which uses locality # optimized scan in one of the tables of the lookup join should be allowed. query TTTTTTTTT retry SELECT * FROM (SELECT * FROM messages_rbr LIMIT 1) rbr INNER LOOKUP JOIN messages_global g2 ON rbr.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id + INNER LOOKUP JOIN messages_global_2 g3 ON g2.account_id = g3.account_id ---- query T retry EXPLAIN (OPT) SELECT * FROM (SELECT * FROM messages_rbr LIMIT 1) rbr INNER LOOKUP JOIN messages_global g2 ON rbr.account_id = g2.account_id - INNER LOOKUP JOIN messages_global g3 ON g2.account_id = g3.account_id + INNER LOOKUP JOIN messages_global_2 g3 ON g2.account_id = g3.account_id ---- -inner-join (lookup messages_global [as=g3]) +inner-join (lookup messages_global_2 [as=g3]) ├── flags: force lookup join (into right side) ├── lookup columns are key ├── inner-join (lookup messages_global [as=g2]) diff --git a/pkg/ccl/partitionccl/scrub_test.go b/pkg/ccl/partitionccl/scrub_test.go index 2dc91f181ee1..5a0437524ec8 100644 --- a/pkg/ccl/partitionccl/scrub_test.go +++ b/pkg/ccl/partitionccl/scrub_test.go @@ -234,11 +234,18 @@ INSERT INTO db.t VALUES (1, 3), (2, 4); t.Fatalf("expected 1 index entry, got %d", len(primaryIndexKey)) } - // Add the primary key via the KV API. + // Add the primary key via the KV API. This will overwrite the old primary + // index KV, so no need to perform a Del. if err := kvDB.Put(context.Background(), primaryIndexKey[0].Key, &primaryIndexKey[0].Value); err != nil { t.Fatalf("unexpected error: %s", err) } + oldValues := []tree.Datum{tree.NewDInt(1), tree.NewDInt(3)} secondaryIndex := tableDesc.PublicNonPrimaryIndexes()[0] + secondaryIndexDelKey, err := rowenc.EncodeSecondaryIndex( + codec, tableDesc, secondaryIndex, colIDtoRowIndex, oldValues, true /* includeEmpty */) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } secondaryIndexKey, err := rowenc.EncodeSecondaryIndex( codec, tableDesc, secondaryIndex, colIDtoRowIndex, values, true /* includeEmpty */) if err != nil { @@ -247,6 +254,10 @@ INSERT INTO db.t VALUES (1, 3), (2, 4); if len(secondaryIndexKey) != 1 { t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndexKey), secondaryIndexKey) } + // Delete the old secondary index KV before inserting the new one. + if _, err := kvDB.Del(context.Background(), secondaryIndexDelKey[0].Key); err != nil { + t.Fatalf("unexpected error: %s", err) + } if err := kvDB.Put(context.Background(), secondaryIndexKey[0].Key, &secondaryIndexKey[0].Value); err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/pkg/sql/colflow/vectorized_flow_planning_test.go b/pkg/sql/colflow/vectorized_flow_planning_test.go index cfd900838c61..e3b0bba531a1 100644 --- a/pkg/sql/colflow/vectorized_flow_planning_test.go +++ b/pkg/sql/colflow/vectorized_flow_planning_test.go @@ -47,9 +47,9 @@ func TestVectorizedPlanning(t *testing.T) { // Check that there is no columnarizer-materializer pair on top of the // root of the execution tree if the root is a wrapped row-execution // processor. - _, err = conn.ExecContext(ctx, `CREATE TABLE t (id INT PRIMARY KEY)`) + _, err = conn.ExecContext(ctx, `CREATE TABLE t (id INT PRIMARY KEY, val INT)`) require.NoError(t, err) - rows, err := conn.QueryContext(ctx, `EXPLAIN (VEC, VERBOSE) SELECT * FROM t AS t1 INNER LOOKUP JOIN t AS t2 ON t1.id = t2.id`) + rows, err := conn.QueryContext(ctx, `EXPLAIN (VEC, VERBOSE) SELECT * FROM t AS t1 INNER LOOKUP JOIN t AS t2 ON t1.val = t2.id`) require.NoError(t, err) expectedOutput := []string{ "│", 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 52cd6b4cf61d..193a5d5264a3 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/join +++ b/pkg/sql/opt/exec/execbuilder/testdata/join @@ -543,18 +543,11 @@ EXPLAIN SELECT * FROM cards LEFT OUTER JOIN customers ON customers.id = cards.cu distribution: local vectorized: true · -• merge join -│ equality: (cust) = (id) -│ right cols are key -│ -├── • scan -│ missing stats -│ table: cards@cards_cust_idx -│ spans: FULL SCAN +• render │ └── • scan missing stats - table: customers@customers_pkey + table: cards@cards_pkey spans: FULL SCAN # Tests for filter propagation through joins. @@ -946,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 @@ -2220,16 +2211,9 @@ EXPLAIN SELECT * FROM cards LEFT OUTER HASH JOIN customers ON customers.id = car distribution: local vectorized: true · -• hash join -│ equality: (cust) = (id) -│ right cols are key -│ -├── • scan -│ missing stats -│ table: cards@cards_pkey -│ spans: FULL SCAN +• render │ └── • scan missing stats - table: customers@customers_pkey + table: cards@cards_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/subquery b/pkg/sql/opt/exec/execbuilder/testdata/subquery index 73d8219b533f..05bf469c2493 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/subquery +++ b/pkg/sql/opt/exec/execbuilder/testdata/subquery @@ -4,7 +4,8 @@ # Uncorrelated subqueries. # ------------------------------------------------------------------------------ statement ok -CREATE TABLE abc (a INT PRIMARY KEY, b INT, c INT) +CREATE TABLE abc (a INT PRIMARY KEY, b INT, c INT); +CREATE TABLE abc2 (a INT PRIMARY KEY, b INT, c INT) query T EXPLAIN ALTER TABLE abc SPLIT AT VALUES ((SELECT 42)) @@ -179,7 +180,7 @@ vectorized: true # IN expression transformed into semi-join. query T -EXPLAIN (VERBOSE) SELECT a FROM abc WHERE a IN (SELECT a FROM abc WHERE b < 0) +EXPLAIN (VERBOSE) SELECT a FROM abc WHERE a IN (SELECT a FROM abc2 WHERE b < 0) ---- distribution: local vectorized: true @@ -209,7 +210,7 @@ vectorized: true columns: (a, b) ordering: +a estimated row count: 1,000 (missing stats) - table: abc@abc_pkey + table: abc2@abc2_pkey spans: FULL SCAN query T @@ -507,11 +508,11 @@ query T kvtrace SELECT k, i, CASE WHEN k > 1 THEN (SELECT i FROM corr tmp WHERE k = corr.k-1) ELSE 0 END AS prev_i FROM corr ---- -Scan /Table/110/{1-2} -Scan /Table/110/1/1/0 -Scan /Table/110/1/2/0 -Scan /Table/110/1/3/0 -Scan /Table/110/1/4/0 +Scan /Table/111/{1-2} +Scan /Table/111/1/1/0 +Scan /Table/111/1/2/0 +Scan /Table/111/1/3/0 +Scan /Table/111/1/4/0 # Case where the EXISTS subquery in a filter cannot be hoisted into an # apply-join. 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/exec/execbuilder/testdata/upsert b/pkg/sql/opt/exec/execbuilder/testdata/upsert index 1d3251050363..f605b19af0d8 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/upsert +++ b/pkg/sql/opt/exec/execbuilder/testdata/upsert @@ -107,30 +107,24 @@ vectorized: true │ auto commit │ arbiter indexes: kv_pkey │ -└── • lookup join (inner) +└── • render │ columns: (k, v_default, k) - │ estimated row count: 2 (missing stats) - │ table: kv@kv_pkey - │ equality: (k) = (k) - │ equality cols are key + │ render k: k + │ render v_default: CAST(NULL AS INT8) + │ render k: k │ - └── • render - │ columns: (v_default, k) - │ render v_default: CAST(NULL AS INT8) - │ render k: k + └── • top-k + │ columns: (k, v) + │ ordering: -v + │ estimated row count: 2 (missing stats) + │ order: -v + │ k: 2 │ - └── • top-k - │ columns: (k, v) - │ ordering: -v - │ estimated row count: 2 (missing stats) - │ order: -v - │ k: 2 - │ - └── • scan - columns: (k, v) - estimated row count: 1,000 (missing stats) - table: kv@kv_pkey - spans: FULL SCAN + └── • scan + columns: (k, v) + estimated row count: 1,000 (missing stats) + table: kv@kv_pkey + spans: FULL SCAN # Use Upsert with indexed table, default columns, computed columns, and check # columns. 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 ef5e5cacc0d2..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) @@ -2427,11 +2431,11 @@ INNER JOIN (SELECT xysd.x, a.x AS t FROM xysd INNER JOIN xysd AS a ON xysd.x = a ---- inner-join (hash) ├── columns: k:1(int!null) v:2(int) r1:3(int!null) r2:4(int) x:7(int!null) t:13(int!null) - ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) + ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) ├── key: (1) ├── fd: (1)-->(2-4), (7)==(3,13), (13)==(3,7), (3)==(7,13) - ├── prune: (1,2,4) - ├── interesting orderings: (+1) (+7) (+13) + ├── prune: (1,2,4,7) + ├── interesting orderings: (+1) (+(7|13)) ├── scan fk │ ├── columns: k:1(int!null) v:2(int) r1:3(int!null) r2:4(int) │ ├── key: (1) @@ -2439,29 +2443,21 @@ inner-join (hash) │ ├── prune: (1-4) │ ├── interesting orderings: (+1) │ └── unfiltered-cols: (1-6) - ├── inner-join (hash) - │ ├── columns: xysd.x:7(int!null) a.x:13(int!null) - │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ ├── key: (13) + ├── project + │ ├── columns: a.x:13(int!null) xysd.x:7(int!null) + │ ├── key: (7) │ ├── fd: (7)==(13), (13)==(7) - │ ├── interesting orderings: (+7) (+13) - │ ├── unfiltered-cols: (7-18) + │ ├── prune: (7,13) + │ ├── interesting orderings: (+(7|13)) + │ ├── unfiltered-cols: (7-12) │ ├── scan xysd │ │ ├── columns: xysd.x:7(int!null) │ │ ├── key: (7) │ │ ├── prune: (7) │ │ ├── interesting orderings: (+7) │ │ └── unfiltered-cols: (7-12) - │ ├── scan xysd [as=a] - │ │ ├── columns: a.x:13(int!null) - │ │ ├── key: (13) - │ │ ├── prune: (13) - │ │ ├── interesting orderings: (+13) - │ │ └── unfiltered-cols: (13-18) - │ └── filters - │ └── eq [type=bool, outer=(7,13), constraints=(/7: (/NULL - ]; /13: (/NULL - ]), fd=(7)==(13), (13)==(7)] - │ ├── variable: xysd.x:7 [type=int] - │ └── variable: a.x:13 [type=int] + │ └── projections + │ └── variable: xysd.x:7 [as=a.x:13, type=int, outer=(7)] └── filters └── eq [type=bool, outer=(3,13), constraints=(/3: (/NULL - ]; /13: (/NULL - ]), fd=(3)==(13), (13)==(3)] ├── variable: r1:3 [type=int] @@ -2683,11 +2679,10 @@ FROM (SELECT r1, r2, r3 FROM ref WHERE r2 IS NOT NULL) INNER JOIN abc ON (r1, r2, r3) = (a, b, c) ---- -inner-join (hash) +project ├── columns: r1:1(int!null) r2:2(int!null) r3:3(int!null) a:7(int!null) b:8(int!null) c:9(int!null) - ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) ├── fd: (1)==(7), (7)==(1), (2)==(8), (8)==(2), (3)==(9), (9)==(3) - ├── interesting orderings: (+7,+8,+9) + ├── prune: (1-3,7-9) ├── select │ ├── columns: r1:1(int!null) r2:2(int!null) r3:3(int!null) │ ├── prune: (1,3) @@ -2698,22 +2693,10 @@ inner-join (hash) │ └── is-not [type=bool, outer=(2), constraints=(/2: (/NULL - ]; tight)] │ ├── variable: r2:2 [type=int] │ └── null [type=unknown] - ├── scan abc - │ ├── columns: a:7(int!null) b:8(int!null) c:9(int!null) - │ ├── key: (7-9) - │ ├── prune: (7-9) - │ ├── interesting orderings: (+7,+8,+9) - │ └── unfiltered-cols: (7-11) - └── filters - ├── eq [type=bool, outer=(1,7), constraints=(/1: (/NULL - ]; /7: (/NULL - ]), fd=(1)==(7), (7)==(1)] - │ ├── variable: r1:1 [type=int] - │ └── variable: a:7 [type=int] - ├── eq [type=bool, outer=(2,8), constraints=(/2: (/NULL - ]; /8: (/NULL - ]), fd=(2)==(8), (8)==(2)] - │ ├── variable: r2:2 [type=int] - │ └── variable: b:8 [type=int] - └── eq [type=bool, outer=(3,9), constraints=(/3: (/NULL - ]; /9: (/NULL - ]), fd=(3)==(9), (9)==(3)] - ├── variable: r3:3 [type=int] - └── variable: c:9 [type=int] + └── projections + ├── variable: r1:1 [as=a:7, type=int, outer=(1)] + ├── variable: r2:2 [as=b:8, type=int, outer=(2)] + └── variable: r3:3 [as=c:9, type=int, outer=(3)] # Case with a not-null multi-column foreign key and an equality on only one of # the foreign key columns. @@ -2796,15 +2779,15 @@ limit ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) st_id:7(int!null) tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) ├── cardinality: [0 - 50] ├── fd: (13)-->(14,15), (15)==(18), (18)==(15), (1)==(7), (7)==(1), (2)==(10), (10)==(2), (3)==(13), (13)==(3) - ├── prune: (14) - ├── interesting orderings: (+7) (+10) (+13) (+18) + ├── prune: (14,15,18) + ├── interesting orderings: (+7) (+10) (+13) ├── inner-join (hash) │ ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) st_id:7(int!null) tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15), (1)==(7), (7)==(1), (2)==(10), (10)==(2), (3)==(13), (13)==(3) │ ├── limit hint: 50.00 - │ ├── prune: (14) - │ ├── interesting orderings: (+7) (+10) (+13) (+18) + │ ├── prune: (14,15,18) + │ ├── interesting orderings: (+7) (+10) (+13) │ ├── scan trade │ │ ├── columns: t_st_id:1(int!null) t_tt_id:2(int!null) t_s_symb:3(int!null) │ │ ├── prune: (1-3) @@ -2814,8 +2797,8 @@ limit │ │ ├── multiplicity: left-rows(zero-or-more), right-rows(one-or-more) │ │ ├── key: (7,10,13) │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ ├── prune: (7,10,13,14) - │ │ ├── interesting orderings: (+7) (+10) (+13) (+18) + │ │ ├── prune: (7,10,13-15,18) + │ │ ├── interesting orderings: (+7) (+10) (+13) │ │ ├── scan status_type │ │ │ ├── columns: st_id:7(int!null) │ │ │ ├── key: (7) @@ -2826,21 +2809,20 @@ limit │ │ │ ├── columns: tt_id:10(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) │ │ │ ├── key: (10,13) │ │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ │ ├── prune: (10,13,14) - │ │ │ ├── interesting orderings: (+10) (+13) (+18) + │ │ │ ├── prune: (10,13-15,18) + │ │ │ ├── interesting orderings: (+10) (+13) │ │ │ ├── scan trade_type │ │ │ │ ├── columns: tt_id:10(int!null) │ │ │ │ ├── key: (10) │ │ │ │ ├── prune: (10) │ │ │ │ ├── interesting orderings: (+10) │ │ │ │ └── unfiltered-cols: (10-12) - │ │ │ ├── inner-join (hash) - │ │ │ │ ├── columns: s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) ex_id:18(int!null) - │ │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-more) + │ │ │ ├── project + │ │ │ │ ├── columns: ex_id:18(int!null) s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) │ │ │ │ ├── key: (13) │ │ │ │ ├── fd: (13)-->(14,15), (15)==(18), (18)==(15) - │ │ │ │ ├── prune: (13,14) - │ │ │ │ ├── interesting orderings: (+13) (+18) + │ │ │ │ ├── prune: (13-15,18) + │ │ │ │ ├── interesting orderings: (+13) │ │ │ │ ├── unfiltered-cols: (13-17) │ │ │ │ ├── scan security │ │ │ │ │ ├── columns: s_symb:13(int!null) s_st_id:14(int!null) s_ex_id:15(int!null) @@ -2849,16 +2831,8 @@ limit │ │ │ │ │ ├── prune: (13-15) │ │ │ │ │ ├── interesting orderings: (+13) │ │ │ │ │ └── unfiltered-cols: (13-17) - │ │ │ │ ├── scan exchange - │ │ │ │ │ ├── columns: ex_id:18(int!null) - │ │ │ │ │ ├── key: (18) - │ │ │ │ │ ├── prune: (18) - │ │ │ │ │ ├── interesting orderings: (+18) - │ │ │ │ │ └── unfiltered-cols: (18-20) - │ │ │ │ └── filters - │ │ │ │ └── eq [type=bool, outer=(15,18), constraints=(/15: (/NULL - ]; /18: (/NULL - ]), fd=(15)==(18), (18)==(15)] - │ │ │ │ ├── variable: ex_id:18 [type=int] - │ │ │ │ └── variable: s_ex_id:15 [type=int] + │ │ │ │ └── projections + │ │ │ │ └── variable: s_ex_id:15 [as=ex_id:18, type=int, outer=(15)] │ │ │ └── filters (true) │ │ └── filters (true) │ └── filters @@ -2913,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/general_funcs.go b/pkg/sql/opt/norm/general_funcs.go index 26be490cc06d..83a59008bd73 100644 --- a/pkg/sql/opt/norm/general_funcs.go +++ b/pkg/sql/opt/norm/general_funcs.go @@ -305,6 +305,18 @@ func (c *CustomFuncs) MakeBoolCol() opt.ColumnID { return c.mem.Metadata().AddColumn("", types.Bool) } +// CanRemapCols returns true if it's possible to remap every column in the +// "from" set to a column in the "to" set using the given FDs. +func (c *CustomFuncs) CanRemapCols(from, to opt.ColSet, fds *props.FuncDepSet) bool { + for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) { + if !fds.ComputeEquivGroup(col).Intersects(to) { + // It is not possible to remap this column to one from the "to" set. + return false + } + } + return true +} + // ---------------------------------------------------------------------- // // Outer column functions @@ -575,6 +587,11 @@ func (c *CustomFuncs) sharedProps(e opt.Expr) *props.Shared { } } +// FuncDeps retrieves the FuncDepSet for the given expression. +func (c *CustomFuncs) FuncDeps(expr memo.RelExpr) *props.FuncDepSet { + return &expr.Relational().FuncDeps +} + // ---------------------------------------------------------------------- // // Ordering functions diff --git a/pkg/sql/opt/norm/project_funcs.go b/pkg/sql/opt/norm/project_funcs.go index ae32774e7324..12a71fe84ddf 100644 --- a/pkg/sql/opt/norm/project_funcs.go +++ b/pkg/sql/opt/norm/project_funcs.go @@ -15,6 +15,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" + "github.com/cockroachdb/cockroach/pkg/sql/opt/props" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/intsets" @@ -849,3 +850,57 @@ func (c *CustomFuncs) IsStaticTuple(expr opt.ScalarExpr) bool { } return false } + +// ProjectRemappedCols creates a projection for each column in the "from" set +// that is not in the "to" set, mapping it to an equivalent column in the "to" +// set. ProjectRemappedCols panics if this is not possible. +func (c *CustomFuncs) ProjectRemappedCols( + from, to opt.ColSet, fds *props.FuncDepSet, +) (projections memo.ProjectionsExpr) { + for col, ok := from.Next(0); ok; col, ok = from.Next(col + 1) { + if !to.Contains(col) { + // TODO(mgartner): We don't need to compute the entire equivalence group, + // we only need to find the first equivalent column that's in the to set. + candidates := fds.ComputeEquivGroup(col) + candidates.IntersectionWith(to) + if candidates.Empty() { + panic(errors.AssertionFailedf("cannot remap column %v", col)) + } + toCol, _ := candidates.Next(0) + projections = append( + projections, + c.f.ConstructProjectionsItem(c.f.ConstructVariable(toCol), col), + ) + } + } + return projections +} + +// RemapProjectionCols remaps column references in the given projections to +// refer to the "to" set. +func (c *CustomFuncs) RemapProjectionCols( + projections memo.ProjectionsExpr, to opt.ColSet, fds *props.FuncDepSet, +) memo.ProjectionsExpr { + getReplacement := func(col opt.ColumnID) opt.ColumnID { + // TODO(mgartner): We don't need to compute the entire equivalence group, we + // only need to find the first equivalent column that's in the to set. + candidates := fds.ComputeEquivGroup(col) + candidates.IntersectionWith(to) + if candidates.Empty() { + panic(errors.AssertionFailedf("cannot remap column")) + } + replacement, _ := candidates.Next(0) + return replacement + } + + // Replace any references to the "from" columns in the projections. + var replace ReplaceFunc + replace = func(e opt.Expr) opt.Expr { + if v, ok := e.(*memo.VariableExpr); ok && !to.Contains(v.Col) { + // This variable needs to be remapped. + return c.f.ConstructVariable(getReplacement(v.Col)) + } + return c.f.Replace(e, replace) + } + return *(replace(&projections).(*memo.ProjectionsExpr)) +} diff --git a/pkg/sql/opt/norm/rules/groupby.opt b/pkg/sql/opt/norm/rules/groupby.opt index 582ba75cf919..2bdd2cddf9fe 100644 --- a/pkg/sql/opt/norm/rules/groupby.opt +++ b/pkg/sql/opt/norm/rules/groupby.opt @@ -58,6 +58,10 @@ # EliminateJoinUnderGroupByLeft should stay at the top of the file so that it # has a chance to fire before rules like EliminateDistinctOn that might prevent # matching. +# +# Similar to the join-elimination rules that match on Project operators, +# EliminateJoinUnderGroupByLeft can remap references to the right input of the +# join to refer to equivalent columns on the left input. [EliminateJoinUnderGroupByLeft, Normalize] (GroupBy | ScalarGroupBy | DistinctOn $input:(InnerJoin | LeftJoin $left:*) @@ -67,18 +71,23 @@ $ordering $leftCols:(OutputCols $left) ) & - (ColsAreSubset - (UnionCols + (CanRemapCols + $toRemap:(UnionCols $groupingCols (AggregationOuterCols $aggs) ) $leftCols + $fds:(FuncDeps $input) ) & (CanEliminateJoinUnderGroupByLeft $input $aggs) ) => ((OpName) - $left + (Project + $left + (ProjectRemappedCols $toRemap $leftCols $fds) + $leftCols + ) $aggs (MakeGrouping $groupingCols @@ -97,18 +106,23 @@ $ordering $rightCols:(OutputCols $right) ) & - (ColsAreSubset - (UnionCols + (CanRemapCols + $toRemap:(UnionCols $groupingCols (AggregationOuterCols $aggs) ) $rightCols + $fds:(FuncDeps $input) ) & (CanEliminateJoinUnderGroupByRight $input $aggs) ) => ((OpName) - $right + (Project + $right + (ProjectRemappedCols $toRemap $rightCols $fds) + $rightCols + ) $aggs (MakeGrouping $groupingCols diff --git a/pkg/sql/opt/norm/rules/project.opt b/pkg/sql/opt/norm/rules/project.opt index 7f102bc6db01..08861772f550 100644 --- a/pkg/sql/opt/norm/rules/project.opt +++ b/pkg/sql/opt/norm/rules/project.opt @@ -10,20 +10,41 @@ # # Note: EliminateJoinUnderProjectLeft should stay above EliminateProject so that # it has a chance to fire before the Project can be removed. +# +# It is possible for references to the right input of the join to be replaced by +# equivalent columns from the left input. This is handled by adding projections +# that map the left column to the equivalent right column (leftCol AS rightCol). +# +# It is ok to call MergeProjections without checking CanMergeProjections because +# RemapProjectionCols returns projections that only reference columns from the +# left input, and ProjectRemappedCols only projects columns from the right side +# of the join. [EliminateJoinUnderProjectLeft, Normalize] (Project $join:(InnerJoin | LeftJoin $left:* $right:*) & (JoinDoesNotDuplicateLeftRows $join) & (JoinPreservesLeftRows $join) - $projections:* & - ^(AreProjectionsCorrelated - $projections - $rightCols:(OutputCols $right) + $projections:* + $passthrough:* & + (CanRemapCols + (UnionCols + $passthrough + (ProjectionOuterCols $projections) + ) + $leftCols:(OutputCols $left) + $fds:(FuncDeps $join) ) - $passthrough:* & ^(ColsIntersect $passthrough $rightCols) ) => -(Project $left $projections $passthrough) +(Project + $left + (MergeProjections + (RemapProjectionCols $projections $leftCols $fds) + (ProjectRemappedCols $passthrough $leftCols $fds) + $passthrough + ) + (DifferenceCols $passthrough (OutputCols $right)) +) # EliminateJoinUnderProjectRight mirrors EliminateJoinUnderProjectLeft, except # that it only matches InnerJoins. @@ -32,15 +53,27 @@ $join:(InnerJoin $left:* $right:*) & (JoinDoesNotDuplicateRightRows $join) & (JoinPreservesRightRows $join) - $projections:* & - ^(AreProjectionsCorrelated - $projections - $leftCols:(OutputCols $left) + $projections:* + $passthrough:* & + (CanRemapCols + (UnionCols + $passthrough + (ProjectionOuterCols $projections) + ) + $rightCols:(OutputCols $right) + $fds:(FuncDeps $join) ) - $passthrough:* & ^(ColsIntersect $passthrough $leftCols) ) => -(Project $right $projections $passthrough) +(Project + $right + (MergeProjections + (RemapProjectionCols $projections $rightCols $fds) + (ProjectRemappedCols $passthrough $rightCols $fds) + $passthrough + ) + (DifferenceCols $passthrough (OutputCols $left)) +) # EliminateProject discards a Project operator which is not adding or removing # columns. 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 ce57a23bcb20..eec07141c019 100644 --- a/pkg/sql/opt/norm/testdata/rules/project +++ b/pkg/sql/opt/norm/testdata/rules/project @@ -113,6 +113,21 @@ project └── projections └── 1 [as="?column?":12] +# The right column can be remapped to a left column. +norm expect=EliminateJoinUnderProjectLeft +SELECT b.j, b1.x FROM b INNER JOIN b AS b1 ON b.x = b1.x +---- +project + ├── columns: j:3 x:6!null + ├── key: (6) + ├── fd: (6)-->(3) + ├── scan b + │ ├── columns: b.x:1!null b.j:3 + │ ├── key: (1) + │ └── fd: (1)-->(3) + └── projections + └── b.x:1 [as=b1.x:6, outer=(1)] + # No-op case because the cross join may duplicate left rows. norm expect-not=EliminateJoinUnderProjectLeft SELECT 1 FROM b LEFT JOIN a ON True @@ -219,19 +234,43 @@ project └── filters └── x:1 = r1:10 [outer=(1,10), constraints=(/1: (/NULL - ]; /10: (/NULL - ]), fd=(1)==(10), (10)==(1)] +# Regression test for #102614 - parent.id should be mapped to child.id to allow +# join elimination to proceed. +exec-ddl +CREATE TABLE parent ( + id INT PRIMARY KEY +) +---- + +exec-ddl +CREATE TABLE child ( + id INT PRIMARY KEY, + parent_id INT NOT NULL REFERENCES parent(id) +) +---- + +opt +SELECT + id, + (SELECT child.parent_id FROM parent WHERE id = child.parent_id) +FROM child +---- +project + ├── columns: id:1!null parent_id:9!null + ├── key: (1) + ├── fd: (1)-->(9) + ├── scan child + │ ├── columns: child.id:1!null child.parent_id:2!null + │ ├── key: (1) + │ └── fd: (1)-->(2) + └── projections + └── child.parent_id:2 [as=parent_id:9, outer=(2)] + # -------------------------------------------------- # 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 @@ -241,24 +280,43 @@ scan fks ├── key: (7) └── fd: (7)-->(8) -# No-op case because columns from the right side of a LeftJoin are being -# projected. +# The left column can be remapped to a right column. +norm expect=EliminateJoinUnderProjectRight +SELECT x, k, v FROM a INNER JOIN fks ON r1 = x +---- +project + ├── 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 + └── r1:10 [as=x:1, outer=(10)] + +# No-op case because the left input of a LeftJoin cannot be removed. norm expect-not=EliminateJoinUnderProjectRight -SELECT b.x, b1.x FROM b LEFT JOIN b AS b1 ON b.x = b1.x +SELECT x, k, v FROM a LEFT JOIN fks ON r1 = x ---- -inner-join (hash) - ├── columns: x:1!null x:6!null - ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - ├── key: (6) - ├── fd: (1)==(6), (6)==(1) - ├── scan b - │ ├── columns: b.x:1!null - │ └── key: (1) - ├── scan b [as=b1] - │ ├── columns: b1.x:6!null - │ └── key: (6) - └── filters - └── b.x:1 = b1.x:6 [outer=(1,6), constraints=(/1: (/NULL - ]; /6: (/NULL - ]), fd=(1)==(6), (6)==(1)] +project + ├── 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 + └── 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 44f3154aef95..f4608eeade07 100644 --- a/pkg/sql/opt/norm/testdata/rules/prune_cols +++ b/pkg/sql/opt/norm/testdata/rules/prune_cols @@ -5296,7 +5296,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) +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/norm/testdata/rules/scalar b/pkg/sql/opt/norm/testdata/rules/scalar index 8d710fe008a8..6b56dded28d8 100644 --- a/pkg/sql/opt/norm/testdata/rules/scalar +++ b/pkg/sql/opt/norm/testdata/rules/scalar @@ -1747,21 +1747,17 @@ project ├── group-by (hash) │ ├── columns: b.k:1!null a.k:8!null array_agg:17!null │ ├── grouping columns: b.k:1!null - │ ├── key: (8) - │ ├── fd: (1)==(8), (8)==(1), (8)-->(17), (1)-->(8,17) - │ ├── inner-join (hash) - │ │ ├── columns: b.k:1!null a.k:8!null - │ │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ │ ├── key: (8) + │ ├── key: (1) + │ ├── fd: (1)==(8), (8)==(1), (1)-->(8,17) + │ ├── project + │ │ ├── columns: a.k:8!null b.k:1!null + │ │ ├── key: (1) │ │ ├── fd: (1)==(8), (8)==(1) │ │ ├── scan a [as=b] │ │ │ ├── columns: b.k:1!null │ │ │ └── key: (1) - │ │ ├── scan a - │ │ │ ├── columns: a.k:8!null - │ │ │ └── key: (8) - │ │ └── filters - │ │ └── a.k:8 = b.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + │ │ └── projections + │ │ └── b.k:1 [as=a.k:8, outer=(1)] │ └── aggregations │ ├── array-agg [as=array_agg:17, outer=(8)] │ │ └── a.k:8 @@ -1869,31 +1865,22 @@ project │ ├── grouping columns: c.k:1!null │ ├── key: (1) │ ├── fd: ()-->(25), (1)-->(25,26) - │ ├── inner-join (hash) - │ │ ├── columns: c.k:1!null b.k:8!null array:22 canary:25!null - │ │ ├── multiplicity: left-rows(exactly-one), right-rows(exactly-one) - │ │ ├── key: (8) - │ │ ├── fd: ()-->(25), (8)-->(22), (1)==(8), (8)==(1) - │ │ ├── scan a [as=c] - │ │ │ ├── columns: c.k:1!null - │ │ │ └── key: (1) - │ │ ├── project - │ │ │ ├── columns: canary:25!null array:22 b.k:8!null - │ │ │ ├── key: (8) - │ │ │ ├── fd: ()-->(25), (8)-->(22) - │ │ │ ├── scan a [as=b] - │ │ │ │ ├── columns: b.k:8!null - │ │ │ │ └── key: (8) - │ │ │ └── projections - │ │ │ ├── true [as=canary:25] - │ │ │ └── indirection [as=array:22, subquery] - │ │ │ ├── array-flatten - │ │ │ │ └── scan a - │ │ │ │ ├── columns: k:15!null - │ │ │ │ └── key: (15) - │ │ │ └── 1 - │ │ └── filters - │ │ └── b.k:8 = c.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] + │ ├── project + │ │ ├── columns: c.k:1!null canary:25!null array:22 + │ │ ├── key: (1,22) + │ │ ├── fd: ()-->(25) + │ │ ├── scan a [as=b] + │ │ │ ├── columns: b.k:8!null + │ │ │ └── key: (8) + │ │ └── projections + │ │ ├── b.k:8 [as=c.k:1, outer=(8)] + │ │ ├── true [as=canary:25] + │ │ └── indirection [as=array:22, subquery] + │ │ ├── array-flatten + │ │ │ └── scan a + │ │ │ ├── columns: k:15!null + │ │ │ └── key: (15) + │ │ └── 1 │ └── aggregations │ ├── array-agg [as=array_agg:26, outer=(22)] │ │ └── array:22 @@ -1932,13 +1919,12 @@ project │ │ │ │ ├── cardinality: [0 - 1] │ │ │ │ ├── key: () │ │ │ │ ├── fd: ()-->(15,24) - │ │ │ │ ├── inner-join (hash) - │ │ │ │ │ ├── columns: b.k:8!null a.k:15!null + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: a.k:15!null │ │ │ │ │ ├── outer: (1) │ │ │ │ │ ├── cardinality: [0 - 1] - │ │ │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) │ │ │ │ │ ├── key: () - │ │ │ │ │ ├── fd: ()-->(8,15), (15)==(8), (8)==(15) + │ │ │ │ │ ├── fd: ()-->(15) │ │ │ │ │ ├── select │ │ │ │ │ │ ├── columns: b.k:8!null │ │ │ │ │ │ ├── outer: (1) @@ -1950,11 +1936,8 @@ project │ │ │ │ │ │ │ └── key: (8) │ │ │ │ │ │ └── filters │ │ │ │ │ │ └── b.k:8 = c.k:1 [outer=(1,8), constraints=(/1: (/NULL - ]; /8: (/NULL - ]), fd=(1)==(8), (8)==(1)] - │ │ │ │ │ ├── scan a - │ │ │ │ │ │ ├── columns: a.k:15!null - │ │ │ │ │ │ └── key: (15) - │ │ │ │ │ └── filters - │ │ │ │ │ └── a.k:15 = b.k:8 [outer=(8,15), constraints=(/8: (/NULL - ]; /15: (/NULL - ]), fd=(8)==(15), (15)==(8)] + │ │ │ │ │ └── projections + │ │ │ │ │ └── b.k:8 [as=a.k:15, outer=(8)] │ │ │ │ └── aggregations │ │ │ │ ├── array-agg [as=array_agg:24, outer=(15)] │ │ │ │ │ └── a.k:15 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/testutils/opttester/forcing_opt.go b/pkg/sql/opt/testutils/opttester/forcing_opt.go index 7ff4b7023385..1e591093d4ff 100644 --- a/pkg/sql/opt/testutils/opttester/forcing_opt.go +++ b/pkg/sql/opt/testutils/opttester/forcing_opt.go @@ -73,15 +73,15 @@ func newForcingOptimizer( fo.o.Factory().SetDisabledRules(tester.Flags.DisableRules) fo.o.NotifyOnMatchedRule(func(ruleName opt.RuleName) bool { + if tester.Flags.DisableRules.Contains(int(ruleName)) { + return false + } if ignoreNormRules && ruleName.IsNormalize() { return true } if fo.remaining == 0 { return false } - if tester.Flags.DisableRules.Contains(int(ruleName)) { - return false - } fo.remaining-- fo.lastMatched = ruleName return true diff --git a/pkg/sql/opt/xform/testdata/external/customer b/pkg/sql/opt/xform/testdata/external/customer index c98bba84bb14..d228f6d65abd 100644 --- a/pkg/sql/opt/xform/testdata/external/customer +++ b/pkg/sql/opt/xform/testdata/external/customer @@ -33,7 +33,7 @@ CREATE TABLE edges ( ) ---- -opt +opt disable=EliminateJoinUnderProjectRight select nodes.id,dst from nodes join edges on edges.dst=nodes.id ---- inner-join (merge) @@ -50,6 +50,18 @@ inner-join (merge) │ └── ordering: +6 └── filters (true) +# The join is actually not necessary because of the foreign-key relation. +opt +select nodes.id,dst from nodes join edges on edges.dst=nodes.id +---- +project + ├── columns: id:1!null dst:6!null + ├── fd: (1)==(6), (6)==(1) + ├── scan edges@edges_auto_index_fk_dst_ref_nodes + │ └── columns: dst:6!null + └── projections + └── dst:6 [as=id:1, outer=(6)] + # ------------------------------------------------------------------------------ # Github Issues 16313/16426: Ensure that STORING index is used to filter unread # articles before index joining to the primary index. 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/external/trading b/pkg/sql/opt/xform/testdata/external/trading index 5ad353b93d1a..675e24234807 100644 --- a/pkg/sql/opt/xform/testdata/external/trading +++ b/pkg/sql/opt/xform/testdata/external/trading @@ -964,11 +964,11 @@ sort │ ├── stable │ ├── stats: [rows=139576.9, distinct(45)=82395.9, null(45)=0] │ ├── inner-join (hash) - │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null id:20!null cardsinfo.dealerid:28!null cardsinfo.cardid:29!null + │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null id:20!null │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ │ ├── stats: [rows=139576.9, distinct(3)=82395.9, null(3)=0, distinct(4)=37420.4, null(4)=0, distinct(20)=37420.4, null(20)=0] - │ │ ├── key: (5,13,29) - │ │ ├── fd: ()-->(1,2,11,12,28), (3-5)-->(6,7), (13)-->(14,15), (3)==(13), (13)==(3), (20)==(4,29), (29)==(4,20), (4)==(20,29) + │ │ ├── key: (5,13,20) + │ │ ├── fd: ()-->(1,2,11,12), (3-5)-->(6,7), (13)-->(14,15), (3)==(13), (13)==(3), (4)==(20), (20)==(4) │ │ ├── inner-join (merge) │ │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:11!null transactions.isbuy:12!null date:13!null accountname:14!null customername:15!null │ │ │ ├── left ordering: +3 @@ -1000,24 +1000,18 @@ sort │ │ │ │ ├── accountname:14 != 'someaccount' [outer=(14), constraints=(/14: (/NULL - /'someaccount') [/e'someaccount\x00' - ]; tight)] │ │ │ │ └── customername:15 != 'somecustomer' [outer=(15), constraints=(/15: (/NULL - /'somecustomer') [/e'somecustomer\x00' - ]; tight)] │ │ │ └── filters (true) - │ │ ├── inner-join (hash) - │ │ │ ├── columns: id:20!null cardsinfo.dealerid:28!null cardsinfo.cardid:29!null - │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) - │ │ │ ├── stats: [rows=58333.33, distinct(20)=37420.4, null(20)=0, distinct(28)=1, null(28)=0, distinct(29)=37420.4, null(29)=0] - │ │ │ ├── key: (29) - │ │ │ ├── fd: ()-->(28), (20)==(29), (29)==(20) + │ │ ├── project + │ │ │ ├── columns: id:20!null + │ │ │ ├── stats: [rows=58333.33, distinct(20)=37420.4, null(20)=0] + │ │ │ ├── key: (20) │ │ │ ├── scan cardsinfo@cardsinfoversionindex │ │ │ │ ├── columns: cardsinfo.dealerid:28!null cardsinfo.cardid:29!null │ │ │ │ ├── constraint: /28/36: [/1 - /1] │ │ │ │ ├── stats: [rows=58333.33, distinct(28)=1, null(28)=0, distinct(29)=37420.4, null(29)=0] │ │ │ │ ├── key: (29) │ │ │ │ └── fd: ()-->(28) - │ │ │ ├── scan cards@cardsnamesetnumber - │ │ │ │ ├── columns: id:20!null - │ │ │ │ ├── stats: [rows=57000, distinct(20)=57000, null(20)=0] - │ │ │ │ └── key: (20) - │ │ │ └── filters - │ │ │ └── id:20 = cardsinfo.cardid:29 [outer=(20,29), constraints=(/20: (/NULL - ]; /29: (/NULL - ]), fd=(20)==(29), (29)==(20)] + │ │ │ └── projections + │ │ │ └── cardsinfo.cardid:29 [as=id:20, outer=(29)] │ │ └── filters │ │ └── id:20 = transactiondetails.cardid:4 [outer=(4,20), constraints=(/4: (/NULL - ]; /20: (/NULL - ]), fd=(4)==(20), (20)==(4)] │ └── projections diff --git a/pkg/sql/opt/xform/testdata/external/trading-mutation b/pkg/sql/opt/xform/testdata/external/trading-mutation index a518cf30ee45..371b80e7e612 100644 --- a/pkg/sql/opt/xform/testdata/external/trading-mutation +++ b/pkg/sql/opt/xform/testdata/external/trading-mutation @@ -968,11 +968,11 @@ sort │ ├── stable │ ├── stats: [rows=139576.9, distinct(53)=82395.9, null(53)=0] │ ├── inner-join (hash) - │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null id:24!null cardsinfo.dealerid:32!null cardsinfo.cardid:33!null + │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null id:24!null │ │ ├── multiplicity: left-rows(zero-or-one), right-rows(zero-or-more) │ │ ├── stats: [rows=139576.9, distinct(3)=82395.9, null(3)=0, distinct(4)=37420.4, null(4)=0, distinct(24)=37420.4, null(24)=0] - │ │ ├── key: (5,15,33) - │ │ ├── fd: ()-->(1,2,13,14,32), (3-5)-->(6,7), (15)-->(16,17), (3)==(15), (15)==(3), (24)==(4,33), (33)==(4,24), (4)==(24,33) + │ │ ├── key: (5,15,24) + │ │ ├── fd: ()-->(1,2,13,14), (3-5)-->(6,7), (15)-->(16,17), (3)==(15), (15)==(3), (4)==(24), (24)==(4) │ │ ├── inner-join (merge) │ │ │ ├── columns: transactiondetails.dealerid:1!null transactiondetails.isbuy:2!null transactiondate:3!null transactiondetails.cardid:4!null quantity:5!null transactiondetails.sellprice:6!null transactiondetails.buyprice:7!null transactions.dealerid:13!null transactions.isbuy:14!null date:15!null accountname:16!null customername:17!null │ │ │ ├── left ordering: +3 @@ -1004,24 +1004,18 @@ sort │ │ │ │ ├── accountname:16 != 'someaccount' [outer=(16), constraints=(/16: (/NULL - /'someaccount') [/e'someaccount\x00' - ]; tight)] │ │ │ │ └── customername:17 != 'somecustomer' [outer=(17), constraints=(/17: (/NULL - /'somecustomer') [/e'somecustomer\x00' - ]; tight)] │ │ │ └── filters (true) - │ │ ├── inner-join (hash) - │ │ │ ├── columns: id:24!null cardsinfo.dealerid:32!null cardsinfo.cardid:33!null - │ │ │ ├── multiplicity: left-rows(exactly-one), right-rows(zero-or-one) - │ │ │ ├── stats: [rows=58333.33, distinct(24)=37420.4, null(24)=0, distinct(32)=1, null(32)=0, distinct(33)=37420.4, null(33)=0] - │ │ │ ├── key: (33) - │ │ │ ├── fd: ()-->(32), (24)==(33), (33)==(24) + │ │ ├── project + │ │ │ ├── columns: id:24!null + │ │ │ ├── stats: [rows=58333.33, distinct(24)=37420.4, null(24)=0] + │ │ │ ├── key: (24) │ │ │ ├── scan cardsinfo@cardsinfoversionindex │ │ │ │ ├── columns: cardsinfo.dealerid:32!null cardsinfo.cardid:33!null │ │ │ │ ├── constraint: /32/40: [/1 - /1] │ │ │ │ ├── stats: [rows=58333.33, distinct(32)=1, null(32)=0, distinct(33)=37420.4, null(33)=0] │ │ │ │ ├── key: (33) │ │ │ │ └── fd: ()-->(32) - │ │ │ ├── scan cards@cardsnamesetnumber - │ │ │ │ ├── columns: id:24!null - │ │ │ │ ├── stats: [rows=57000, distinct(24)=57000, null(24)=0] - │ │ │ │ └── key: (24) - │ │ │ └── filters - │ │ │ └── id:24 = cardsinfo.cardid:33 [outer=(24,33), constraints=(/24: (/NULL - ]; /33: (/NULL - ]), fd=(24)==(33), (33)==(24)] + │ │ │ └── projections + │ │ │ └── cardsinfo.cardid:33 [as=id:24, outer=(33)] │ │ └── filters │ │ └── id:24 = transactiondetails.cardid:4 [outer=(4,24), constraints=(/4: (/NULL - ]; /24: (/NULL - ]), fd=(4)==(24), (24)==(4)] │ └── projections diff --git a/pkg/sql/opt/xform/testdata/physprops/ordering b/pkg/sql/opt/xform/testdata/physprops/ordering index 6e22da614395..eeef6dd411b4 100644 --- a/pkg/sql/opt/xform/testdata/physprops/ordering +++ b/pkg/sql/opt/xform/testdata/physprops/ordering @@ -2983,7 +2983,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 @@ -3010,7 +3010,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 b/pkg/sql/opt/xform/testdata/rules/join index 9ce11a9aa21c..3d269029b927 100644 --- a/pkg/sql/opt/xform/testdata/rules/join +++ b/pkg/sql/opt/xform/testdata/rules/join @@ -1911,7 +1911,7 @@ inner-join (lookup xyz@xy) └── y:8 = 1 [outer=(8), constraints=(/8: [/1 - /1]; tight), fd=()-->(8)] # Verify case where we generate multiple merge-joins. -memo +memo disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u) ---- memo (optimized, ~19KB, required=[presentation: s:1,t:2,u:3,s:6,t:7,u:8]) @@ -1951,7 +1951,7 @@ memo (optimized, ~19KB, required=[presentation: s:1,t:2,u:3,s:6,t:7,u:8]) ├── G13: (variable l.u) └── G14: (variable r.u) -exploretrace rule=GenerateMergeJoins +exploretrace rule=GenerateMergeJoins disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT * FROM stu AS l JOIN stu AS r ON (l.s, l.t, l.u) = (r.s, r.t, r.u) ---- ---- diff --git a/pkg/sql/opt/xform/testdata/rules/join_order b/pkg/sql/opt/xform/testdata/rules/join_order index 138dfe4f49db..45913f964797 100644 --- a/pkg/sql/opt/xform/testdata/rules/join_order +++ b/pkg/sql/opt/xform/testdata/rules/join_order @@ -752,7 +752,7 @@ exec-ddl CREATE TABLE a (id INT8 PRIMARY KEY) ---- -opt set=reorder_joins_limit=3 +opt set=reorder_joins_limit=3 disable=(EliminateJoinUnderProjectLeft,EliminateJoinUnderProjectRight) SELECT 1 FROM @@ -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] diff --git a/pkg/sql/pgwire/testdata/pgtest/row_description b/pkg/sql/pgwire/testdata/pgtest/row_description index 3e8ff08c9040..294d5bf2a615 100644 --- a/pkg/sql/pgwire/testdata/pgtest/row_description +++ b/pkg/sql/pgwire/testdata/pgtest/row_description @@ -141,7 +141,7 @@ ReadyForQuery # Regression test for not setting OIDs in some cases (#71891). send crdb_only -Query {"String": "SELECT a, tab1_a FROM tab2 INNER MERGE JOIN tab1 ON a = tab1_a WHERE a = 1"} +Query {"String": "SELECT a, tab1_a FROM tab2 RIGHT MERGE JOIN tab1 ON a = tab1_a WHERE a = 1"} ---- until crdb_only