GraphQL - Preload epics group using Lookahead
What does this MR do?
Related to #210553 (closed)
Fixes N+1 queries in group epics by using the Lookahead feature.
For example, when querying a group with a sample of 10 epics, we can see the number of queries against the namespaces
table reduced by preloading the field :group
unconditionally:
Query
{
group(fullPath: "gitlab-org") {
epics {
edges {
node {
title
}
}
}
}
}
Result
{
"data": {
"group": {
"epics": {
"edges": [
{
"node": {
"title": "Test Epic 10"
}
},
{
"node": {
"title": "Test Epic 9"
}
},
{
"node": {
"title": "Test Epic 8"
}
},
{
"node": {
"title": "Test Epic 7"
}
},
{
"node": {
"title": "Test Epic 6"
}
},
{
"node": {
"title": "Test Epic 5"
}
},
{
"node": {
"title": "Test Epic 4"
}
},
{
"node": {
"title": "Test Epic 3"
}
},
{
"node": {
"title": "Test Epic 2"
}
},
{
"node": {
"title": "Test Epic 1"
}
}
]
}
}
}
}
Log before preloading field
Started POST "/api/graphql" for 127.0.0.1 at 2020-09-23 14:14:11 +0100
Processing by GraphqlController#execute as */*
Parameters: {"query"=>"query {\n group(fullPath: \"test-group\") {\n epics {\n edges {\n node {\n title\n }\n }\n }\n }\n}", "variables"=>"[FILTERED]", "graphql"=>{"query"=>"query {\n group(fullPath: \"test-group\") {\n epics {\n edges {\n node {\n title\n }\n }\n }\n }\n}", "variables"=>"[FILTERED]"}}
[1m[36mUser Load (0.5ms)[0m [1m[34mSELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2[0m [["id", 1], ["LIMIT", 1]]
↳ app/controllers/concerns/impersonation.rb:7:in `current_user'
Can't verify CSRF token authenticity.
[1m[35m (0.1ms)[0m [1m[35mBEGIN[0m
↳ app/models/user.rb:1315:in `ensure_namespace_correct'
[1m[36mNamespace Load (0.4ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."owner_id" = $1 AND "namespaces"."type" IS NULL LIMIT $2[0m [["owner_id", 1], ["LIMIT", 1]]
↳ app/models/user.rb:1315:in `ensure_namespace_correct'
[1m[35m (0.2ms)[0m [1m[35mCOMMIT[0m
↳ app/models/user.rb:753:in `forget_me!'
[1m[35mSQL (0.7ms)[0m [1m[34mSELECT "namespaces"."id" AS t0_r0, "namespaces"."name" AS t0_r1, "namespaces"."path" AS t0_r2, "namespaces"."owner_id" AS t0_r3, "namespaces"."created_at" AS t0_r4, "namespaces"."updated_at" AS t0_r5, "namespaces"."type" AS t0_r6, "namespaces"."description" AS t0_r7, "namespaces"."avatar" AS t0_r8, "namespaces"."membership_lock" AS t0_r9, "namespaces"."share_with_group_lock" AS t0_r10, "namespaces"."visibility_level" AS t0_r11, "namespaces"."request_access_enabled" AS t0_r12, "namespaces"."ldap_sync_status" AS t0_r13, "namespaces"."ldap_sync_error" AS t0_r14, "namespaces"."ldap_sync_last_update_at" AS t0_r15, "namespaces"."ldap_sync_last_successful_update_at" AS t0_r16, "namespaces"."ldap_sync_last_sync_at" AS t0_r17, "namespaces"."description_html" AS t0_r18, "namespaces"."lfs_enabled" AS t0_r19, "namespaces"."parent_id" AS t0_r20, "namespaces"."shared_runners_minutes_limit" AS t0_r21, "namespaces"."repository_size_limit" AS t0_r22, "namespaces"."require_two_factor_authentication" AS t0_r23, "namespaces"."two_factor_grace_period" AS t0_r24, "namespaces"."cached_markdown_version" AS t0_r25, "namespaces"."project_creation_level" AS t0_r26, "namespaces"."runners_token" AS t0_r27, "namespaces"."file_template_project_id" AS t0_r28, "namespaces"."saml_discovery_token" AS t0_r29, "namespaces"."runners_token_encrypted" AS t0_r30, "namespaces"."custom_project_templates_group_id" AS t0_r31, "namespaces"."auto_devops_enabled" AS t0_r32, "namespaces"."extra_shared_runners_minutes_limit" AS t0_r33, "namespaces"."last_ci_minutes_notification_at" AS t0_r34, "namespaces"."last_ci_minutes_usage_notification_level" AS t0_r35, "namespaces"."subgroup_creation_level" AS t0_r36, "namespaces"."emails_disabled" AS t0_r37, "namespaces"."max_pages_size" AS t0_r38, "namespaces"."max_artifacts_size" AS t0_r39, "namespaces"."mentions_disabled" AS t0_r40, "namespaces"."default_branch_protection" AS t0_r41, "namespaces"."unlock_membership_to_ldap" AS t0_r42, "namespaces"."max_personal_access_token_lifetime" AS t0_r43, "namespaces"."push_rule_id" AS t0_r44, "namespaces"."shared_runners_enabled" AS t0_r45, "namespaces"."allow_descendants_override_disabled_shared_runners" AS t0_r46, "namespaces"."traversal_ids" AS t0_r47, "namespaces"."delayed_project_removal" AS t0_r48, "routes"."id" AS t1_r0, "routes"."source_id" AS t1_r1, "routes"."source_type" AS t1_r2, "routes"."path" AS t1_r3, "routes"."created_at" AS t1_r4, "routes"."updated_at" AS t1_r5, "routes"."name" AS t1_r6 FROM "namespaces" LEFT OUTER JOIN "routes" ON "routes"."source_type" = $1 AND "routes"."source_id" = "namespaces"."id" WHERE "namespaces"."type" = $2 AND ((LOWER(routes.path) = LOWER('test-group')))[0m [["source_type", "Namespace"], ["type", "Group"]]
↳ lib/gitlab/graphql/loaders/full_path_model_loader.rb:18:in `block in find'
[1m[36mSamlProvider Load (0.2ms)[0m [1m[34mSELECT "saml_providers".* FROM "saml_providers" WHERE "saml_providers"."group_id" = $1 LIMIT $2[0m [["group_id", 100], ["LIMIT", 1]]
↳ ee/lib/gitlab/auth/group_saml/sso_enforcer.rb:35:in `group_access_restricted?'
[1m[36mLicense Load (0.2ms)[0m [1m[34mSELECT "licenses".* FROM "licenses" ORDER BY "licenses"."id" DESC LIMIT $1[0m [["LIMIT", 100]]
↳ ee/app/models/license.rb:258:in `load_license'
[1m[36mIpRestriction Load (0.2ms)[0m [1m[34mSELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = $1[0m [["group_id", 100]]
↳ ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
[1m[36mGroup Load (1.4ms)[0m [1m[35mWITH RECURSIVE "base_and_ancestors" AS ((WITH RECURSIVE "base_and_descendants" AS ((SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 100)
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_descendants" WHERE "namespaces"."type" = 'Group' AND "namespaces"."parent_id" = "base_and_descendants"."id")) SELECT "namespaces".* FROM "base_and_descendants" AS "namespaces")
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_ancestors" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = "base_and_ancestors"."parent_id")) SELECT namespaces.id, namespaces.parent_id, gitlab_subscriptions.hosted_plan_id FROM "base_and_ancestors" AS "namespaces" LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:20:in `each_with_object'
[1m[36mPlan Load (0.3ms)[0m [1m[34mSELECT "plans".* FROM "plans" WHERE 1=0[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:46:in `each_with_object'
[1m[36mGroup Load (0.8ms)[0m [1m[35mWITH RECURSIVE "base_and_descendants" AS ((SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 100)
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_descendants" WHERE "namespaces"."type" = 'Group' AND "namespaces"."parent_id" = "base_and_descendants"."id")) SELECT "namespaces".* FROM "base_and_descendants" AS "namespaces"[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:51:in `preload'
[1m[36mEpic Load (0.6ms)[0m [1m[34mSELECT "epics".* FROM "epics" WHERE "epics"."group_id" = $1 AND ("epics"."confidential" = $2 OR "epics"."confidential" = $3 AND "epics"."group_id" IN (SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = $4 AND (1=0))) ORDER BY "epics"."id" DESC LIMIT $5[0m [["group_id", 100], ["confidential", false], ["confidential", true], ["type", "Group"], ["LIMIT", 2000]]
↳ lib/gitlab/graphql/pagination/keyset/connection.rb:100:in `nodes'
[1m[36mGroup Load (0.3ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.1ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.0ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.1ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.0ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.0ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.1ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.0ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.0ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
[1m[36mCACHE Group Load (0.1ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2 LIMIT $3[0m [["type", "Group"], ["id", 100], ["LIMIT", 1]]
↳ ee/app/policies/epic_policy.rb:6:in `block in <class:EpicPolicy>'
Completed 200 OK in 90ms (Views: 0.2ms | ActiveRecord: 6.5ms | Elasticsearch: 0.0ms | Allocations: 92671)
Log after preloading field
Started POST "/api/graphql" for 127.0.0.1 at 2020-09-23 14:15:37 +0100
Processing by GraphqlController#execute as */*
Parameters: {"query"=>"query {\n group(fullPath: \"test-group\") {\n epics {\n edges {\n node {\n title\n }\n }\n }\n }\n}", "variables"=>"[FILTERED]", "graphql"=>{"query"=>"query {\n group(fullPath: \"test-group\") {\n epics {\n edges {\n node {\n title\n }\n }\n }\n }\n}", "variables"=>"[FILTERED]"}}
[1m[36mUser Load (0.4ms)[0m [1m[34mSELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2[0m [["id", 1], ["LIMIT", 1]]
↳ app/controllers/concerns/impersonation.rb:7:in `current_user'
Can't verify CSRF token authenticity.
[1m[35m (0.1ms)[0m [1m[35mBEGIN[0m
↳ app/models/user.rb:1315:in `ensure_namespace_correct'
[1m[36mNamespace Load (0.3ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."owner_id" = $1 AND "namespaces"."type" IS NULL LIMIT $2[0m [["owner_id", 1], ["LIMIT", 1]]
↳ app/models/user.rb:1315:in `ensure_namespace_correct'
[1m[35m (0.1ms)[0m [1m[35mCOMMIT[0m
↳ app/models/user.rb:753:in `forget_me!'
[1m[35mSQL (0.7ms)[0m [1m[34mSELECT "namespaces"."id" AS t0_r0, "namespaces"."name" AS t0_r1, "namespaces"."path" AS t0_r2, "namespaces"."owner_id" AS t0_r3, "namespaces"."created_at" AS t0_r4, "namespaces"."updated_at" AS t0_r5, "namespaces"."type" AS t0_r6, "namespaces"."description" AS t0_r7, "namespaces"."avatar" AS t0_r8, "namespaces"."membership_lock" AS t0_r9, "namespaces"."share_with_group_lock" AS t0_r10, "namespaces"."visibility_level" AS t0_r11, "namespaces"."request_access_enabled" AS t0_r12, "namespaces"."ldap_sync_status" AS t0_r13, "namespaces"."ldap_sync_error" AS t0_r14, "namespaces"."ldap_sync_last_update_at" AS t0_r15, "namespaces"."ldap_sync_last_successful_update_at" AS t0_r16, "namespaces"."ldap_sync_last_sync_at" AS t0_r17, "namespaces"."description_html" AS t0_r18, "namespaces"."lfs_enabled" AS t0_r19, "namespaces"."parent_id" AS t0_r20, "namespaces"."shared_runners_minutes_limit" AS t0_r21, "namespaces"."repository_size_limit" AS t0_r22, "namespaces"."require_two_factor_authentication" AS t0_r23, "namespaces"."two_factor_grace_period" AS t0_r24, "namespaces"."cached_markdown_version" AS t0_r25, "namespaces"."project_creation_level" AS t0_r26, "namespaces"."runners_token" AS t0_r27, "namespaces"."file_template_project_id" AS t0_r28, "namespaces"."saml_discovery_token" AS t0_r29, "namespaces"."runners_token_encrypted" AS t0_r30, "namespaces"."custom_project_templates_group_id" AS t0_r31, "namespaces"."auto_devops_enabled" AS t0_r32, "namespaces"."extra_shared_runners_minutes_limit" AS t0_r33, "namespaces"."last_ci_minutes_notification_at" AS t0_r34, "namespaces"."last_ci_minutes_usage_notification_level" AS t0_r35, "namespaces"."subgroup_creation_level" AS t0_r36, "namespaces"."emails_disabled" AS t0_r37, "namespaces"."max_pages_size" AS t0_r38, "namespaces"."max_artifacts_size" AS t0_r39, "namespaces"."mentions_disabled" AS t0_r40, "namespaces"."default_branch_protection" AS t0_r41, "namespaces"."unlock_membership_to_ldap" AS t0_r42, "namespaces"."max_personal_access_token_lifetime" AS t0_r43, "namespaces"."push_rule_id" AS t0_r44, "namespaces"."shared_runners_enabled" AS t0_r45, "namespaces"."allow_descendants_override_disabled_shared_runners" AS t0_r46, "namespaces"."traversal_ids" AS t0_r47, "namespaces"."delayed_project_removal" AS t0_r48, "routes"."id" AS t1_r0, "routes"."source_id" AS t1_r1, "routes"."source_type" AS t1_r2, "routes"."path" AS t1_r3, "routes"."created_at" AS t1_r4, "routes"."updated_at" AS t1_r5, "routes"."name" AS t1_r6 FROM "namespaces" LEFT OUTER JOIN "routes" ON "routes"."source_type" = $1 AND "routes"."source_id" = "namespaces"."id" WHERE "namespaces"."type" = $2 AND ((LOWER(routes.path) = LOWER('test-group')))[0m [["source_type", "Namespace"], ["type", "Group"]]
↳ lib/gitlab/graphql/loaders/full_path_model_loader.rb:18:in `block in find'
[1m[36mSamlProvider Load (0.2ms)[0m [1m[34mSELECT "saml_providers".* FROM "saml_providers" WHERE "saml_providers"."group_id" = $1 LIMIT $2[0m [["group_id", 100], ["LIMIT", 1]]
↳ ee/lib/gitlab/auth/group_saml/sso_enforcer.rb:35:in `group_access_restricted?'
[1m[36mLicense Load (0.2ms)[0m [1m[34mSELECT "licenses".* FROM "licenses" ORDER BY "licenses"."id" DESC LIMIT $1[0m [["LIMIT", 100]]
↳ ee/app/models/license.rb:258:in `load_license'
[1m[36mIpRestriction Load (0.2ms)[0m [1m[34mSELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = $1[0m [["group_id", 100]]
↳ ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
[1m[36mGroup Load (1.3ms)[0m [1m[35mWITH RECURSIVE "base_and_ancestors" AS ((WITH RECURSIVE "base_and_descendants" AS ((SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 100)
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_descendants" WHERE "namespaces"."type" = 'Group' AND "namespaces"."parent_id" = "base_and_descendants"."id")) SELECT "namespaces".* FROM "base_and_descendants" AS "namespaces")
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_ancestors" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = "base_and_ancestors"."parent_id")) SELECT namespaces.id, namespaces.parent_id, gitlab_subscriptions.hosted_plan_id FROM "base_and_ancestors" AS "namespaces" LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:20:in `each_with_object'
[1m[36mPlan Load (0.2ms)[0m [1m[34mSELECT "plans".* FROM "plans" WHERE 1=0[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:46:in `each_with_object'
[1m[36mGroup Load (0.8ms)[0m [1m[35mWITH RECURSIVE "base_and_descendants" AS ((SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 100)
UNION
(SELECT "namespaces".* FROM "namespaces", "base_and_descendants" WHERE "namespaces"."type" = 'Group' AND "namespaces"."parent_id" = "base_and_descendants"."id")) SELECT "namespaces".* FROM "base_and_descendants" AS "namespaces"[0m
↳ ee/lib/gitlab/group_plans_preloader.rb:51:in `preload'
[1m[36mEpic Load (0.5ms)[0m [1m[34mSELECT "epics".* FROM "epics" WHERE "epics"."group_id" = $1 AND ("epics"."confidential" = $2 OR "epics"."confidential" = $3 AND "epics"."group_id" IN (SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = $4 AND (1=0))) ORDER BY "epics"."id" DESC LIMIT $5[0m [["group_id", 100], ["confidential", false], ["confidential", true], ["type", "Group"], ["LIMIT", 2000]]
↳ lib/gitlab/graphql/pagination/keyset/connection.rb:100:in `nodes'
[1m[36mGroup Load (0.3ms)[0m [1m[34mSELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = $1 AND "namespaces"."id" = $2[0m [["type", "Group"], ["id", 100]]
↳ lib/gitlab/graphql/pagination/keyset/connection.rb:100:in `nodes'
Completed 200 OK in 64ms (Views: 0.2ms | ActiveRecord: 5.5ms | Elasticsearch: 0.0ms | Allocations: 60235)
Screenshots
Does this MR meet the acceptance criteria?
Conformity
-
Changelog entry -
Documentation (if required) -
Code review guidelines -
Merge request performance guidelines -
Style guides -
Database guides -
Separation of EE specific content
Availability and Testing
-
Review and add/update tests for this feature/bug. Consider all test levels. See the Test Planning Process. -
Tested in all supported browsers -
Informed Infrastructure department of a default or new setting change, if applicable per definition of done
Security
If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:
-
Label as security and @ mention @gitlab-com/gl-security/appsec
-
The MR includes necessary changes to maintain consistency between UI, API, email, or other methods -
Security reports checked/validated by a reviewer from the AppSec team
Edited by Mark Chao