Repository: edgedb/edgedb

Issue:
`??` sometimes slow when RHS is slow, even if LHS is fast and non-empty
```
create type Entity {
    create required property name -> str { create constraint exclusive };
    create multi property aka -> str { create constraint exclusive; };
};

for name in <str>range_unpack(range(0, 100000)) union (
  insert Entity {
    name := name,
    aka := name ++ '_' ++ {'a', 'b', 'c', 'd'},
  }
);
```

```
with name := '500',
select (
 (select Entity filter .name = name)
 ?? (select Entity filter name in .aka)
) { name };
```
is slow (180ms), even though the LHS takes under 1ms.

Doing it with `if then else` is fast, though, though it performs the select twice:
```
with name := '500',
     obj := (select Entity filter .name = name),
select (
  if exists obj then obj else
  (select Entity filter name in .aka)
) { name };
```

In internal testmode, this version is fast and only does the select once:
```
with name := '500',
for optional obj in ((select Entity filter .name = name),)
union (
 select (
   if exists obj then obj.0 else (select Entity filter name in .aka)
  ) { name }
);
```

So probably the investigation to do is to figure out what is slower about what we actually do than that


Discussion:
As an aside, the trick to make it always fast is the kind of horrific:
```
with name := '500_d',
select (
 (select Entity filter .name = name)
 ?? (select (Entity, Entity.aka).0 filter name = Entity.aka)
) { name };
```

