core/optimizer: use join-then-group subquery rewrite when group-first is unsafe
We attempt to "unnest" or "decorrelate" certain subqueries, because instead of evaluating the subquery for every outer row, we can perform some computations once and then rewrite the subquery as a join, which substantially reduces work. For a query like ```sql SELECT o.id FROM outer_table o WHERE o.limit > ( SELECT sum(i.value) FROM inner_table i WHERE i.key = o.key ); ``` The standard subquery unnesting strategy is of the form: ```sql SELECT o.id FROM outer_table o LEFT JOIN ( SELECT sum(i.value) AS sum_value, key FROM inner_table i GROUP BY key ) i ON i.key = o.key WHERE o.limit > i.sum_value ``` Which does a single grouping over the inner table, joins it with the outer, and then filters the joined result on the original WHERE condition. `LEFT JOIN` is needed to preserve "semi-join" semantics i.e. every outer row is preserved regardless of the match. Good stuff. The problem with this is that: 1. the un-rewritten form only aggregates values from the inner table where `i.key = o.key` 2. the rewritten form computes sum for every `i.key`, even the keys the outer query doesn't need in the un-rewritten form 3. some of the sums can overflow and error, which can cause the rewritten query to fail when summing on a key that the outer query didn't ask need. 4. this makes the rewrite incorrect because it changes behavior: the un-rewritten query would succeed, but the rewritten form would fail. So, for these cases where the aggregation over all rows can potentially fail, we instead join the outer and inner tables first, THEN perform the grouping, and put the original correlated where into `HAVING` instead: ```sql //! SELECT o.id //! FROM outer_table o //! LEFT JOIN inner_table i ON i.key = o.key //! GROUP BY o.rowid //! HAVING o.limit > sum(i.value) FILTER (WHERE i.rowid IS NOT NULL) ``` This requires both tables to be rowid tables (currently invariant since we dont support `WITHOUT ROWID`) - rowids are guaranteed unique so it guarantees that each outer row appears exactly once before the HAVING (= the rewritten WHERE) is evaluated - like in the un-rewritten form. The FILTER is needed so that the nulled out rows from unmatched inner rows don't contribute to the aggregate. Using `rowid IS NOT NULL` as the FILTER condition is an unambiguous way to determine that the inner row was null-extended instead of just having a NULL. This allows us to unnest the correlated subquery-within-subquery in TPC-H query 20 and run it in the order of seconds instead of taking forever.
J
Jussi Saurio committed
f39fc3dbd8a8fc1dd6e239e4bcf07f0c19111335
Parent: 78864a1