Add limit to security policy projects queried before deleting group

What does this MR do and why?

As a part of Update delete group message (!178971 - merged) we updated the group settings page to check if the group has any security policy projects before enabling the delete button. The query to get the security policy projects can get expensive for large namespaces with large number of security policy projects.

This MR fixes it by limiting the number of security policy projects queried and displaying only 10 of them.

Database queries

Offset

Query
SELECT
    1 AS one 
FROM
    "security_orchestration_policy_configurations" 
WHERE
    "security_orchestration_policy_configurations"."security_policy_management_project_id" IN (
        SELECT
            "projects"."id" 
        FROM
            UNNEST(   COALESCE(     (SELECT
                ids 
            FROM
                (SELECT
                    "namespace_descendants"."all_project_ids" AS ids 
                FROM
                    "namespace_descendants" 
                WHERE
                    "namespace_descendants"."outdated_at" IS NULL 
                    AND "namespace_descendants"."namespace_id" = 9970) cached_query),
                (SELECT
                    ids 
                FROM
                    (SELECT
                        ARRAY_AGG("projects"."id") AS ids 
                    FROM
                        (SELECT
                            "projects"."id" 
                        FROM
                            "projects" 
                        WHERE
                            "projects"."namespace_id" IN (SELECT
                                "namespaces"."id" 
                            FROM
                                UNNEST(   COALESCE(     (SELECT
                                    ids 
                                FROM
                                    (SELECT
                                        "namespace_descendants"."self_and_descendant_group_ids" AS ids 
                                    FROM
                                        "namespace_descendants" 
                                    WHERE
                                        "namespace_descendants"."outdated_at" IS NULL 
                                        AND "namespace_descendants"."namespace_id" = 9970) cached_query),
                                    (SELECT
                                        ids 
                                    FROM
                                        (SELECT
                                            ARRAY_AGG("namespaces"."id") AS ids 
                                        FROM
                                            (SELECT
                                                namespaces.traversal_ids[array_length(namespaces.traversal_ids,
                                                1)] AS id 
                                            FROM
                                                "namespaces" 
                                            WHERE
                                                "namespaces"."type" = 'Group' 
                                                AND (traversal_ids @> ('{9970}'))) namespaces) consistent_query)) ) AS namespaces(id) )) projects) consistent_query)) ) AS projects(id) 
                        ) LIMIT 1 OFFSET 10