Resolving patch:
diff --git a/edb/pgsql/compiler/relgen.py b/edb/pgsql/compiler/relgen.py
--- a/edb/pgsql/compiler/relgen.py
+++ b/edb/pgsql/compiler/relgen.py
@@ -725,7 +725,7 @@ def finalize_optional_rel(
 
         if lvar.nullable:
             # The left var is still nullable, which may be the
-            # case for non-required singleton scalar links.
+            # case for non-required singleton properties.
             # Filter out NULLs.
             setrel.where_clause = astutils.extend_binop(
                 setrel.where_clause,
@@ -1808,122 +1808,101 @@ def process_set_as_coalesce(
             pathctx.put_path_value_var(ctx.rel, ir_set.path_id, set_expr)
 
         else:
-            # Things become tricky in cases where the RHS is a non-singleton.
+            # Things become tricky in cases where the RHS is a non-singleton
+            # or where we need to worry about source aspects.
             # We cannot use the regular scalar COALESCE over a JOIN,
-            # as that'll blow up the result cardinality. Instead, we
-            # compute a UNION of both sides and annotate each side with
-            # a marker.  We then select only rows that match the marker
-            # of the first row:
+            # as that'll blow up the result cardinality. Instead, we do
+            # something like:
             #
-            #     SELECT
-            #         q.*
-            #     FROM
-            #         (SELECT
-            #             marker = first_value(marker) OVER () AS marker,
-            #             ...
-            #          FROM
-            #             (SELECT 1 AS marker, * FROM left
-            #              UNION ALL
-            #              SELECT 2 AS marker, * FROM right) AS u
-            #         ) AS q
-            #     WHERE marker
-            with newctx.subrel() as subctx:
-                subqry = subctx.rel
-
-                with subctx.subrel() as sub2ctx:
-
-                    with sub2ctx.subrel() as scopectx:
-                        larg = scopectx.rel
-                        pathctx.put_path_id_map(
-                            larg, ir_set.path_id, left_ir.path_id)
-                        lvar = dispatch.compile(left_ir, ctx=scopectx)
-
-                        if lvar.nullable:
-                            # The left var is still nullable, which may be the
-                            # case for non-required singleton scalar links.
-                            # Filter out NULLs.
-                            larg.where_clause = astutils.extend_binop(
-                                larg.where_clause,
-                                pgast.NullTest(
-                                    arg=lvar, negated=True
-                                )
-                            )
-
-                    with sub2ctx.subrel() as scopectx:
-                        rarg = scopectx.rel
-                        pathctx.put_path_id_map(
-                            rarg, ir_set.path_id, right_ir.path_id)
-                        rvar = dispatch.compile(right_ir, ctx=scopectx)
-
-                        if rvar.nullable:
-                            rarg.where_clause = astutils.extend_binop(
-                                rarg.where_clause,
-                                pgast.NullTest(arg=rvar, negated=True)
-                            )
-
-                    marker = sub2ctx.env.aliases.get('m')
-
-                    larg.target_list.insert(
-                        0,
-                        pgast.ResTarget(val=pgast.NumericConstant(val='1'),
-                                        name=marker))
-                    rarg.target_list.insert(
-                        0,
-                        pgast.ResTarget(val=pgast.NumericConstant(val='2'),
-                                        name=marker))
-
-                    unionqry = sub2ctx.rel
-                    unionqry.op = 'UNION'
-                    unionqry.all = True
-                    unionqry.larg = larg
-                    unionqry.rarg = rarg
+            #     CROSS JOIN LATERAL
+            #       (<left>) as lhs
+            #     CROSS JOIN LATERAL (
+            #       SELECT * FROM lhs WHERE lhs.value IS NOT NULL
+            #       UNION ALL
+            #       SELECT * FROM <right> as rhs WHERE lhs.value IS NULL
+            #     ) as q
+            #
+            # Note that <left> will be compiled with optional wrapping,
+            # so it shouldn't ever produce zero rows.
+            lhs_rvar = get_set_rvar(left_ir, ctx=newctx)
+            lvar = pathctx.get_rvar_path_var(
+                lhs_rvar, left_ir.path_id, aspect='value', env=ctx.env
+            )
+            lval = output.output_as_value(lvar, env=ctx.env)
 
-                union_rvar = relctx.rvar_for_rel(
-                    unionqry, lateral=True, ctx=subctx)
+            with newctx.subrel() as lctx:
+                larg = lctx.rel
+                pathctx.put_path_id_map(larg, ir_set.path_id, left_ir.path_id)
 
                 relctx.include_rvar(
-                    subqry, union_rvar, path_id=ir_set.path_id, ctx=subctx)
+                    larg,
+                    lhs_rvar,
+                    path_id=left_ir.path_id,
+                    ctx=lctx,
+                    # Only include the aspects that got included
+                    # into our rel; it's possible some were explicitly
+                    # left out, and we should respect that.
+                    aspects=pathctx.list_path_aspects(
+                        newctx.rel, left_ir.path_id
+                    ),
+                )
 
-                lagged_marker = pgast.FuncCall(
-                    name=('first_value',),
-                    args=[pgast.ColumnRef(name=[marker])],
-                    over=pgast.WindowDef()
+                # Include the LHS when it is not NULL.
+                larg.where_clause = astutils.extend_binop(
+                    larg.where_clause,
+                    pgast.NullTest(
+                        arg=lval, negated=True
+                    )
                 )
 
-                marker_ok = astutils.new_binop(
-                    pgast.ColumnRef(name=[marker]),
-                    lagged_marker,
-                    op='=',
+            with newctx.subrel() as rctx:
+                rarg = rctx.rel
+                pathctx.put_path_id_map(rarg, ir_set.path_id, right_ir.path_id)
+                rvar = dispatch.compile(right_ir, ctx=rctx)
+
+                # Include the RHS when the LHS is NULL.
+                # Note that an important precondition of this is that
+                # there are not "stray" NULLs in the LHS input set.
+                #
+                # HACK: We need to use IS NOT DISTINCT FROM
+                # because ROW() IS NULL is true, and that
+                # breaks some things.
+                rarg.where_clause = astutils.extend_binop(
+                    rarg.where_clause,
+                    astutils.new_binop(
+                        lval, pgast.NullConstant(),
+                        'IS NOT DISTINCT FROM',
+                    ),
                 )
 
-                subqry.target_list.append(
-                    pgast.ResTarget(
-                        name=marker,
-                        val=marker_ok
+                if rvar.nullable:
+                    rarg.where_clause = astutils.extend_binop(
+                        rarg.where_clause,
+                        pgast.NullTest(arg=rvar, negated=True)
                     )
-                )
 
-            aspects = pathctx.list_path_aspects(
-                larg, left_ir.path_id
-            ) & pathctx.list_path_aspects(rarg, right_ir.path_id)
+            union_rvar = relctx.rvar_for_rel(
+                pgast.SelectStmt(op='UNION', all=True, larg=larg, rarg=rarg),
+                lateral=True,
+                ctx=newctx,
+            )
 
-            subrvar = relctx.rvar_for_rel(subqry, lateral=True, ctx=newctx)
+            aspects = (
+                pathctx.list_path_aspects(larg, left_ir.path_id)
+                & pathctx.list_path_aspects(rarg, right_ir.path_id)
+            )
 
             # No pull_namespace because we don't want the coalesce arguments to
             # escape, just the final result.
             relctx.include_rvar(
                 ctx.rel,
-                subrvar,
+                union_rvar,
                 path_id=ir_set.path_id,
                 aspects=aspects,
                 pull_namespace=False,
                 ctx=newctx,
             )
 
-            ctx.rel.where_clause = astutils.extend_binop(
-                ctx.rel.where_clause,
-                astutils.get_column(subrvar, marker, nullable=False))
-
     return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)
 
 


Test patch: