Skip to content

Commit 202dfc5

Browse files
authored
Fix SSO queries performance regression (#12509)
This was introduced in #12447. Even locally the previous query is slow, after trying a couple of things I was able to fix it. This is the comparison running explain in production: Current code (rolled back) ``` Out[24]: 'Sort (cost=7188.29..7209.50 rows=8486 width=715)\n Sort Key: projects_project.slug\n -> Hash Left Join (cost=3517.65..3936.04 rows=8486 width=715)\n Hash Cond: (organizations_organization.id = sso_ssointegration.organi zation_id)\n Filter: ((organizations_team_projects.team_id = 2457) OR (organizations_team_projects.team_id = 6031) OR (organizations_team_projects.team_id = 2302) OR (organizations_team_projects.team_id = 5976) OR (organizations_team _projects.team_id = 6032) OR (organizations_organization_projects.organization_id = 5511) OR (organizations_organization_projects.organization_id = 11709) OR (organizations_organization_projects.organization_id = 1236) OR (organizations_org anization_projects.organization_id = 2991) OR (organizations_organization_projects.organization_id = 8804) OR (organizations_organization_projects.organization_id = 12806) OR (organizations_organization_projects.organization_id = 13165) OR ((sso_ssointegration.id IS NOT NULL) AND (hashed SubPlan 1)) OR ((sso_ssointegration.id IS NOT NULL) AND (hashed SubPlan 2)))\n -> Hash Left Join (cost=1639.26..2028.00 rows=11278 width=727)\n Hash Cond: (organizations _organization_projects.organization_id = organizations_organization.id)\n -> Hash Left Join (cost=1033.89..1393.01 rows=11278 width=723)\n Hash Cond: (projects_project.id = organizations_organization_projec ts.project_id)\n -> Hash Right Join (cost=790.75..995.15 rows=11278 width=719)\n Hash Cond: (organizations_team_projects.project_id = projects_project.id)\n -> Seq Scan on organizations_team_projects (cost=0.00..174.78 rows=11278 width=8)\n -> Hash (cost=682.00..682.00 rows=8700 width=715)\n -> Seq Scan on projects_project (cost=0.00..682.00 row s=8700 width=715)\n -> Hash (cost=134.73..134.73 rows=8673 width=8)\n -> Seq Scan on organizations_organization_projects (cost=0.00..134.73 rows=8673 width=8)\n -> Hash (cost=43 5.72..435.72 rows=13572 width=4)\n -> Seq Scan on organizations_organization (cost=0.00..435.72 rows=13572 width=4)\n -> Hash (cost=5.87..5.87 rows=287 width=8)\n -> Seq Scan on sso_ssointegration (cost=0.00..5.87 rows=287 width=8)\n SubPlan 1\n -> Index Scan using oauth_remoterepositoryrelation_user_id_3c2baf85 on oauth_remoterepositoryrelation u0 (cost=0.43..934.05 rows=100 width=4)\n Index Cond: (user_id = 2939)\n Filter: admin\n SubPlan 2\n -> Index Scan using oauth_remoterepositoryrelation_user_id_3c2baf85 on oauth_remoterepositoryrelation u0_1 (cost=0.43..934.05 rows=232 width=4)\n Index Cond: (user_id = 2939)\n Filter: (NOT admin)' ``` Code from main ``` Out[13]: "Sort (cost=303529.85..303541.53 rows=4672 width=715)\n Sort Key: projects_project.slug\n -> Hash Left Join (cost=3726.61..303245.09 rows=4672 width=715)\n Hash Cond: (organizations_organization.id = sso_ssointegration. organization_id)\n Filter: ((((sso_ssointegration.provider)::text = 'allauth'::text) AND (hashed SubPlan 1)) OR ((((organizations_team.access)::text = 'admin'::text) OR ((organizations_team.access)::text = 'readonly'::text)) AND (org anizations_teammember.member_id = 2939) AND (NOT (hashed SubPlan 3))) OR ((organizations_organizationowner.owner_id = 2939) AND (NOT (hashed SubPlan 5))))\n -> Hash Left Join (cost=2782.27..302239.85 rows=10402 width=734)\n Hash Cond: (projects_project.id = organizations_team_projects.project_id)\n -> Nested Loop Left Join (cost=936.03..300038.84 rows=8024 width=723)\n -> Nested Loop Left Join (cost=935.75..297563.62 r ows=7612 width=719)\n -> Merge Left Join (cost=935.45..295996.09 rows=7612 width=719)\n Merge Cond: (projects_project.id = organizations_organization_projects.project_id)\n -> Index Scan using projects_project_pkey on projects_project (cost=935.17..295550.85 rows=7612 width=715)\n Filter: ((hashed SubPlan 1) OR (NOT (hashed SubPlan 3)) OR (NOT (hashe d SubPlan 5)))\n SubPlan 1\n -> Index Scan using oauth_remoterepositoryrelation_user_id_3c2baf85 on oauth_remoterepositoryrelation u0 (cost=0.43..934.05 rows=332 width=4)\n Index Cond: (user_id = 2939)\n SubPlan 3\n -> Nested Loop (cost=10.10..451.03 rows=165 width=4)\n -> Hash Join (cost=9.81..167.34 rows=679 width=12)\n Hash Cond: (u1.organization_id = u3.organization_id)\n -> Seq Scan on organizations_organization_projects u1 (cost=0.00..134.73 rows=8673 width=8)\n -> Hash (cost=6.59..6.59 rows=258 width=4)\n -> Seq Scan on sso_ssointegration u3 (cost=0.00..6.59 rows=258 width=4)\n Filter: ((provider)::text = 'allauth'::text)\n -> Index Only Scan using organizations_organization_pkey on organizations_organization u2 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = u1.organization_id)\n SubPlan 5\n -> Nested Loop (cost=10.10..451.03 rows=165 width=4)\n -> Hash Join (cost=9.81..167.34 rows=679 width=12)\n Hash Cond: (u1_1.organization_id = u3_1.organization_id)\n -> Seq Scan on organizations_organization_projects u1_1 (cost=0.00..134.73 rows=867 3 width=8)\n -> Hash (cost=6.59..6.59 rows=258 width=4)\n -> Seq Scan on sso_ssointegration u3_1 (cost=0.00..6.59 rows=258 width =4)\n Filter: ((provider)::text = 'allauth'::text)\n -> Index Only Scan using organizations_organization_pkey on organizations_orga nization u2_1 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = u1_1.organization_id)\n -> Index Scan using organizations_organization_projects_project_i d on organizations_organization_projects (cost=0.29..328.65 rows=8673 width=8)\n -> Memoize (cost=0.30..0.43 rows=1 width=4)\n Cache Key: organizations_organization_projects.organiz ation_id\n Cache Mode: logical\n -> Index Only Scan using organizations_organization_pkey on organizations_organization (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = organizations_organization_projects.organization_id)\n -> Index Only Scan using organizations_organization_owners_organization_id_user_id_key on organizations_organizationowner (cost=0. 29..0.32 rows=1 width=8)\n Index Cond: (organization_id = organizations_organization.id)\n -> Hash (cost=1705.27..1705.27 rows=11278 width=15)\n -> Hash Right Join (cost=1293.80.. 1705.27 rows=11278 width=15)\n Hash Cond: (organizations_teammember.team_id = organizations_team.id)\n -> Seq Scan on organizations_teammember (cost=0.00..275.29 rows=17629 width=8)\n -> Hash (cost=1152.82..1152.82 rows=11278 width=15)\n -> Hash Left Join (cost=948.44..1152.82 rows=11278 width=15)\n Hash Cond: (organizations_team _projects.team_id = organizations_team.id)\n -> Seq Scan on organizations_team_projects (cost=0.00..174.78 rows=11278 width=8)\n -> Hash (cost=593.75..593.75 rows =28375 width=11)\n -> Seq Scan on organizations_team (cost=0.00..593.75 rows=28375 width=11)\n -> Hash (cost=5.87..5.87 rows=287 width=11)\n -> Seq Scan on sso_ssointegrati on (cost=0.00..5.87 rows=287 width=11)" ``` Fixed code ``` Out[25]: "Index Scan using projects_project_slug_uniq on projects_project (cost=2417.89..5720.59 rows=7612 width=715)\n Filter: ((hashed SubPlan 1) OR (hashed SubPlan 2) OR (hashed SubPlan 3))\n SubPlan 1\n -> Nested Loop Semi Join (cost=10.81..2307.67 rows=1 width=4)\n Join Filter: (v0.remote_repository_id = u0.remote_repository_id)\n -> Nested Loop (cost=10.38..551.09 rows=165 width=8)\n -> Nested Loop (cost=10.10..451.03 rows=16 5 width=4)\n -> Hash Join (cost=9.81..167.34 rows=679 width=12)\n Hash Cond: (v1.organization_id = v3.organization_id)\n -> Seq Scan on organizations_organization _projects v1 (cost=0.00..134.73 rows=8673 width=8)\n -> Hash (cost=6.59..6.59 rows=258 width=4)\n -> Seq Scan on sso_ssointegration v3 (cost=0.00..6.59 rows=258 width=4)\n Filter: ((provider)::text = 'allauth'::text)\n -> Index Only Scan using organizations_organization_pkey on organizations_organization v2 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = v1.organization_id)\n -> Index Scan using projects_project_pkey on projects_project v0 (cost=0.29..0.61 rows=1 width=8)\n Index Cond: (id = v1.project_id)\n -> Materialize (cost=0.43..935.71 rows=332 width=4)\n -> Index Scan using oauth_remoterepositoryrelation_user_id_3c2baf85 on oauth_remoterepositoryrelation u0 (cost=0.43..934.05 rows=332 width=4)\n In dex Cond: (user_id = 2939)\n SubPlan 2\n -> Nested Loop Anti Join (cost=1.86..58.55 rows=1 width=4)\n -> Nested Loop (cost=1.15..57.61 rows=1 width=4)\n -> Nested Loop (cost=0.86..57.25 rows=1 width=4)\n -> Nested Loop (cost=0.57..55.72 rows=4 width=8)\n -> Index Scan using organizations_team_members_user_id on organizations_teammember v3_1 (cost=0.29..14.17 rows=5 width=4)\n Index Cond: (member_id = 2939)\n -> Index Scan using organizations_team_pkey on organizations_team v2_1 (cost=0.29..8.31 rows=1 width=4)\n Index Cond: (id = v3_ 1.team_id)\n Filter: (((access)::text = 'admin'::text) OR ((access)::text = 'readonly'::text))\n -> Index Only Scan using organizations_team_projects_team_id_project_id_key on organizat ions_team_projects v1_1 (cost=0.29..0.35 rows=3 width=8)\n Index Cond: (team_id = v2_1.id)\n -> Index Only Scan using projects_project_pkey on projects_project v0_1 (cost=0.29..0.36 rows=1 width= 4)\n Index Cond: (id = v1_1.project_id)\n -> Nested Loop (cost=0.72..0.93 rows=1 width=4)\n -> Nested Loop (cost=0.57..0.76 rows=1 width=12)\n -> Index Scan using organi zations_organization_projects_project_id on organizations_organization_projects u1 (cost=0.29..0.34 rows=1 width=8)\n Index Cond: (project_id = v0_1.id)\n -> Index Only Scan using organizati ons_organization_pkey on organizations_organization u2 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = u1.organization_id)\n -> Index Scan using sso_ssointegration_organization_id_key on sso_ ssointegration u3 (cost=0.15..0.17 rows=1 width=4)\n Index Cond: (organization_id = u1.organization_id)\n Filter: ((provider)::text = 'allauth'::text)\n SubPlan 3\n -> Nested Loop Anti Join ( cost=1.86..51.38 rows=4 width=4)\n -> Nested Loop (cost=1.14..47.63 rows=4 width=4)\n -> Nested Loop (cost=0.86..46.14 rows=4 width=4)\n -> Nested Loop (cost=0.57..41.54 rows=11 width=12)\n -> Index Scan using organizations_organization_owners_user_id on organizations_organizationowner v3_2 (cost=0.29..10.97 rows=7 width=4)\n Index Cond: (owner_id = 2939)\n -> Index Only Scan using organizations_organization_proje_organization_id_project_id_key on organizations_organization_projects v1_2 (cost=0.29..4.34 rows=3 width=8)\n Index Cond: (orga nization_id = v3_2.organization_id)\n -> Index Only Scan using organizations_organization_pkey on organizations_organization v2_2 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = v1_2.or ganization_id)\n -> Index Only Scan using projects_project_pkey on projects_project v0_2 (cost=0.29..0.37 rows=1 width=4)\n Index Cond: (id = v1_2.project_id)\n -> Nested Loop (cost=0.72..0.9 3 rows=1 width=4)\n -> Nested Loop (cost=0.57..0.76 rows=1 width=12)\n -> Index Scan using organizations_organization_projects_project_id on organizations_organization_projects u1_1 (cost=0.29..0.34 r ows=1 width=8)\n Index Cond: (project_id = v0_2.id)\n -> Index Only Scan using organizations_organization_pkey on organizations_organization u2_1 (cost=0.29..0.42 rows=1 width=4)\n Index Cond: (id = u1_1.organization_id)\n -> Index Scan using sso_ssointegration_organization_id_key on sso_ssointegration u3_1 (cost=0.15..0.17 rows=1 width=4)\n Index Cond: (organiza tion_id = u1_1.organization_id)\n Filter: ((provider)::text = 'allauth'::text)" ``` The query generated before (slow) ``` Out[12]: 'SELECT "projects_project"."id", "projects_project"."pub_date", "projects_project"."modified_date", "projects_project"."name", "projects_project"."slug", "projects_project"."description", "projects_project"."git_checkout_command", "projects_project"."repo", "projects_project"."repo_type", "projects_project"."project_url", "projects_project"."canonical_url", "projects_project"."versioning_scheme", "projects_project"."single_version", "projects_project"."default_versio n", "projects_project"."default_branch", "projects_project"."custom_prefix", "projects_project"."custom_subproject_prefix", "projects_project"."external_builds_enabled", "projects_project"."external_builds_privacy_level", "projects_project" ."show_build_overview_in_comment", "projects_project"."cdn_enabled", "projects_project"."analytics_code", "projects_project"."analytics_disabled", "projects_project"."container_image", "projects_project"."container_mem_limit", "projects_pro ject"."container_time_limit", "projects_project"."build_queue", "projects_project"."max_concurrent_builds", "projects_project"."allow_promos", "projects_project"."ad_free", "projects_project"."is_spam", "projects_project"."show_version_warn ing", "projects_project"."readthedocs_yaml_path", "projects_project"."featured", "projects_project"."skip", "projects_project"."delisted", "projects_project"."privacy_level", "projects_project"."language", "projects_project"."programming_la nguage", "projects_project"."main_language_project_id", "projects_project"."has_valid_webhook", "projects_project"."has_valid_clone", "projects_project"."remote_repository_id", "projects_project"."documentation_type", "projects_project"."ha s_ssh_key_with_write_access", "projects_project"."latest_build_id" FROM "projects_project" LEFT OUTER JOIN "organizations_organization_projects" ON ("projects_project"."id" = "organizations_organization_projects"."project_id") LEFT OUTER JO IN "organizations_organization" ON ("organizations_organization_projects"."organization_id" = "organizations_organization"."id") LEFT OUTER JOIN "sso_ssointegration" ON ("organizations_organization"."id" = "sso_ssointegration"."organization _id") LEFT OUTER JOIN "organizations_team_projects" ON ("projects_project"."id" = "organizations_team_projects"."project_id") LEFT OUTER JOIN "organizations_team" ON ("organizations_team_projects"."team_id" = "organizations_team"."id") LEFT OUTER JOIN "organizations_teammember" ON ("organizations_team"."id" = "organizations_teammember"."team_id") LEFT OUTER JOIN "organizations_organizationowner" ON ("organizations_organization"."id" = "organizations_organizationowner"."organi zation_id") WHERE (("sso_ssointegration"."provider" = allauth AND "projects_project"."remote_repository_id" IN (SELECT U0."remote_repository_id" AS "remote_repository" FROM "oauth_remoterepositoryrelation" U0 WHERE U0."user_id" = 2939)) OR (("organizations_team"."access" = admin OR "organizations_team"."access" = readonly) AND "organizations_teammember"."member_id" = 2939 AND NOT (EXISTS(SELECT 1 AS "a" FROM "organizations_organization_projects" U1 INNER JOIN "organizations_o rganization" U2 ON (U1."organization_id" = U2."id") INNER JOIN "sso_ssointegration" U3 ON (U2."id" = U3."organization_id") WHERE (U3."provider" = allauth AND U1."project_id" = ("projects_project"."id")) LIMIT 1))) OR ("organizations_organiz ationowner"."owner_id" = 2939 AND NOT (EXISTS(SELECT 1 AS "a" FROM "organizations_organization_projects" U1 INNER JOIN "organizations_organization" U2 ON (U1."organization_id" = U2."id") INNER JOIN "sso_ssointegration" U3 ON (U2."id" = U3." organization_id") WHERE (U3."provider" = allauth AND U1."project_id" = ("projects_project"."id")) LIMIT 1)))) ORDER BY "projects_project"."slug" ASC' ``` The query now ``` Out[11]: 'SELECT "projects_project"."id", "projects_project"."pub_date", "projects_project"."modified_date", "projects_project"."name", "projects_project"."slug", "projects_project"."description", "projects_project"."git_checkout_command", "projects_project"."repo", "projects_project"."repo_type", "projects_project"."project_url", "projects_project"."canonical_url", "projects_project"."versioning_scheme", "projects_project"."single_version", "projects_project"."default_versio n", "projects_project"."default_branch", "projects_project"."custom_prefix", "projects_project"."custom_subproject_prefix", "projects_project"."external_builds_enabled", "projects_project"."external_builds_privacy_level", "projects_project" ."show_build_overview_in_comment", "projects_project"."cdn_enabled", "projects_project"."analytics_code", "projects_project"."analytics_disabled", "projects_project"."container_image", "projects_project"."container_mem_limit", "projects_pro ject"."container_time_limit", "projects_project"."build_queue", "projects_project"."max_concurrent_builds", "projects_project"."allow_promos", "projects_project"."ad_free", "projects_project"."is_spam", "projects_project"."show_version_warn ing", "projects_project"."readthedocs_yaml_path", "projects_project"."featured", "projects_project"."skip", "projects_project"."delisted", "projects_project"."privacy_level", "projects_project"."language", "projects_project"."programming_la nguage", "projects_project"."main_language_project_id", "projects_project"."has_valid_webhook", "projects_project"."has_valid_clone", "projects_project"."remote_repository_id", "projects_project"."documentation_type", "projects_project"."ha s_ssh_key_with_write_access", "projects_project"."latest_build_id" FROM "projects_project" WHERE ("projects_project"."id" IN (SELECT V0."id" FROM "projects_project" V0 INNER JOIN "organizations_organization_projects" V1 ON (V0."id" = V1."pr oject_id") INNER JOIN "organizations_organization" V2 ON (V1."organization_id" = V2."id") INNER JOIN "sso_ssointegration" V3 ON (V2."id" = V3."organization_id") WHERE (V3."provider" = allauth AND V0."remote_repository_id" IN (SELECT U0."rem ote_repository_id" AS "remote_repository" FROM "oauth_remoterepositoryrelation" U0 WHERE U0."user_id" = 2939))) OR "projects_project"."id" IN (SELECT V0."id" FROM "projects_project" V0 INNER JOIN "organizations_team_projects" V1 ON (V0."id" = V1."project_id") INNER JOIN "organizations_team" V2 ON (V1."team_id" = V2."id") INNER JOIN "organizations_teammember" V3 ON (V2."id" = V3."team_id") WHERE ((V2."access" = admin OR V2."access" = readonly) AND V3."member_id" = 2939 AND NOT (EXISTS(SELECT 1 AS "a" FROM "organizations_organization_projects" U1 INNER JOIN "organizations_organization" U2 ON (U1."organization_id" = U2."id") INNER JOIN "sso_ssointegration" U3 ON (U2."id" = U3."organization_id") WHERE (U3."provider " = allauth AND U1."project_id" = (V0."id")) LIMIT 1)))) OR "projects_project"."id" IN (SELECT V0."id" FROM "projects_project" V0 INNER JOIN "organizations_organization_projects" V1 ON (V0."id" = V1."project_id") INNER JOIN "organizations_o rganization" V2 ON (V1."organization_id" = V2."id") INNER JOIN "organizations_organizationowner" V3 ON (V2."id" = V3."organization_id") WHERE (V3."owner_id" = 2939 AND NOT (EXISTS(SELECT 1 AS "a" FROM "organizations_organization_projects" U 1 INNER JOIN "organizations_organization" U2 ON (U1."organization_id" = U2."id") INNER JOIN "sso_ssointegration" U3 ON (U2."id" = U3."organization_id") WHERE (U3."provider" = allauth AND U1."project_id" = (V0."id")) LIMIT 1))))) ORDER BY "p rojects_project"."slug" ASC' ```
1 parent 2df4d99 commit 202dfc5

File tree

1 file changed

+15
-4
lines changed

1 file changed

+15
-4
lines changed

readthedocs/core/permissions.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,39 @@ def projects(cls, user, admin=False, member=False):
4545
# when we aren't using organizations.
4646
return user.projects.all()
4747

48+
projects_from_sso = Project.objects.none()
49+
projects_from_teams = Project.objects.none()
50+
projects_from_owners = Project.objects.none()
51+
4852
if admin or member:
4953
# Projects from VCS SSO.
50-
projects |= cls._get_projects_for_sso_user(user, admin=admin, member=member)
54+
projects_from_sso = cls._get_projects_for_sso_user(user, admin=admin, member=member)
5155

5256
# Projects from teams that don't have VCS SSO enabled.
5357
filter = Q()
5458
if admin:
5559
filter |= Q(teams__access=ADMIN_ACCESS)
5660
if member:
5761
filter |= Q(teams__access=READ_ONLY_ACCESS)
58-
projects |= Project.objects.filter(filter, teams__members=user).exclude(
62+
projects_from_teams = Project.objects.filter(filter, teams__members=user).exclude(
5963
organizations__ssointegration__provider=SSOIntegration.PROVIDER_ALLAUTH,
6064
)
6165

6266
# Projects from organizations that don't have VCS SSO enabled,
6367
# where the user is an owner.
6468
if admin:
65-
projects |= Project.objects.filter(organizations__owners=user).exclude(
69+
projects_from_owners = Project.objects.filter(organizations__owners=user).exclude(
6670
organizations__ssointegration__provider=SSOIntegration.PROVIDER_ALLAUTH,
6771
)
6872

69-
return projects
73+
# NOTE: We use a filter with Q objects instead of several union operations
74+
# (e.g projects_from_sso | projects_from_teams | projects_from_owners),
75+
# as the latter can generate a very complex and slow query.
76+
return Project.objects.filter(
77+
Q(id__in=projects_from_sso)
78+
| Q(id__in=projects_from_teams)
79+
| Q(id__in=projects_from_owners)
80+
)
7081

7182
@classmethod
7283
def organizations(cls, user, owner=False, member=False):

0 commit comments

Comments
 (0)