-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/optimize interfaces #4574
base: dev
Are you sure you want to change the base?
Conversation
…erfaces # Conflicts: # packages/graphql/src/translate/create-projection-and-params.ts # packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts # packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts # packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts
|
CLA Assistant Lite bot: I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request |
Hi @jroith |
Yes. So for example, let's say you have a schema like: interface Base {
name: String
}
type Start {
id: ID! @id @unique
bases: [Base!]! @relationship(type: "HAS_BASE", direction: OUT)
}
type A implements Base {
name: String
something: String
}
type B implements Base {
name: String
somethingElse: String
}
type C implements Base {
name: String
different: String
}
[...]
type Z implements Base {
name: String
}
and a query: Yes. So for a simple query: {
start(where: {id:"foo"}) {
bases {
name
}
}
} The 4.4.5 library might generate Cypher like: MATCH (this:Start)
WHERE this.id = $param0
CALL {
WITH this
CALL {
WITH *
MATCH (this)-[this0:HAS_BASE]->(this1:A)
WITH this1 { .name, __resolveType: "A", __id: id(this1) } AS this1
RETURN this1 AS var2
UNION
WITH *
MATCH (this)-[this3:HAS_BASE]->(this4:B)
WITH this4 { .name, __resolveType: "B", __id: id(this4) } AS this4
RETURN this4 AS var2
UNION
WITH *
MATCH (this)-[this5:HAS_BASE]->(this6:C)
WITH this6 { .name, __resolveType: "C", __id: id(this6) } AS this6
RETURN this6 AS var2
UNION
WITH *
MATCH (this)-[this7:HAS_BASE]->(this8:D)
WITH this8 { .name, __resolveType: "D", __id: id(this8) } AS this8
RETURN this8 AS var2
UNION
... MANY MORE ...
UNION
WITH *
MATCH (this)-[this35:HAS_BASE]->(this36:Z)
WITH this36 { .name, __resolveType: "Z", __id: id(this36) } AS this36
RETURN this36 AS var2
}
WITH var2
RETURN collect(var2) AS var2
} While this patch will generate: MATCH (this:Start)
WHERE this.id = $param0
CALL {
WITH this
CALL {
WITH *
MATCH (this)-[this0:HAS_BASE]->(this1:Base)
WITH this1 { .name, __resolveType: this1.mainType, __id: id(this1) } AS this1
RETURN this1 AS var2
}
WITH var2
RETURN collect(var2) AS var2
} If there are some differences that cannot be unified, only those will create an extra UNION case. |
Just to clarify the mechanism a bit more: In the example shown above, if you substitute the interface selector e.g. "Base" or "A|B|..." for the concrete type label (A in the first union) and __resolveType for "this1.mainType" then if the cypher builder is called independently for each union case it will produce an identical string. This is then used to identify the cases that can be unified and the ones that can't and the translation is called a second time to generate only the required cases (one in this case, since no "... on" was used and everything can be folded). |
Hi @jroith Thanks again for sharing |
This is a fork that I'm currently maintaining internally, that optimised performance in certain cases. The implementations shown here is not particularly efficient, it's not fully general purpose and I'm providing it to share some ideas and because @angrykoala inquired about it in another MR in the cypher builder.
Let me try to quickly break down what is going on here.
We have a schema with many interfaces and wanted to achieve two goals:
We have addressed this for our case like this:
These changes are made, amongst other reasons, in order to be able to make each branch of a UNION as similar as possible and possibly identical. If an interface is queried, we can try to instead use a common label or common expression (if no label is available). Likewise we can replace __resolveType with the mainType property. This is usually enough to make the code in different UNION branches identical. If a "... on Foo" notation is used or perhaps in other cases such as authorisation or whatever, the code may differ. We then check all cases that are identical and collapse those cases to a single one and leave the other cases in place, excluding the combined ones using a predicate.
In practice this sometimes brings the original execution time down from 5 seconds to 100ms, especially due to long query planning times. A drawback is that the query generation itself is inefficient, because the query has to be built twice and recursively before being compressed and we only compare the resulting Cypher string which is robust but again not efficient. Since there is no cache in the library this is not optimal.
Nevertheless it is still much better for our cases and a negligible cost.
The patched library is a drop-in replacement because it will not have an effect unless the labelManager is present on and does not expose any new APIs.
Although I don't really have any hope that this MR will be merged, perhaps it can provide some useful ideas for the future that may help to improve the query generation for interfaces to a point where the fork is no longer necessary and can be dropped.