Fix N+1 queries in work items field linkedItems
When querying multiple work items that include the widget WorkItemWidgetLinkedItems, we execute N+1 queries for the linkedItems list.
extra queries
Expected a maximum of 58 queries, got 69:
Query Diff:
-----------
SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."description_html", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."file_template_project_id", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."max_personal_access_token_lifetime", "namespaces"."push_rule_id", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids", "namespaces"."organization_id" FROM "namespaces"...
-- (expected: 3, got: 4)
WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 92
-- (expected: 2, got: 3)
WHERE "namespaces"."id" = 96
SELECT "project_features".* FROM "project_features"...
-- (expected: 2, got: 3)
WHERE "project_features"."project_id" = 49
SELECT "work_item_widget_definitions".* FROM "work_item_widget_definitions"...
-- (expected: 3, got: 4)
WHERE "work_item_widget_definitions"."disabled" = FALSE AND "work_item_widget_definitions"."work_item_type_id" = 1
SELECT "projects"."id", "projects"."name", "projects"."path", "projects"."description", "projects"."created_at", "projects"."updated_at", "projects"."creator_id", "projects"."namespace_id", "projects"."last_activity_at", "projects"."import_url", "projects"."visibility_level", "projects"."archived", "projects"."avatar", "projects"."merge_requests_template", "projects"."star_count", "projects"."merge_requests_rebase_enabled", "projects"."import_type", "projects"."import_source", "projects"."approvals_before_merge", "projects"."reset_approvals_on_push", "projects"."merge_requests_ff_only_enabled", "projects"."issues_template", "projects"."mirror", "projects"."mirror_last_update_at", "projects"."mirror_last_successful_update_at", "projects"."mirror_user_id", "projects"."shared_runners_enabled", "projects"."runners_token", "projects"."build_allow_git_fetch", "projects"."build_timeout", "projects"."mirror_trigger_builds", "projects"."pending_delete", "projects"."public_builds", "projects"."last_repository_check_failed", "projects"."last_repository_check_at", "projects"."only_allow_merge_if_pipeline_succeeds", "projects"."has_external_issue_tracker", "projects"."repository_storage", "projects"."repository_read_only", "projects"."request_access_enabled", "projects"."has_external_wiki", "projects"."ci_config_path", "projects"."lfs_enabled", "projects"."description_html", "projects"."only_allow_merge_if_all_discussions_are_resolved", "projects"."repository_size_limit", "projects"."printing_merge_request_link_enabled", "projects"."auto_cancel_pending_pipelines", "projects"."service_desk_enabled", "projects"."cached_markdown_version", "projects"."delete_error", "projects"."last_repository_updated_at", "projects"."disable_overriding_approvers_per_merge_request", "projects"."storage_version", "projects"."resolve_outdated_diff_discussions", "projects"."remote_mirror_available_overridden", "projects"."only_mirror_protected_branches", "projects"."pull_mirror_available_overridden", "projects"."jobs_cache_index", "projects"."external_authorization_classification_label", "projects"."mirror_overwrites_diverged_branches", "projects"."pages_https_only", "projects"."external_webhook_token", "projects"."packages_enabled", "projects"."merge_requests_author_approval", "projects"."pool_repository_id", "projects"."runners_token_encrypted", "projects"."bfg_object_map", "projects"."detected_repository_languages", "projects"."merge_requests_disable_committers_approval", "projects"."require_password_to_approve", "projects"."max_pages_size", "projects"."max_artifacts_size", "projects"."pull_mirror_branch_prefix", "projects"."remove_source_branch_after_merge", "projects"."marked_for_deletion_at", "projects"."marked_for_deletion_by_user_id", "projects"."autoclose_referenced_issues", "projects"."suggestion_commit_message", "projects"."project_namespace_id", "projects"."hidden", "projects"."organization_id" FROM "projects"...
-- (expected: 4, got: 6)
WHERE "projects"."id" = 49
SELECT "users"."id", "users"."email", "users"."encrypted_password", "users"."reset_password_token", "users"."reset_password_sent_at", "users"."remember_created_at", "users"."sign_in_count", "users"."current_sign_in_at", "users"."last_sign_in_at", "users"."current_sign_in_ip", "users"."last_sign_in_ip", "users"."created_at", "users"."updated_at", "users"."name", "users"."admin", "users"."projects_limit", "users"."failed_attempts", "users"."locked_at", "users"."username", "users"."can_create_group", "users"."can_create_team", "users"."state", "users"."color_scheme_id", "users"."password_expires_at", "users"."created_by_id", "users"."last_credential_check_at", "users"."avatar", "users"."confirmation_token", "users"."confirmed_at", "users"."confirmation_sent_at", "users"."unconfirmed_email", "users"."hide_no_ssh_key", "users"."admin_email_unsubscribed_at", "users"."notification_email", "users"."hide_no_password", "users"."password_automatically_set", "users"."encrypted_otp_secret", "users"."encrypted_otp_secret_iv", "users"."encrypted_otp_secret_salt", "users"."otp_required_for_login", "users"."otp_backup_codes", "users"."public_email", "users"."dashboard", "users"."project_view", "users"."consumed_timestep", "users"."layout", "users"."hide_project_limit", "users"."note", "users"."unlock_token", "users"."otp_grace_period_started_at", "users"."external", "users"."incoming_email_token", "users"."auditor", "users"."require_two_factor_authentication_from_group", "users"."two_factor_grace_period", "users"."last_activity_on", "users"."notified_of_own_activity", "users"."preferred_language", "users"."theme_id", "users"."accepted_term_id", "users"."feed_token", "users"."private_profile", "users"."roadmap_layout", "users"."include_private_contributions", "users"."commit_email", "users"."group_view", "users"."managing_group_id", "users"."first_name", "users"."last_name", "users"."static_object_token", "users"."role", "users"."user_type", "users"."static_object_token_encrypted", "users"."otp_secret_expires_at", "users"."onboarding_in_progress", "users"."color_mode_id", "users"."composite_identity_enforced" FROM "users"...
-- (expected: 2, got: 3)
WHERE "users"."id" = 166
SELECT "work_item_types".* FROM "work_item_types"...
-- (expected: 3, got: 4)
WHERE "work_item_types"."correct_id" = 1
SELECT "epics".* FROM "epics"...
-- (expected: 1, got: 0)
WHERE "epics"."issue_id" IN (288, 287)
-- (expected: 2, got: 0)
WHERE "epics"."issue_id" = 291
-- (expected: 0, got: 1)
WHERE "epics"."issue_id" IN (294, 288, 287)
-- (expected: 0, got: 1)
WHERE "epics"."issue_id" IN (293, 292)
-- (expected: 0, got: 2)
WHERE "epics"."issue_id" IN (293, 292, 291)
SELECT issues.*, issue_links.id AS issue_link_id, issue_links.link_type as issue_link_type_value, issue_links.target_id as issue_link_source_id, issue_links.created_at as issue_link_created_at, issue_links.updated_at as issue_link_updated_at FROM "issues"...
-- (expected: 0, got: 1)
INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = 294)
OR
(issue_links.target_id = issues.id AND issue_links.source_id = 294) ORDER BY "issue_links"."id" DESC LIMIT 101
SELECT "milestones".* FROM "milestones"...
-- (expected: 2, got: 3)
WHERE "milestones"."id" = 9
To reproduce:
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
--- a/spec/requests/api/graphql/project/work_items_spec.rb (revision f559c6866ae4fef2f55e0d38c103b99c7a2274ef)
+++ b/spec/requests/api/graphql/project/work_items_spec.rb (date 1736253598430)
@@ -503,7 +503,9 @@
post_graphql(query, current_user: current_user)
end
- [item1, item2].each do |item|
+ item3 = create(:work_item, project: project, discussion_locked: true, title: 'item1', labels: [label1])
+
+ [item1, item2, item3].each do |item|
create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to')
create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to')
end