Repository: edgedb/edgedb

Issue:
Slow query when filtering by second link
- EdgeDB Version: 1.4+ffe1237 
- OS Version: your CloudFormation template

Simplified schema:
```
Person { multi link personPosts := .<person[is Profile].profilePosts; }
Profile {
  link person -> Person {
    delegated constraint exclusive;
  }
  multi link profilePosts := .<owner[is Post];
}
IGProfile {}
TTProfile {}
Post { required link owner -> Profile; }
IGPost { overloaded required link owner -> IGProfile; }
TTPost { overloaded required link owner -> TTProfile; }
```

These queries are both fast:
```
select Post filter .owner.id = <uuid>"";
select Person { personPosts } filter .id = <uuid>"";
```

But this one take 5+ seconds:
```
select Post filter .owner.person.id = <uuid>"";
```

Number of objects:
Person: 100
Profile: 150k
Post: 431k

I'm happy to share more details if it would be helpful.


Discussion:
Interesting. I was able to reproduce this slowness.

If we do the select on just one of the post types, like
```
select TTPost filter .owner.person.id = <uuid>'3752263c-12a0-11ed-80df-cbe9314dd309';
```
it is much faster, but still kind of slow: 250ms vs 2.6ms for one of the shape ones.
I think the crux of the issue is that we are doing a semi-join on `.person` because `Post.owner` isn't visible.
If we do 
```
select Post filter (for x in .owner union (x.person.id = <uuid>$uid))
```
or
```
select Post filter (.owner, .owner.person.id = <uuid>$uid).1
```
then it is fast.

I think it should be possible to disable the semi-join in this case, since `Post` is visible and `.owner` is single

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
@@ -964,6 +964,35 @@ def process_set_as_path_type_intersection(
         ir_set, stmt, aspects=['value', 'source'], ctx=ctx)
 
 
+def _source_path_needs_semi_join(
+        ir_source: irast.Set,
+        ctx: context.CompilerContextLevel) -> bool:
+    """Check if the path might need a semi-join
+
+    It does not need one if it has a visible prefix followed by single
+    pointers. Otherwise it might.
+
+    This is an optimization that allows us to avoid doing a semi-join
+    when there is a chain of single links referenced (probably in a filter
+    or a computable).
+
+    """
+    if ctx.scope_tree.is_visible(ir_source.path_id):
+        return False
+
+    while (
+        ir_source.rptr
+        and ir_source.rptr.dir_cardinality.is_single()
+        and not ir_source.expr
+    ):
+        ir_source = ir_source.rptr.source
+
+        if ctx.scope_tree.is_visible(ir_source.path_id):
+            return False
+
+    return True
+
+
 def process_set_as_path(
         ir_set: irast.Set, stmt: pgast.SelectStmt, *,
         ctx: context.CompilerContextLevel) -> SetRVars:
@@ -1008,10 +1037,10 @@ def process_set_as_path(
         ctx.disable_semi_join |= new_paths
 
     semi_join = (
-        not source_is_visible and
         not root_source_is_visible and
         ir_set.path_id not in ctx.disable_semi_join and
         not (is_linkprop or is_primitive_ref) and
+        _source_path_needs_semi_join(ir_source, ctx=ctx) and
         # This is an optimization for when we are inside of a semi-join on
         # a computable: process_set_as_subquery will have included an
         # rvar for the computable source, and we want to join on it
@@ -1288,6 +1317,11 @@ def process_set_as_subquery(
 
             if is_objtype_path and not source_is_visible:
                 # Non-scalar computable semi-join.
+
+                # TODO: The basic path case has a more sophisticated
+                # understanding of when to do semi-joins. Using that
+                # naively here doesn't work, but perhaps it could be
+                # adapted?
                 semi_join = True
 
                 # We need to compile the source and include it in,


Test patch:
diff --git a/tests/test_edgeql_sql_codegen.py b/tests/test_edgeql_sql_codegen.py
--- a/tests/test_edgeql_sql_codegen.py
+++ b/tests/test_edgeql_sql_codegen.py
@@ -229,3 +229,15 @@ def test_codegen_filtered_link_no_semijoin(self):
             " IN ", sql,
             "unexpected semi-join",
         )
+
+    def test_codegen_chained_single_no_semijoin(self):
+        sql = self._compile('''
+            select Issue {
+               z := .owner.todo
+            }
+       ''')
+
+        self.assertNotIn(
+            " IN ", sql,
+            "unexpected semi-join",
+        )