Skip to content

Commit

Permalink
opt: only infer self-join equality with a key over the base table
Browse files Browse the repository at this point in the history
Self-join equality inference was added by #105214, so that the `FuncDeps`
for a self-join would include equalities between *every* pair of columns
at the same ordinal position in the base table if there was an equality
between key columns (also at the same ordinal position). However, the
key column check was performed using the FDs of the join inputs rather
than the base table's FDs. This could lead to incorrectly adding self-join
equality filters. For example, consider the following:
```
CREATE TABLE t106371 (x INT NOT NULL, y INT NOT NULL);
INSERT INTO t106371 VALUES (1, 1), (1, 2);

SELECT * FROM t106371 a JOIN t106371 b ON a.x = b.x;

SELECT * FROM (SELECT * FROM t106371 ORDER BY y DESC LIMIT 1) a
JOIN (SELECT DISTINCT ON (x) * FROM t106371) b ON a.x = b.x;
```
In the first query above, `a.x = b.x` does not consitute joining on
key columns. But in the second query, one input has one row and the
other de-duplicated by the `x` column and so `x` is a key over both
inputs. However, the query as written will select different rows for
each input - `a` will return the `(1, 2)` row, while `b` will return
the `(1, 1)` row. Inferring a `a.y = b.y` filter will incorrectly cause
the join to return no rows.

This patch fixes the problem by requiring the initial self-join
equalities to form a key over the *base* table, not just the inputs
of the join.

Fixes #106371

Release note: None
  • Loading branch information
DrewKimball committed Jul 18, 2023
1 parent a06ea31 commit 1da6390
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 17 deletions.
12 changes: 12 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/join
Original file line number Diff line number Diff line change
Expand Up @@ -1205,3 +1205,15 @@ FROM task AS task0_ WHERE EXISTS (
11 taskWithPatient1WithValidSite1 5
12 taskWithPatient2WithValidSite1 6
13 taskWithPatient3WithValidSite2 7

# Regression test for #106371 - only infer self-join equalities if the original
# equality columns form a key over the *base* table, not just the join inputs.
statement ok
CREATE TABLE t106371 (x INT NOT NULL, y INT NOT NULL);
INSERT INTO t106371 VALUES (1, 1), (1, 2);

query IIII
SELECT * FROM (SELECT * FROM t106371 ORDER BY y DESC LIMIT 1) a
JOIN (SELECT DISTINCT ON (x) * FROM t106371) b ON a.x = b.x;
----
1 2 1 1
8 changes: 4 additions & 4 deletions pkg/sql/opt/memo/logical_props_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2550,14 +2550,15 @@ func (h *joinPropsHelper) addSelfJoinImpliedFDs(rel *props.Relational) {
return
}
for leftTable, leftTableOrds := range leftTables {
baseTabFDs := MakeTableFuncDep(md, leftTable)
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.
// key on the base table, *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) {
Expand All @@ -2567,8 +2568,7 @@ func (h *joinPropsHelper) addSelfJoinImpliedFDs(rel *props.Relational) {
eqCols.Add(rightCol)
}
}
if !eqCols.Empty() && h.leftProps.FuncDeps.ColsAreStrictKey(eqCols) &&
h.rightProps.FuncDeps.ColsAreStrictKey(eqCols) {
if !eqCols.Empty() && baseTabFDs.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) {
Expand Down
12 changes: 7 additions & 5 deletions pkg/sql/opt/memo/testdata/logprops/join
Original file line number Diff line number Diff line change
Expand Up @@ -2951,16 +2951,18 @@ inner-join (hash)
├── 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
# Self-join filters can only be inferred even if the join key was a key in the
# base table.
# TODO(drewk): if the optimizer could see that the same row is being selected
# from each input, we could still infer equalities.
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)
├── fd: ()-->(2,7), (1)-->(3), (6)-->(8), (1)==(6), (6)==(1)
├── prune: (3,8)
├── interesting orderings: (+1 opt(2)) (+6 opt(7))
├── select
Expand Down Expand Up @@ -3002,16 +3004,16 @@ inner-join (hash)
├── 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.
# Self-join filters cannot be inferred here because different rows are selected
# from each input.
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)
├── fd: ()-->(2,7), (1)-->(3), (6)-->(8), (1)==(6), (6)==(1)
├── prune: (3,8)
├── interesting orderings: (+1 opt(2)) (+6 opt(7))
├── select
Expand Down
2 changes: 1 addition & 1 deletion pkg/sql/opt/norm/testdata/rules/combo
Original file line number Diff line number Diff line change
Expand Up @@ -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#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==
https://raduberinde.github.io/optsteps.html#eJzsXd1y2ziWvt-nwPrKXlFqEZRsSVOu6t50utYpr5ONM3sz6epiO_I2E1nKSvLs9G7t1NQ8Qy776fpJpvgjiQTxcwCBJKCc3LhCEhSJc86Hg_N9AP_v7P4_bs9mZ_cvb1--eEf-hfzw9vW_k5jE4fvlzd3dy7fk1eubu_QAJa_vSBwOPpHr7A97PsrO0_x8VD8_ys5H-flR_fw4Oz_Kz48Hn86Cs7vV-un75PHxbHbW7_dJ_M3NMtkm8eL9stfrkZ8P__32W9IPSS8MxpR8--37Ze_zevVx_rB9v-yR37_89vuXv_3-5W_kYbV4flpuZuTTLPzn5fNiQZIZJY-ziGxmI_JxNiafZpPdiSl5nIVDspmFIfk4C2naaLw7GV6mZ6_Ss5P07JR8mlG6v2dEHmd0RDYzOiYfZ_QyPTvdnY2G6U-GZDOLKPk4i6LqQ36a_zoj52FApxfVE48f0uMX_f775-Ewmp_T_vgiIOdRMLr461-LY2FAg-zo5HDdtB_SILzsh9OARn16GUTDfhSlF4XDIAwPjSfBNEgvHWeX0vRSOt1dOgbd8CoIJ6UbZheOg_AyqN9xcnF9fR6OA0rTV81_4vr6fHI4MM2uuAxoFETD7IrL9Mi0dGCYXXIV0FEQhfkTZEeGpSNhdmQS0HEQ0exI_tNh6QjNjkyD9GXyN8l_nB6OUArpAToK6LjWA3mHps9d7gFK8xdOOyF7YRrl7xdeFu9HR8XbhFfF29Bx8ezhpHh2elk8aTjdPekU8qRRmN6A_6S5oYJoeLjj_kmzH42G-yelxd0OT0pH2RF6eFKae2p0eFJ6WXj3l8K7k-Vyvu5_XCVLcv5LvPklO53948RvCj-7EI7DQRrFcThIAzkOB2ksx-EgDec4HDysP_z8U7LcztfLePHT058fHn7aJk_zzTZ--jy7TK_Yxj8v5qvkw-wqQ68dAsR0kIJATAcZDsR0kEFBTAcZGsRUfuswSi_Z3zscZdC3h5A4GmQoEkeDDEjiaJBhSRwNMjiJI_nt6TC9ZH97GmbIucegeDTIYCgeDTIkikeDDIzi0SDDo3ikuP1Vesnh9pMMkfcgFo8HGY7F40EGZfF4kKFZPB6kgJb-ld4-GqWX7G8fjTnGfnpebJPPi-Qh2f46I4v547a_Xv3P5nz-l_hhu_i1v1rOLwKyTv7rl-LE_87Xq_5q3X9arec872GAlTlbR9crPrrmx6v4OhKi6e5cCT7Dyz4NOWi5A8rirG_4GGbRTYcBvQqi7KVp_uvR4UiY4RkNAzoJogwTaP7ro9KREs6mkDXhwOoOS3dnmwLSq90LDHevNNk9brh73BLYptg65mDrDkr7-yb20TQ9Mjo8Ls29d3x4XDrhub0Mdv-O6OsI-oqNYg0l_y4ASyq5yGHMrAJmFS2rUMngJAOSDEIy8MhgIwOMDCoykMjgIQOGRyNhFQYZDGQAkEE_BvoY3JP4AwBMEFPAmKLsPNuxL4CAcKy-1lUkKMFACQNKAFCO_nLol-O-HPTliC-HeznWy4FejvJyiKu7tBxMD-vVZiNuckpBBX3JnXdOwN3SoI9CH2HzEC9JTP4Ub67j8EdVq5bNqvk4hQGU7178-WJgBZNupebd2qFXc_tW7dq8vrXpt_sbJ4vtfL2RX06qbXIvLWrFs5D8afW8na-vz1MYfFgtN9t1nCy3m-vzb8IZOf_m7o-3t6RPfrwQ249n7khp7rqduxm1-eEjGVu5gaM5EgpvDrMsa9IsQory_iwc721aJJxVs07KZv0D-SYcV-wckMcP17xxmmNRnuVHakc5mLzl2RnX1typFM_Iunl__a4K67JmLax5veunQ6wWUykmXseMZSnlWDY3Zz73yqcl4bhiW55RxxLjH6zZVimSb8aprMM_zPRLQqXbiQzHWqyw0_WuL_YW2xUHqxarGii12JRjsdxKRQVtWljvx_fLnHq7T54-L5LHX-_jp_l_xuuX__0cL5JtMt_syDjJBRk9F4xpiaEje4aOa2PXGDpubST1BHmGiQwdMnQ-M3REztARFsu9nnr6XSNufFisGfvY-lPthgywMmddrTF5g4_I0HnA0NXcXga7p1T48xt9xUaxhpIyhk50kcOYiQxdgwydyB8AYIKYolHrU3Se7diXMXSKa11FApcZOkWX8hg6QZNTCiroS5YZOliLRhk62COwDJ28VesMndbjHBg6UDMzhk6_W6l5t3bo1UKGTr9vbfptrWDcl1xOqm3sM3S8Xzkv7n5zT76_uX93c_fiXb7G5MV39-_Os5t-d09u7t5NLi7I67fVw__6-vXtBfTB_kC26Uh6IfYvEYOoi9rdZBViBhHoI2YMouDmNc_jXse6XFsMIvswIgZR4SitMYiK5ygxiCpzmDGI7F0V1mXN2iqDuP95EYMoMn5rDKLoAUoMorDDzRjEmklYw7EW65BBfLVKlj_sCc4Kd1g5hawhsobIGiJr6PZ02O-6NbKGyBoia4isIaIvsoY-YCayhsgaeo4pyBoia4isIbKGyBoia-gha9gJn6e_7hDweMgrygAAeUXkFZFXRF6RyA3HWqwDXvHN8-aXnDS8WW5Xr1bJ8nb-uN1Ri6KzFXbxCtlFZBeRXUR20bVps9_1bWQXkV1EdhHZRURfZBd9wExkF5Fd9BxTkF1EdhHZRU_YRRntYrpr6L7VfFGUsmRX-8JFynvqKC5Sa2fOBm9ttDls6_YzeSrNrWKbt6bWvqbu0IxIj4sdxCbwWlhU24xfaO3WS86362dAxoakNZLWSFojaY2kdZuk9dv50-rP87vV9u55sXixWn5ItslquSOtRWcL0voqI60vkbRG0hpJayStXavG-E2bIGmNpDWS1khaI_oiae0DZiJpjaS155iCpDWS1khae0Jawx7htOhnrWY2l8IqOGJQYzc7-bi-Pooj1hMTQEhA57hAM0oQCWaxdzW4_hqZWnVsI1OLTC0ytcjUts_UvlwkT8ky3s7vi5Q2p2hrhwtu9jLjZkPkZpGbRW4WuVnXig5-swPIzSI3i9wscrOIvsjN-oCZyM0iN-s5piA3i9wscrOecLPABcW7Qhbs6pPc3BjEAZoxulq3ZnsX1Lj1TjZ5Ks2-bp3R3bNuekwq7nMNNT3yrMizIs-KPCvyrMizAnjW2_nj9rvNZvWQxNv5q1Wy3LxN54w7vlV4uuBdQ-RdkXdF3hV5VzcLCn5X_pF3Rd4VeVfkXRF9kXf1ATORd22Qd2WLrRIwYUurBqwS4g-kJigogCNHexIcrcK6vGiCMYFeBxX0JcscLayFCxytCS_oGCHowjbOx22i3fV-yro7KDfSdYBxvUNqsvGhFfau2iNs-YSmG4zBDbQZ1ZMdQgVZCTLxYvxocEvtPROvyGxYJl43q-km6xZHLbCTzJh4wc1hGxmQ6sWtMfH6GrQIuvlB6-6g9VRgLLfjHFpbx-t9cL1jV4GKfFDjgRoP1HigxgM1Hqjx4HOYqPFAjYfDpQjUeKDGAzUeqPGQazz6rNvLyrc6fCxCdOcQLebeUQiCQhAnhSAKPhpBxYK6oyK_UFzraqy6zB1pkBRqqt4Vjl6XnLdDLWvS8Z3z8GAC3m73wCn3U-LaG8-eFH3YDIfPKeArrmyXtccsqd1titShLmhxghEPfdcjIxP2I-AV86iuQXWNU_tcmG1c0ZVMwqCLAUFpRyYh_w3YphYdyyRgMiChAkskkwBP4FuugImzLFVHmMkk2Lsq5FWsF7Qqk9DQRI8MNNHdZOvaabUdu8PUR4LrnPICpWAKRTUoquHaF0U1KKpBUU3nopq-XFTTE5yuEbtIvnRek2x8yKzJAVB04xd-oujGd9GNiAJH-O0cfsVGqQhfRBc5DG5I6XQgfAEqATqXAIC5_yNZbSjb3x3Nr-b3LXWBxuY4p0Ds-Z42i2xjWSLAKWCJLmldFIDpMabHtvcdNNY-IDbaW0ncjJgCvk4ZZU6nmBMrjI0yJ8C7oswJZU4oc9LqYJQ5faUyJ8V4A9zlpSttiw5Uy3IpO9oWwc3NvuHUqrZFocaqq9xE2hZ1cb4tok5cGhC-rJm2pc-ajpWwsZZtVdsCWTk01lk51HLFCF7hsWNNhcqLvcAN2wrDNRM2vVk_LzPF0u38cftitdjsJE2cEyhmQjETipk8FDMRhVqpNnIjXf4V0C7mxkbjObO_U2UUq8Wxw-IJPwYjpL48oL5qiHZEBodOj07vhdPXRgKVMKp7AZDuLyu3zFLVa4z3CBHfRQQktj78h6lV56lVs4QhYD0dypFweDqZ4QnlSChHQjkSypFQjtRiZKIcCeVIKEdCORLKkTScA-VIXsiR2CeC7qTTlWhF8RySOoAd0Qp7V81vjrUqWqn9vEK0ku2yw1WtlM5UZCtjlK3USQ-UraBs5euUrfg7wfS93N6uDEXLFFodq9FNFuQbyGMjUXBaRIFN8QbmMZjHOJnHEPFUNQRt-mpPMCGcDmpIHSDfv1PLTzD36nzDQrtkRA3K97Nsq9vwuJbLwReTobAD8zXf8zXIBwkFQwCOBNY_xXAkYh_9oQcUdpyisENhbHion2DEQ9-1mcgUc8iwBijsuJZ_cFQh7JC36kbYofVMB2EHqNkRwg79Do7MO7jTkDcNSjvCDuAnZfmfTnJD2KHx8V_JZ7K6EnboQLUsl7Ij7IB9mkxwHesAne4zA91GpithByx55tUB7Ag7jvxKmhM7zXDGFSziYxHfwSK-aImgctNxIoInf-dAvte_pQayuv04EY4J0uuwQIwFYl8LxFLHhqLlCRaOGp-HQDqyCXATT3Qgl2Pl-JQqxxCLa2LACUKB1gs3GLLiwpVGKywp70rKGp3G1pUBTbspLus_2KHCDG97RJnZsNOjIzu9e3w4KnjtlJ4BPwTbIteFIjRk6JJ_zV_qfB190l_6TMBUzU5hWvYLGl7CukeHn_knYueo75rNd6iOts7mP4yqOmGnbC3dI1vmAKzlXVyZ-HKRPCXLeJutQfzj8sN8_SbXQN7OH7e7RYqKi4r1iuNsveLQx_WKffEKF9T5O1JLQ4qgAYqA4_aUOYFuj24PdfsuFtXW0pqvYV1mbUFLY-syMYa_shj2ZegSBQDFNZoYAO4NYpK5t48rKmsvtJ_wWV1H6dpwC5fMoWAMEcWvIVUkz1SunhHm3_4Snr4LxqQGsrrSh5uGcRCRl5ChYIyFNRSMeSAYkzo2FC1PUCXS8sIVfkc2AW7QlTHgDBAFY94KxiAW18SAE4QCrRduMGTFmhONVigYu-auQZZ2mnwhMrdpN4Ix_QcTLUmWtbW2Lhnc6fXFyXqd3j0-HBW8dgRjgB-CrWJ0QTAGGbrkS5elztfR-mXpMwFTNTuCMdkvaHgJ6x4drmkmYueoL2zmO1RHq5v5D6OqTtgRjEmXMcscgLW8EyueDQ3oQJFf_E5CHSnW7bFu3-6ybPAio5OcJ7YsLrc99VTcXQIzWCM64RqRwtjwUD_BiIe-azORKZ5SwhpgKeiau3YQWgWSt-qmAKT1TKLFgvbLPvodXF8i6Eqxx6CLAUFpp8Qj_w3FOh83CjuK8Ua-CLDzco4OVMtyKTtFHMHNYX7AOkCHa_0OT1Eo85LVUrRO8beqw-b1nWyyH5CdyypXExbAXDROZtFw33gKaZzBedH6cRaF-9bhENI8GwWK5ptZRA_NQ2HzUqBmg0fR_OMsig7NO1pNNyS9MBiNTmY1HcXVdFiHcaoOU3fSIudAJ3XFSRvSeDXiqrhSTXOlGsWVau5FHA4LAnetzkfRXd1wV98GCCKemX9tq8D8Y5MVq8B4O-Ujm4zDVRdssmGYaaWBzcSkSrTBrQtjmOEwyxlm1bwrfvGvcYnFKfRh3afsstX4WcML1JFcKNYaHYFnJxmSrXzWEKTwknHVqBwBKkfMJCNOaUV0RSL21SFmspDW9SCC0VQcXPixUPxYqFbg4cdC8WOhjqiz8GOhInewps6y42uCUcmDL5GCxGht-TvbjVwdWvUiUvUmlQ5N1ViqQ1M1VujQVM0VOjT5J06VOjSVCu7I5vtez33WUP2Xg5Cp-i-HLmP1X454xuq_HCi7Vv-NRqQXBtHkVNR_RbqAvDkW9E0K-jyt3sQlqV7N_rUOrtm_1uN1-9dtULd_3Sp1-7dkJ57b17287tN1D676Kwrr9IR11bmZE_GBiOsd4gqca-KcCg6xt2HsZYyNmjWfxDRyzRq3jIdiGhwJDiOByDu-TjGN8iXbEILIiu9IhQGpMDMOzCnyS5f1sk93mfFcjhFc2sxWM5SWGc3Q1RYDbeZGWt4BjkeV0JhHNzWaGuFEptGJjMiWQL2RG0IjsMLoCKwnzB2RvysxSarm0H0kTAg4cLfxCDh4t3EJOHi3cQk4OO3JJeDgtKdR832v5yONIe2Zj0ymtGc-nhnTnvkwaEx75qOnGe35Zv28nN8_xMsXq8VmR3MyB3NaM5r4S2uKiqHOsJqYPjScPtixs9CqaEPkEbvgEWsvXUU35iRC3OmGh1VeD3EOObsWODv1m_hXl5L3Ka_Oi3Upf4NVp8aIoOoAqJoRSG7VDw2eQrkGXklD2V_uKLmjJFawxIklTvslTni3ydcYmNQowd3Gq1HCu41bo4R3G7dGyW1uvUa5X3XxZpc4Mqsx9scrlcp8--X-Ptvs8asGHpcvcbrnf2bSV5S1akWcr6GsJZTHWy9roet36fqMIdlKh1rTC610KJS0nLTXpNJReyEn1MlNVTrkfapb6cBAdCAQW58dA0WzZvNS6SxSFO-VWaT0g7qqWaSqsXQWqWqsmEWqmpssdIfPIsHdxptFgruNN4uEdxt3FgnvNrhQBj6LBHcbbxYJ7jbeLBLebdxZJLzbuLNIuL6IO4vss03YWWRPkmL0bGQXnJscEovecTlFj5dOSJ_av0xC2H-TC-5JzB_cyh84JqoLc61mDeLbcR3nyFyBE8LMd9nMMgRJO8XX2EzzAklL5TfYTLMBSO_wt6wxywFAvSPYqMZ05Af1jkDeaqpsNRW1mutZzaWspmN7TzKsnwVnL__yebFaz79PHh_PZmd5tfjtfLX-MF-_WiXLvaC1eqyoEo9KVWKBogxrwziudlwbFtfGrJTFpBWxI2U_XMXPSSWu0vwDC1_OBxig5mW53AWpdNkrcqnqW6alLdOqlnlBy7yWZV7GMq1gmRavzOtW5iUr82qVaaHKtEZlXp4yr0yZF6W8rjnJiml5kv1vq2SzLWQXP6xXTzdp-pym17uMW3IBJ_3u89Lv3mmsMcNE3P88wetEvM8npE8qERf23-SCexITcbcCrDxccseC44PMDiNx2LuW51HWoV1ZWw-BtXWDtAKSXRyZZJzy3IuD2oy2wFRWYKooMBcTmOsIzCUEpuoBU-GAuWbAXC5grhQwFQmY6gPMpQHmqgBzQYBcC8AGNucSAc2nGaq8BgpiTzs4eU2UVJ52OEpfXfq9CThrpx158leXf0tCh6DTDDLpq0v3mdHh4rTjqWcQSEcQajh5x7mFG3MLophbWMib7ZBGB5dzY27hvW5HMcUIgVMM-HYK6rmTYn6hUdJRGK5v13Di23HrLPZ5KzZLE86rpEmxdLUvnMXSztLk-a98Ja8OYaWZpUlfXbpKV4eb0s7S5K8uX4ELp6E0szT5q8tX1-owTtpZ2tn__9M_AgAA__9V3MKw

# Exploration patterns with varying costs.
optsteps
Expand Down
Loading

0 comments on commit 1da6390

Please sign in to comment.