Query Plan
 Limit  (cost=1681.42..1682.55 rows=1 width=4) (actual time=35.693..35.697 rows=1 loops=1)
   Buffers: shared hit=99 read=60 dirtied=5
   WAL: records=5 fpi=5 bytes=40033
   I/O Timings: read=32.317 write=0.000
   InitPlan 1 (returns $0)
     ->  Index Scan using namespace_descendants_12_pkey on gitlab_partitions_static.namespace_descendants_12 namespace_descendants  (cost=0.14..3.16 rows=1 width=25) (actual time=1.560..1.561 rows=1 loops=1)
           Index Cond: (namespace_descendants.namespace_id = 9970)
           Filter: (namespace_descendants.outdated_at IS NULL)
           Rows Removed by Filter: 0
           Buffers: shared hit=3 read=2
           I/O Timings: read=1.529 write=0.000
   InitPlan 4 (returns $4)
     ->  Aggregate  (cost=1666.50..1666.51 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
           I/O Timings: read=0.000 write=0.000
           InitPlan 2 (returns $1)
             ->  Index Scan using namespace_descendants_12_pkey on gitlab_partitions_static.namespace_descendants_12 namespace_descendants_1  (cost=0.14..3.16 rows=1 width=707) (actual time=0.000..0.000 rows=0 loops=0)
                   Index Cond: (namespace_descendants_1.namespace_id = 9970)
                   Filter: (namespace_descendants_1.outdated_at IS NULL)
                   Rows Removed by Filter: 0
                   I/O Timings: read=0.000 write=0.000
           InitPlan 3 (returns $2)
             ->  Aggregate  (cost=1627.13..1627.14 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                   I/O Timings: read=0.000 write=0.000
                   ->  Bitmap Heap Scan on public.namespaces  (cost=79.66..1622.24 rows=978 width=28) (actual time=0.000..0.000 rows=0 loops=0)
                         I/O Timings: read=0.000 write=0.000
                         ->  Bitmap Index Scan using index_namespaces_on_traversal_ids_for_groups  (cost=0.00..79.42 rows=978 width=0) (actual time=0.000..0.000 rows=0 loops=0)
                               Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
                               I/O Timings: read=0.000 write=0.000
           ->  Nested Loop  (cost=0.72..35.41 rows=316 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                 I/O Timings: read=0.000 write=0.000
                 ->  HashAggregate  (cost=0.15..0.26 rows=10 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                       Group Key: namespaces_1.id
                       I/O Timings: read=0.000 write=0.000
                       ->  Function Scan on unnest namespaces_1  (cost=0.03..0.13 rows=10 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                             I/O Timings: read=0.000 write=0.000
                 ->  Index Only Scan using index_projects_on_namespace_id_and_id on public.projects projects_1  (cost=0.56..3.20 rows=32 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                       Index Cond: (projects_1.namespace_id = namespaces_1.id)
                       Heap Fetches: 0
                       I/O Timings: read=0.000 write=0.000
   ->  Nested Loop  (cost=0.44..35.48 rows=31 width=4) (actual time=11.820..35.691 rows=11 loops=1)
         Buffers: shared hit=99 read=60 dirtied=5
         WAL: records=5 fpi=5 bytes=40033
         I/O Timings: read=32.317 write=0.000
         ->  HashAggregate  (cost=0.15..0.26 rows=10 width=8) (actual time=9.305..9.326 rows=53 loops=1)
               Group Key: projects.id
               Buffers: shared hit=31 read=14
               I/O Timings: read=7.186 write=0.000
               ->  Function Scan on unnest projects  (cost=0.03..0.13 rows=10 width=8) (actual time=7.815..8.051 rows=5914 loops=1)
                     Buffers: shared hit=31 read=14
                     I/O Timings: read=7.186 write=0.000
         ->  Index Only Scan using index_sop_configurations_project_id_policy_project_id on public.security_orchestration_policy_configurations  (cost=0.29..3.49 rows=3 width=8) (actual time=0.497..0.497 rows=0 loops=53)
               Index Cond: (security_orchestration_policy_configurations.security_policy_management_project_id = projects.id)
               Heap Fetches: 7
               Buffers: shared hit=68 read=46 dirtied=5
               WAL: records=5 fpi=5 bytes=40033
               I/O Timings: read=25.130 write=0.000
Settings: random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off', work_mem = '100MB'

Time: 41.399 ms
  - planning: 5.428 ms
  - execution: 35.971 ms
    - I/O read: 32.317 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 99 (~792.00 KiB) from the buffer pool
  - reads: 60 (~480.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 5 (~40.00 KiB)
  - writes: 0

Limit

Query
SELECT 
    "security_orchestration_policy_configurations"."id", 
    "security_orchestration_policy_configurations"."project_id", 
    "security_orchestration_policy_configurations"."security_policy_management_project_id", 
    "security_orchestration_policy_configurations"."created_at", 
    "security_orchestration_policy_configurations"."updated_at", 
    "security_orchestration_policy_configurations"."configured_at", 
    "security_orchestration_policy_configurations"."namespace_id", 
    "security_orchestration_policy_configurations"."experiments" 
FROM "security_orchestration_policy_configurations" 
WHERE "security_orchestration_policy_configurations"."security_policy_management_project_id" IN (
    SELECT "projects"."id" 
    FROM UNNEST(
        COALESCE(
            (
                SELECT ids 
                FROM (
                    SELECT "namespace_descendants"."all_project_ids" AS ids 
                    FROM "namespace_descendants" 
                    WHERE "namespace_descendants"."outdated_at" IS NULL 
                      AND "namespace_descendants"."namespace_id" = 9970
                ) cached_query
            ),
            (
                SELECT ids 
                FROM (
                    SELECT ARRAY_AGG("projects"."id") AS ids 
                    FROM (
                        SELECT "projects"."id" 
                        FROM "projects" 
                        WHERE "projects"."namespace_id" IN (
                            SELECT "namespaces"."id" 
                            FROM UNNEST(
                                COALESCE(
                                    (
                                        SELECT ids 
                                        FROM (
                                            SELECT "namespace_descendants"."self_and_descendant_group_ids" AS ids 
                                            FROM "namespace_descendants" 
                                            WHERE "namespace_descendants"."outdated_at" IS NULL 
                                              AND "namespace_descendants"."namespace_id" = 9970
                                        ) cached_query
                                    ),
                                    (
                                        SELECT ids 
                                        FROM (
                                            SELECT ARRAY_AGG("namespaces"."id") AS ids 
                                            FROM (
                                                SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id 
                                                FROM "namespaces" 
                                                WHERE "namespaces"."type" = 'Group' 
                                                  AND (traversal_ids @> ('{9970}'))
                                            ) namespaces
                                        ) consistent_query
                                    )
                                )
                            ) AS namespaces(id)
                        )
                    ) projects
                ) consistent_query
            )
        )
    ) AS projects(id)
) 
LIMIT 10;
Query Plan
 Limit  (cost=1670.12..1690.13 rows=10 width=61) (actual time=2.213..4.143 rows=10 loops=1)
   Buffers: shared hit=132 read=3
   I/O Timings: read=1.739 write=0.000
   InitPlan 1 (returns $0)
     ->  Index Scan using namespace_descendants_12_pkey on gitlab_partitions_static.namespace_descendants_12 namespace_descendants  (cost=0.14..3.16 rows=1 width=25) (actual time=0.016..0.017 rows=1 loops=1)
           Index Cond: (namespace_descendants.namespace_id = 9970)
           Filter: (namespace_descendants.outdated_at IS NULL)
           Rows Removed by Filter: 0
           Buffers: shared hit=5
           I/O Timings: read=0.000 write=0.000
   InitPlan 4 (returns $4)
     ->  Aggregate  (cost=1666.50..1666.51 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
           I/O Timings: read=0.000 write=0.000
           InitPlan 2 (returns $1)
             ->  Index Scan using namespace_descendants_12_pkey on gitlab_partitions_static.namespace_descendants_12 namespace_descendants_1  (cost=0.14..3.16 rows=1 width=707) (actual time=0.000..0.000 rows=0 loops=0)
                   Index Cond: (namespace_descendants_1.namespace_id = 9970)
                   Filter: (namespace_descendants_1.outdated_at IS NULL)
                   Rows Removed by Filter: 0
                   I/O Timings: read=0.000 write=0.000
           InitPlan 3 (returns $2)
             ->  Aggregate  (cost=1627.13..1627.14 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                   I/O Timings: read=0.000 write=0.000
                   ->  Bitmap Heap Scan on public.namespaces  (cost=79.66..1622.24 rows=978 width=28) (actual time=0.000..0.000 rows=0 loops=0)
                         I/O Timings: read=0.000 write=0.000
                         ->  Bitmap Index Scan using index_namespaces_on_traversal_ids_for_groups  (cost=0.00..79.42 rows=978 width=0) (actual time=0.000..0.000 rows=0 loops=0)
                               Index Cond: (namespaces.traversal_ids @> '{9970}'::integer[])
                               I/O Timings: read=0.000 write=0.000
           ->  Nested Loop  (cost=0.72..35.41 rows=316 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                 I/O Timings: read=0.000 write=0.000
                 ->  HashAggregate  (cost=0.15..0.26 rows=10 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                       Group Key: namespaces_1.id
                       I/O Timings: read=0.000 write=0.000
                       ->  Function Scan on unnest namespaces_1  (cost=0.03..0.13 rows=10 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                             I/O Timings: read=0.000 write=0.000
                 ->  Index Only Scan using index_projects_on_namespace_id_and_id on public.projects projects_1  (cost=0.56..3.20 rows=32 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                       Index Cond: (projects_1.namespace_id = namespaces_1.id)
                       Heap Fetches: 0
                       I/O Timings: read=0.000 write=0.000
   ->  Nested Loop  (cost=0.44..62.48 rows=31 width=61) (actual time=2.212..4.137 rows=10 loops=1)
         Buffers: shared hit=132 read=3
         I/O Timings: read=1.739 write=0.000
         ->  HashAggregate  (cost=0.15..0.26 rows=10 width=8) (actual time=2.197..2.205 rows=42 loops=1)
               Group Key: projects.id
               Buffers: shared hit=41
               I/O Timings: read=0.000 write=0.000
               ->  Function Scan on unnest projects  (cost=0.03..0.13 rows=10 width=8) (actual time=0.539..0.806 rows=5914 loops=1)
                     Buffers: shared hit=41
                     I/O Timings: read=0.000 write=0.000
         ->  Index Scan using index_sop_configurations_project_id_policy_project_id on public.security_orchestration_policy_configurations  (cost=0.29..6.19 rows=3 width=61) (actual time=0.046..0.046 rows=0 loops=42)
               Index Cond: (security_orchestration_policy_configurations.security_policy_management_project_id = projects.id)
               Buffers: shared hit=91 read=3
               I/O Timings: read=1.739 write=0.000
Settings: effective_cache_size = '472585MB', jit = 'off', work_mem = '100MB', random_page_cost = '1.5', seq_page_cost = '4'

Time: 10.348 ms
  - planning: 5.901 ms
  - execution: 4.447 ms
    - I/O read: 1.739 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 132 (~1.00 MiB) from the buffer pool
  - reads: 3 (~24.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

References

Screenshots or screen recordings

Before After
Screenshot_2025-08-14_at_5.21.48_PM Screenshot_2025-08-15_at_1.08.43_PM

How to set up and validate locally

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #560859 (closed)

Edited by Sashi Kumar Kumaresan

Merge request reports

Loading