Increase the performance of the PersonalAccessTokensFinder for the Credentials Inventory
- Related to #524159 (closed)
- Blocked by this !193504 (merged) to remove 2 unused indexes on the PersonalAccessTokens table
- Blocks #511922
What?
- Increases the performance of the PatFinder by about 97.5-99.2% using the efficient InOperatorOptimization
- Uses a gitlab.com top-level-group with 30,523 users for database testing
- Creates a new
::CredentialsInventoryPersonalAccessTokensFinder
in/ee
directory - Adds 4 composite indexes for the 4 sorting orders we use on group_credentials_inventory (expires_at, created_at, last_used at & name)
- Made to be compatible with the upcoming credentials inventory management API, current
/admin
and/group
level credentials inventory - Flagged behind the
credentials_inventory_pat_finder
feature flag
Follow-ups
Index & limit for the name column
- Due to the lack of a limit on the PAT
name
column, the following MRs will be created before adding an index for the account for the 4th sorting order:- Add the limit constraint
- Add the 4th sorting index
CREATE INDEX index_personal_access_tokens_on_user_id_and_name_and_pat_id ON personal_access_tokens USING btree(user_id, name, id DESC) WHERE impersonation = false
Credentials Inventory API
- Make the /manage/personal_access_tokens GET endpoint use the credentials_inventory_pat_finder (!194865 (merged))
Why?
- Our 15-second database timeout constraints have occasionally been hit for multiple large gitlab.com customers (See group timeouts) and consistently for larger namespaces
- Unblocks the rollout of the credentials inventory management API
Database Review
Details
- The query will be focused on the gitlab.com credentials inventory query
- The only difference is that the array_scope is
User.human
for self-managed (no current issues from customers), butgroup.enterprise_user_details
for gitlab.com (our focus)
- The only difference is that the array_scope is
- Test enterprise_group_id = 4249178 (>~7k billable users, >~30k total users)
- 15 seconds database timeout constraint
Old Query
SELECT
personal_access_tokens.id AS t0_r0,
personal_access_tokens.user_id AS t0_r1,
personal_access_tokens.name AS t0_r2,
personal_access_tokens.revoked AS t0_r3,
personal_access_tokens.expires_at AS t0_r4,
personal_access_tokens.created_at AS t0_r5,
personal_access_tokens.updated_at AS t0_r6,
personal_access_tokens.scopes AS t0_r7,
personal_access_tokens.impersonation AS t0_r8,
personal_access_tokens.token_digest AS t0_r9,
personal_access_tokens.expire_notification_delivered AS t0_r10,
personal_access_tokens.last_used_at AS t0_r11,
personal_access_tokens.after_expiry_notification_delivered AS t0_r12,
personal_access_tokens.previous_personal_access_token_id AS t0_r13,
personal_access_tokens.organization_id AS t0_r14,
personal_access_tokens.seven_days_notification_sent_at AS t0_r15,
personal_access_tokens.thirty_days_notification_sent_at AS t0_r16,
personal_access_tokens.sixty_days_notification_sent_at AS t0_r17,
personal_access_tokens.description AS t0_r18,
users.id AS t1_r0,
users.email AS t1_r1,
users.encrypted_password AS t1_r2,
users.reset_password_token AS t1_r3,
users.reset_password_sent_at AS t1_r4,
users.remember_created_at AS t1_r5,
users.sign_in_count AS t1_r6,
users.current_sign_in_at AS t1_r7,
users.last_sign_in_at AS t1_r8,
users.current_sign_in_ip AS t1_r9,
users.last_sign_in_ip AS t1_r10,
users.created_at AS t1_r11,
users.updated_at AS t1_r12,
users.name AS t1_r13,
users.admin AS t1_r14,
users.projects_limit AS t1_r15,
users.failed_attempts AS t1_r16,
users.locked_at AS t1_r17,
users.username AS t1_r18,
users.can_create_group AS t1_r19,
users.can_create_team AS t1_r20,
users.state AS t1_r21,
users.color_scheme_id AS t1_r22,
users.password_expires_at AS t1_r23,
users.created_by_id AS t1_r24,
users.last_credential_check_at AS t1_r25,
users.avatar AS t1_r26,
users.confirmation_token AS t1_r27,
users.confirmed_at AS t1_r28,
users.confirmation_sent_at AS t1_r29,
users.unconfirmed_email AS t1_r30,
users.hide_no_ssh_key AS t1_r31,
users.admin_email_unsubscribed_at AS t1_r32,
users.notification_email AS t1_r33,
users.hide_no_password AS t1_r34,
users.password_automatically_set AS t1_r35,
users.encrypted_otp_secret AS t1_r36,
users.encrypted_otp_secret_iv AS t1_r37,
users.encrypted_otp_secret_salt AS t1_r38,
users.otp_required_for_login AS t1_r39,
users.otp_backup_codes AS t1_r40,
users.public_email AS t1_r41,
users.dashboard AS t1_r42,
users.project_view AS t1_r43,
users.consumed_timestep AS t1_r44,
users.layout AS t1_r45,
users.hide_project_limit AS t1_r46,
users.note AS t1_r47,
users.unlock_token AS t1_r48,
users.otp_grace_period_started_at AS t1_r49,
users.external AS t1_r50,
users.incoming_email_token AS t1_r51,
users.auditor AS t1_r52,
users.require_two_factor_authentication_from_group AS t1_r53,
users.two_factor_grace_period AS t1_r54,
users.last_activity_on AS t1_r55,
users.notified_of_own_activity AS t1_r56,
users.preferred_language AS t1_r57,
users.theme_id AS t1_r58,
users.accepted_term_id AS t1_r59,
users.feed_token AS t1_r60,
users.private_profile AS t1_r61,
users.roadmap_layout AS t1_r62,
users.include_private_contributions AS t1_r63,
users.commit_email AS t1_r64,
users.group_view AS t1_r65,
users.managing_group_id AS t1_r66,
users.first_name AS t1_r67,
users.last_name AS t1_r68,
users.static_object_token AS t1_r69,
users.role AS t1_r70,
users.user_type AS t1_r71,
users.static_object_token_encrypted AS t1_r72,
users.otp_secret_expires_at AS t1_r73,
users.onboarding_in_progress AS t1_r74,
users.color_mode_id AS t1_r75,
users.composite_identity_enforced AS t1_r76
FROM
personal_access_tokens
LEFT JOIN users ON users.id = personal_access_tokens.user_id
WHERE
personal_access_tokens.user_id IN (
SELECT
users.id
FROM
users
JOIN user_details ON users.id = user_details.user_id
WHERE
user_details.enterprise_group_id = 4249178
) AND
personal_access_tokens.impersonation = false AND
users.user_type = 0
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
LIMIT 21
OFFSET 0
Optimized Query
SELECT
*
FROM
(
WITH RECURSIVE array_cte AS MATERIALIZED (
SELECT
user_details.user_id
FROM
user_details
WHERE
user_details.enterprise_group_id = 4249178
), recursive_keyset_cte AS (
(
SELECT
NULL::personal_access_tokens AS records,
array_cte_user_id_array,
personal_access_tokens_expires_at_array,
personal_access_tokens_id_array,
0::pg_catalog.int8 AS count
FROM
(
SELECT
array_agg( array_cte.user_id ) AS array_cte_user_id_array,
array_agg( personal_access_tokens.expires_at ) AS personal_access_tokens_expires_at_array,
array_agg( personal_access_tokens.id ) AS personal_access_tokens_id_array
FROM
(
SELECT
array_cte.user_id
FROM
array_cte
) AS array_cte
LEFT JOIN LATERAL (
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
personal_access_tokens
WHERE
personal_access_tokens.user_id = array_cte.user_id AND
personal_access_tokens.impersonation = false
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
LIMIT 1
) AS personal_access_tokens ON true
WHERE
personal_access_tokens.id IS NOT NULL
) AS array_scope_lateral_query
LIMIT 1
)
UNION ALL
(
SELECT
(
SELECT
personal_access_tokens
FROM
personal_access_tokens
WHERE
personal_access_tokens.id = recursive_keyset_cte.personal_access_tokens_id_array[position]
LIMIT 1
),
array_cte_user_id_array,
recursive_keyset_cte.personal_access_tokens_expires_at_array[:position_query.position - 1] || next_cursor_values.expires_at || recursive_keyset_cte.personal_access_tokens_expires_at_array[position_query.position + 1:],
recursive_keyset_cte.personal_access_tokens_id_array[:position_query.position - 1] || next_cursor_values.id || recursive_keyset_cte.personal_access_tokens_id_array[position_query.position + 1:],
recursive_keyset_cte.count + 1
FROM
recursive_keyset_cte,
LATERAL (
SELECT
expires_at,
id,
position
FROM
unnest( personal_access_tokens_expires_at_array, personal_access_tokens_id_array ) WITH ORDINALITY AS u ( expires_at, id, position )
WHERE
id IS NOT NULL
ORDER BY
1 ASC NULLS LAST,
2 DESC
LIMIT 1
) AS position_query,
LATERAL (
SELECT
record.expires_at,
record.id
FROM
(
VALUES
( NULL, NULL )
) AS nulls
LEFT JOIN (
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
(
(
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
personal_access_tokens
WHERE
personal_access_tokens.user_id = recursive_keyset_cte.array_cte_user_id_array[position] AND
personal_access_tokens.impersonation = false AND
recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NULL AND
personal_access_tokens.expires_at IS NULL AND
personal_access_tokens.id < recursive_keyset_cte.personal_access_tokens_id_array[position]
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
)
UNION ALL
(
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
personal_access_tokens
WHERE
personal_access_tokens.user_id = recursive_keyset_cte.array_cte_user_id_array[position] AND
personal_access_tokens.impersonation = false AND
recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND
personal_access_tokens.expires_at IS NULL
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
)
UNION ALL
(
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
personal_access_tokens
WHERE
personal_access_tokens.user_id = recursive_keyset_cte.array_cte_user_id_array[position] AND
personal_access_tokens.impersonation = false AND
recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND
personal_access_tokens.expires_at > recursive_keyset_cte.personal_access_tokens_expires_at_array[position]
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
)
UNION ALL
(
SELECT
personal_access_tokens.expires_at AS expires_at,
personal_access_tokens.id AS id
FROM
personal_access_tokens
WHERE
personal_access_tokens.user_id = recursive_keyset_cte.array_cte_user_id_array[position] AND
personal_access_tokens.impersonation = false AND
recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND
personal_access_tokens.expires_at = recursive_keyset_cte.personal_access_tokens_expires_at_array[position] AND
personal_access_tokens.id < recursive_keyset_cte.personal_access_tokens_id_array[position]
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
)
) AS personal_access_tokens
ORDER BY
personal_access_tokens.expires_at ASC,
personal_access_tokens.id DESC
LIMIT 1
) AS record ON true
LIMIT 1
) AS next_cursor_values
)
)
SELECT
records.*
FROM
recursive_keyset_cte AS personal_access_tokens
WHERE
count <> 0
) AS personal_access_tokens
LIMIT 21
OFFSET 0
Base PAT Query Results
Cold Cache | Warm Cache |
---|---|
1.130 min | 549.268 ms |
Indexes
-
expires_asc, id_desc
is our most common tie-breaker, so we can addDESC
to its index - the last 2 columns must be in the form (timestamp, tie_breaker)
- PG's default order sort is
ASC NULLS LAST
exec CREATE INDEX index_personal_access_tokens_on_user_id_and_expires_at_and_pat_id ON personal_access_tokens USING btree(user_id, expires_at, id DESC) WHERE impersonation = false
exec CREATE INDEX index_personal_access_tokens_on_user_id_and_last_used_at_and_pat_id ON personal_access_tokens USING btree(user_id, last_used_at, id) WHERE impersonation = false
exec CREATE INDEX index_personal_access_tokens_on_user_id_and_created_at_and_pat_id ON personal_access_tokens USING btree(user_id, created_at, id) WHERE impersonation = false
Optimized PAT Query Results
Initial Query w/ Sorting Indexes | Cold Cache | Warm Cache |
---|---|---|
user_id, expires_at, id DESC (Default) | 588.315 ms | 230.366 ms |
user_id, last_used_at, id | 1.710 s | 238.518 ms |
user_id, created_at, id | 585.158 ms | 261.970 ms |
Database Migrations
bin/rails db:migrate
hakeem@habdul-razak--20240918-F45T4 gitlab % bin/rails db:migrate
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41935
main: == 20250612052552 AddIndexToPatsOnExpiresAt: migrating ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0083s
main: -- index_exists?(:personal_access_tokens, [:user_id, :expires_at, :id], {:order=>{:id=>:desc}, :where=>"impersonation = false", :name=>"index_pats_on_user_id_and_expires_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0031s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- add_index(:personal_access_tokens, [:user_id, :expires_at, :id], {:order=>{:id=>:desc}, :where=>"impersonation = false", :name=>"index_pats_on_user_id_and_expires_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0052s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612052552 AddIndexToPatsOnExpiresAt: migrated (0.0310s) ===============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41935
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41936
ci: == 20250612052552 AddIndexToPatsOnExpiresAt: migrating ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :expires_at, :id], {:order=>{:id=>:desc}, :where=>"impersonation = false", :name=>"index_pats_on_user_id_and_expires_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0086s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :expires_at, :id], {:order=>{:id=>:desc}, :where=>"impersonation = false", :name=>"index_pats_on_user_id_and_expires_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0038s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250612052552 AddIndexToPatsOnExpiresAt: migrated (0.0253s) ===============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41936
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41938
main: == 20250612065613 AddIndexToPatsOnLastUsedAt: migrating =======================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0003s
main: -- index_exists?(:personal_access_tokens, [:user_id, :last_used_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0034s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- add_index(:personal_access_tokens, [:user_id, :last_used_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0030s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612065613 AddIndexToPatsOnLastUsedAt: migrated (0.0145s) ==============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41938
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41940
ci: == 20250612065613 AddIndexToPatsOnLastUsedAt: migrating =======================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0004s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :last_used_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0037s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :last_used_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0033s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250612065613 AddIndexToPatsOnLastUsedAt: migrated (0.0174s) ==============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41940
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41942
main: == 20250612065637 AddIndexToPatsOnCreatedAt: migrating ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0003s
main: -- index_exists?(:personal_access_tokens, [:user_id, :created_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_created_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0035s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- add_index(:personal_access_tokens, [:user_id, :created_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_created_at_and_pat_id", :algorithm=>:concurrently})
main: -> 0.0038s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612065637 AddIndexToPatsOnCreatedAt: migrated (0.0152s) ===============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41942
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41943
ci: == 20250612065637 AddIndexToPatsOnCreatedAt: migrating ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :created_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_created_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0039s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :created_at, :id], {:where=>"impersonation = false", :name=>"index_pats_on_user_id_and_created_at_and_pat_id", :algorithm=>:concurrently})
ci: -> 0.0055s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0007s
ci: == 20250612065637 AddIndexToPatsOnCreatedAt: migrated (0.0212s) ===============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 41943
Database Rollback
index_pats_on_user_id_and_expires_at_and_pat_id
VERSION=20250612070642 bin/rails db:migrate:down:main && VERSION=20250612070642 bin/rails db:migrate:down:ci
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250612070642 bin/rails db:migrate:down:main && VERSION=20250612070642 bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43726
main: == 20250612070642 AddIndexToPatsOnExpiresAt: reverting ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0200s
main: -- indexes(:personal_access_tokens)
main: -> 0.0049s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_expires_at_and_pat_id"})
main: -> 0.0016s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612070642 AddIndexToPatsOnExpiresAt: reverted (0.0408s) ===============
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43726
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43804
ci: == 20250612070642 AddIndexToPatsOnExpiresAt: reverting ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0213s
ci: -- indexes(:personal_access_tokens)
ci: -> 0.0062s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0004s
ci: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_expires_at_and_pat_id"})
ci: -> 0.0020s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250612070642 AddIndexToPatsOnExpiresAt: reverted (0.0464s) ===============
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43804
index_pats_on_user_id_and_last_used_at_and_pat_id
VERSION=20250612065613 bin/rails db:migrate:down:main && VERSION=20250612065613 bin/rails db:migrate:down:ci
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250612065613 bin/rails db:migrate:down:main && VERSION=20250612065613 bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43971
main: == 20250612065613 AddIndexToPatsOnLastUsedAt: reverting =======================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0199s
main: -- indexes(:personal_access_tokens)
main: -> 0.0046s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id"})
main: -> 0.0021s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612065613 AddIndexToPatsOnLastUsedAt: reverted (0.0417s) ==============
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 43971
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44027
ci: == 20250612065613 AddIndexToPatsOnLastUsedAt: reverting =======================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0173s
ci: -- indexes(:personal_access_tokens)
ci: -> 0.0049s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_last_used_at_and_pat_id"})
ci: -> 0.0019s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250612065613 AddIndexToPatsOnLastUsedAt: reverted (0.0412s) ==============
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44027
index_pats_on_user_id_and_created_at_and_pat_id
VERSION=20250612065637 bin/rails db:migrate:down:main && VERSION=20250612065637 bin/rails db:migrate:down:ci
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250612065637 bin/rails db:migrate:down:main && VERSION=20250612065637 bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44147
main: == 20250612065637 AddIndexToPatsOnCreatedAt: reverting ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0165s
main: -- indexes(:personal_access_tokens)
main: -> 0.0034s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_created_at_and_pat_id"})
main: -> 0.0017s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250612065637 AddIndexToPatsOnCreatedAt: reverted (0.0342s) ===============
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44147
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44212
ci: == 20250612065637 AddIndexToPatsOnCreatedAt: reverting ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0180s
ci: -- indexes(:personal_access_tokens)
ci: -> 0.0042s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_pats_on_user_id_and_created_at_and_pat_id"})
ci: -> 0.0019s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250612065637 AddIndexToPatsOnCreatedAt: reverted (0.0637s) ===============
ci: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 44212
How to set up and validate locally
- Navigate to the admin credentials inventory: https://gdk.test:3443/admin/credentials
- Verify that filters & sorting work
- Turn on SAAS Mode
- Create a group with a premium/ultimate plan
- As a top-level group owner:
- Add multiple users to the group and make them enterprise users
gdk rails console
group = Group.find(group_id)
user = User.find_by(username: 'regular_user')
user.enterprise_group_id = group.id
user.save!
- Confirm an Enterprise Users badge next to your enterprise users
- Impersonate them and make some PATs
- Navigate to the group credentials inventory: https://gdk.test:3443/groups/service-accounts-group/-/security/credentials
- Test the PAT filters and sorting to confirm that its behaviour is identical to the non-optimized version
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.
Iterations
Draft 1
Related to #524159 (closed)
Blocked by this !193504 (merged) to remove 2 unused indexes on the PersonalAccessTokens table
What?
- Increases the performance of the PatFinder by ~98.88% using the efficient InOperatorOptimization
- Implements a scalable-optimization strategy that switches to an optimized PersonalAccessTokenFinder once a gitlab.com group surpasses a set amount of enterprise users
- This was estimated from these group timeouts, this larger namespace and the average count of their users.
- Adds 4 composite indexes for the 4 sorting orders we use on group_credentials_inventory (expires_at, created_at, updated_at & name)
- Made to be compatible with the credentials inventory management API
Why?
- Our 15-second database timeout constraints have occasionally been hit for multiple gitlab.com customers & internal groups
- Unblocks the rollout of the credentials inventory management API
Database
Details
- Test enterprise_group_id = 4249178 (7k billable users)
- 15 seconds database timeout constraint
Old Query for joe.ai
explain SELECT "personal_access_tokens"."id" AS t0_r0, "personal_access_tokens"."user_id" AS t0_r1, "personal_access_tokens"."name" AS t0_r2, "personal_access_tokens"."revoked" AS t0_r3, "personal_access_tokens"."expires_at" AS t0_r4, "personal_access_tokens"."created_at" AS t0_r5, "personal_access_tokens"."updated_at" AS t0_r6, "personal_access_tokens"."scopes" AS t0_r7, "personal_access_tokens"."impersonation" AS t0_r8, "personal_access_tokens"."token_digest" AS t0_r9, "personal_access_tokens"."expire_notification_delivered" AS t0_r10, "personal_access_tokens"."last_used_at" AS t0_r11, "personal_access_tokens"."after_expiry_notification_delivered" AS t0_r12, "personal_access_tokens"."previous_personal_access_token_id" AS t0_r13, "personal_access_tokens"."organization_id" AS t0_r14, "personal_access_tokens"."seven_days_notification_sent_at" AS t0_r15, "personal_access_tokens"."thirty_days_notification_sent_at" AS t0_r16, "personal_access_tokens"."sixty_days_notification_sent_at" AS t0_r17, "personal_access_tokens"."description" AS t0_r18, "users"."id" AS t1_r0, "users"."email" AS t1_r1, "users"."encrypted_password" AS t1_r2, "users"."reset_password_token" AS t1_r3, "users"."reset_password_sent_at" AS t1_r4, "users"."remember_created_at" AS t1_r5, "users"."sign_in_count" AS t1_r6, "users"."current_sign_in_at" AS t1_r7, "users"."last_sign_in_at" AS t1_r8, "users"."current_sign_in_ip" AS t1_r9, "users"."last_sign_in_ip" AS t1_r10, "users"."created_at" AS t1_r11, "users"."updated_at" AS t1_r12, "users"."name" AS t1_r13, "users"."admin" AS t1_r14, "users"."projects_limit" AS t1_r15, "users"."failed_attempts" AS t1_r16, "users"."locked_at" AS t1_r17, "users"."username" AS t1_r18, "users"."can_create_group" AS t1_r19, "users"."can_create_team" AS t1_r20, "users"."state" AS t1_r21, "users"."color_scheme_id" AS t1_r22, "users"."password_expires_at" AS t1_r23, "users"."created_by_id" AS t1_r24, "users"."last_credential_check_at" AS t1_r25, "users"."avatar" AS t1_r26, "users"."confirmation_token" AS t1_r27, "users"."confirmed_at" AS t1_r28, "users"."confirmation_sent_at" AS t1_r29, "users"."unconfirmed_email" AS t1_r30, "users"."hide_no_ssh_key" AS t1_r31, "users"."admin_email_unsubscribed_at" AS t1_r32, "users"."notification_email" AS t1_r33, "users"."hide_no_password" AS t1_r34, "users"."password_automatically_set" AS t1_r35, "users"."encrypted_otp_secret" AS t1_r36, "users"."encrypted_otp_secret_iv" AS t1_r37, "users"."encrypted_otp_secret_salt" AS t1_r38, "users"."otp_required_for_login" AS t1_r39, "users"."otp_backup_codes" AS t1_r40, "users"."public_email" AS t1_r41, "users"."dashboard" AS t1_r42, "users"."project_view" AS t1_r43, "users"."consumed_timestep" AS t1_r44, "users"."layout" AS t1_r45, "users"."hide_project_limit" AS t1_r46, "users"."note" AS t1_r47, "users"."unlock_token" AS t1_r48, "users"."otp_grace_period_started_at" AS t1_r49, "users"."external" AS t1_r50, "users"."incoming_email_token" AS t1_r51, "users"."auditor" AS t1_r52, "users"."require_two_factor_authentication_from_group" AS t1_r53, "users"."two_factor_grace_period" AS t1_r54, "users"."last_activity_on" AS t1_r55, "users"."notified_of_own_activity" AS t1_r56, "users"."preferred_language" AS t1_r57, "users"."theme_id" AS t1_r58, "users"."accepted_term_id" AS t1_r59, "users"."feed_token" AS t1_r60, "users"."private_profile" AS t1_r61, "users"."roadmap_layout" AS t1_r62, "users"."include_private_contributions" AS t1_r63, "users"."commit_email" AS t1_r64, "users"."group_view" AS t1_r65, "users"."managing_group_id" AS t1_r66, "users"."first_name" AS t1_r67, "users"."last_name" AS t1_r68, "users"."static_object_token" AS t1_r69, "users"."role" AS t1_r70, "users"."user_type" AS t1_r71, "users"."static_object_token_encrypted" AS t1_r72, "users"."otp_secret_expires_at" AS t1_r73, "users"."onboarding_in_progress" AS t1_r74, "users"."color_mode_id" AS t1_r75, "users"."composite_identity_enforced" AS t1_r76 FROM "personal_access_tokens" LEFT OUTER JOIN "users" ON "users"."id" = "personal_access_tokens"."user_id" WHERE "personal_access_tokens"."user_id" IN (SELECT "users"."id" FROM "users" INNER JOIN "user_details" ON "users"."id" = "user_details"."user_id" WHERE "user_details"."enterprise_group_id" = 4249178) AND "personal_access_tokens"."impersonation" = FALSE AND "users"."user_type" = 0 ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC LIMIT 21 OFFSET 0
Sorting Indexes
exec CREATE INDEX index_user_id_pat_id_and_expired_at ON personal_access_tokens USING btree(user_id, id, expires_at) WHERE impersonation = false
exec CREATE INDEX index_user_id_pat_id_and_updated_at ON personal_access_tokens USING btree(user_id, id, updated_at) WHERE impersonation = false
exec CREATE INDEX index_user_id_pat_id_and_created_at ON personal_access_tokens USING btree(user_id, id, created_at) WHERE impersonation = false
exec CREATE INDEX index_user_id_pat_id_and_name ON personal_access_tokens USING btree(user_id, id, name) WHERE impersonation = false
Optimized Query for joe.ai
explain SELECT * FROM (WITH RECURSIVE "array_cte" AS MATERIALIZED (SELECT "users"."id" FROM "users" INNER JOIN "user_details" ON "users"."id" = "user_details"."user_id" WHERE "user_details"."enterprise_group_id" = 4249178), "recursive_keyset_cte" AS ((SELECT NULL::personal_access_tokens AS records, array_cte_id_array, personal_access_tokens_expires_at_array, personal_access_tokens_id_array, 0::bigint AS count FROM (SELECT ARRAY_AGG("array_cte"."id") AS array_cte_id_array, ARRAY_AGG("personal_access_tokens"."expires_at") AS personal_access_tokens_expires_at_array, ARRAY_AGG("personal_access_tokens"."id") AS personal_access_tokens_id_array FROM (SELECT "array_cte"."id" FROM array_cte) array_cte LEFT JOIN LATERAL (SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM "personal_access_tokens" WHERE "personal_access_tokens"."user_id" = "array_cte"."id" AND "personal_access_tokens"."impersonation" = FALSE ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC LIMIT 1) personal_access_tokens ON TRUE WHERE "personal_access_tokens"."id" IS NOT NULL) array_scope_lateral_query LIMIT 1)
UNION ALL
(SELECT (SELECT personal_access_tokens FROM "personal_access_tokens" WHERE "personal_access_tokens"."id" = recursive_keyset_cte.personal_access_tokens_id_array[position] LIMIT 1), array_cte_id_array, recursive_keyset_cte.personal_access_tokens_expires_at_array[:position_query.position-1]||next_cursor_values.expires_at||recursive_keyset_cte.personal_access_tokens_expires_at_array[position_query.position+1:], recursive_keyset_cte.personal_access_tokens_id_array[:position_query.position-1]||next_cursor_values.id||recursive_keyset_cte.personal_access_tokens_id_array[position_query.position+1:], recursive_keyset_cte.count + 1 FROM recursive_keyset_cte,
LATERAL (SELECT expires_at, id, position FROM UNNEST(personal_access_tokens_expires_at_array, personal_access_tokens_id_array) WITH ORDINALITY AS u(expires_at, id, position) WHERE id IS NOT NULL ORDER BY 1 ASC NULLS LAST, 2 DESC LIMIT 1) AS position_query,
LATERAL (SELECT "record"."expires_at", "record"."id" FROM (VALUES (NULL, NULL)) AS nulls LEFT JOIN (SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM ((SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM "personal_access_tokens" WHERE "personal_access_tokens"."user_id" = recursive_keyset_cte.array_cte_id_array[position] AND "personal_access_tokens"."impersonation" = FALSE AND recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NULL AND "personal_access_tokens"."expires_at" IS NULL AND "personal_access_tokens"."id" < recursive_keyset_cte.personal_access_tokens_id_array[position] ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC)
UNION ALL
(SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM "personal_access_tokens" WHERE "personal_access_tokens"."user_id" = recursive_keyset_cte.array_cte_id_array[position] AND "personal_access_tokens"."impersonation" = FALSE AND recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND "personal_access_tokens"."expires_at" IS NULL ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC)
UNION ALL
(SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM "personal_access_tokens" WHERE "personal_access_tokens"."user_id" = recursive_keyset_cte.array_cte_id_array[position] AND "personal_access_tokens"."impersonation" = FALSE AND recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND "personal_access_tokens"."expires_at" > recursive_keyset_cte.personal_access_tokens_expires_at_array[position] ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC)
UNION ALL
(SELECT "personal_access_tokens"."expires_at" AS expires_at, "personal_access_tokens"."id" AS id FROM "personal_access_tokens" WHERE "personal_access_tokens"."user_id" = recursive_keyset_cte.array_cte_id_array[position] AND "personal_access_tokens"."impersonation" = FALSE AND recursive_keyset_cte.personal_access_tokens_expires_at_array[position] IS NOT NULL AND "personal_access_tokens"."expires_at" = recursive_keyset_cte.personal_access_tokens_expires_at_array[position] AND "personal_access_tokens"."id" < recursive_keyset_cte.personal_access_tokens_id_array[position] ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC)) personal_access_tokens ORDER BY "personal_access_tokens"."expires_at" ASC, "personal_access_tokens"."id" DESC LIMIT 1) record ON TRUE LIMIT 1) AS next_cursor_values
)) SELECT (records).* FROM "recursive_keyset_cte" AS "personal_access_tokens" WHERE (count <> 0)) personal_access_tokens LIMIT 21 OFFSET 0
Scenario | Base Query | Optimized Query w/ Order Sort Indexes | Improvement |
---|---|---|---|
Cold Cache | 53.196 seconds | 596.374 ms | ~98.88% + |
Inital query times of the base query varied from ~20->~50s
Database Migrations
bin/rails db:migrate
hakeem@habdul-razak--20240918-F45T4 gitlab % bin/rails db:migrate
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90894
main: == 20250604001619 AddIndexToPatsOnExpiresAt: migrating ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0192s
main: -- index_exists?(:personal_access_tokens, [:user_id, :id, :expires_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_expires_at", :algorithm=>:concurrently})
main: -> 0.0033s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0006s
main: -- add_index(:personal_access_tokens, [:user_id, :id, :expires_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_expires_at", :algorithm=>:concurrently})
main: -> 0.0051s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250604001619 AddIndexToPatsOnExpiresAt: migrated (0.0421s) ===============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90894
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90895
ci: == 20250604001619 AddIndexToPatsOnExpiresAt: migrating ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :id, :expires_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_expires_at", :algorithm=>:concurrently})
ci: -> 0.0074s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :id, :expires_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_expires_at", :algorithm=>:concurrently})
ci: -> 0.0037s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250604001619 AddIndexToPatsOnExpiresAt: migrated (0.0228s) ===============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90895
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90897
main: == 20250604004156 AddIndexToPatsOnCreatedAt: migrating ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0004s
main: -- index_exists?(:personal_access_tokens, [:user_id, :id, :created_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_created_at", :algorithm=>:concurrently})
main: -> 0.0042s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0004s
main: -- add_index(:personal_access_tokens, [:user_id, :id, :created_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_created_at", :algorithm=>:concurrently})
main: -> 0.0046s
main: -- execute("RESET statement_timeout")
main: -> 0.0004s
main: == 20250604004156 AddIndexToPatsOnCreatedAt: migrated (0.0189s) ===============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90897
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90898
ci: == 20250604004156 AddIndexToPatsOnCreatedAt: migrating ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :id, :created_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_created_at", :algorithm=>:concurrently})
ci: -> 0.0032s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :id, :created_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_created_at", :algorithm=>:concurrently})
ci: -> 0.0034s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250604004156 AddIndexToPatsOnCreatedAt: migrated (0.0170s) ===============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90898
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90900
main: == 20250604004507 AddIndexToPatsOnUpdatedAt: migrating ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0003s
main: -- index_exists?(:personal_access_tokens, [:user_id, :id, :updated_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_updated_at", :algorithm=>:concurrently})
main: -> 0.0033s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- add_index(:personal_access_tokens, [:user_id, :id, :updated_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_updated_at", :algorithm=>:concurrently})
main: -> 0.0029s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250604004507 AddIndexToPatsOnUpdatedAt: migrated (0.0141s) ===============
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90900
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90902
ci: == 20250604004507 AddIndexToPatsOnUpdatedAt: migrating ========================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :id, :updated_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_updated_at", :algorithm=>:concurrently})
ci: -> 0.0035s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :id, :updated_at], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_updated_at", :algorithm=>:concurrently})
ci: -> 0.0035s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250604004507 AddIndexToPatsOnUpdatedAt: migrated (0.0173s) ===============
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90902
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90904
main: == 20250604004844 AddIndexToPatsOnName: migrating =============================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0003s
main: -- index_exists?(:personal_access_tokens, [:user_id, :id, :name], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_name", :algorithm=>:concurrently})
main: -> 0.0034s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- add_index(:personal_access_tokens, [:user_id, :id, :name], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_name", :algorithm=>:concurrently})
main: -> 0.0031s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250604004844 AddIndexToPatsOnName: migrated (0.0143s) ====================
main: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90904
ci: == [advisory_lock_connection] object_id: 136500, pg_backend_pid: 90905
ci: == 20250604004844 AddIndexToPatsOnName: migrating =============================
ci: -- transaction_open?(nil)
ci: -> 0.0000s
ci: -- view_exists?(:postgres_partitions)
ci: -> 0.0003s
ci: -- index_exists?(:personal_access_tokens, [:user_id, :id, :name], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_name", :algorithm=>:concurrently})
ci: -> 0.0036s
ci: -- execute("SET statement_timeout TO 0")
ci: -> 0.0003s
ci: -- add_index(:personal_access_tokens, [:user_id, :id, :name], {:where=>"impersonation IS false", :name=>"index_personal_access_tokens_on_user_id_and_id_and_name", :algorithm=>:concurrently})
ci: -> 0.0034s
ci: -- execute("RESET statement_timeout")
ci: -> 0.0003s
ci: == 20250604004844 AddIndexToPatsOnName: migrated (0.0172s) ====================
Database Rollback
index_personal_access_tokens_on_user_id_and_id_and_expires_at
VERSION=20250604001619 bin/rails db:migrate:down:main
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250604001619 bin/rails db:migrate:down:main && bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 93598
main: == 20250604001619 AddIndexToPatsOnExpiresAt: reverting ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0198s
main: -- indexes(:personal_access_tokens)
main: -> 0.0054s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0003s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_personal_access_tokens_on_user_id_and_id_and_expires_at"})
main: -> 0.0041s
main: -- execute("RESET statement_timeout")
main: -> 0.0004s
main: == 20250604001619 AddIndexToPatsOnExpiresAt: reverted (0.0446s) ===============
index_personal_access_tokens_on_user_id_and_id_and_created_at
VERSION=20250604004156 bin/rails db:migrate:down:main
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250604004156 bin/rails db:migrate:down:main && bin/rails db:migrate:down:ci
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 94557
main: == 20250604004156 AddIndexToPatsOnCreatedAt: reverting ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0239s
main: -- indexes(:personal_access_tokens)
main: -> 0.0058s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0006s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_personal_access_tokens_on_user_id_and_id_and_created_at"})
main: -> 0.0037s
main: -- execute("RESET statement_timeout")
main: -> 0.0005s
main: == 20250604004156 AddIndexToPatsOnCreatedAt: reverted (0.0531s) ===============
index_personal_access_tokens_on_user_id_and_id_and_updated_at
VERSION=20250604004507 bin/rails db:migrate:down:main
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250604004507 bin/rails db:migrate:down:main
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 94843
main: == 20250604004507 AddIndexToPatsOnUpdatedAt: reverting ========================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0190s
main: -- indexes(:personal_access_tokens)
main: -> 0.0040s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0005s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_personal_access_tokens_on_user_id_and_id_and_updated_at"})
main: -> 0.0028s
main: -- execute("RESET statement_timeout")
main: -> 0.0260s
main: == 20250604004507 AddIndexToPatsOnUpdatedAt: reverted (0.0711s) ===============
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 94843
index_personal_access_tokens_on_user_id_and_id_and_name
VERSION=20250604004844 bin/rails db:migrate:down:main
hakeem@habdul-razak--20240918-F45T4 gitlab % VERSION=20250604004844 bin/rails db:migrate:down:main
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 95048
main: == 20250604004844 AddIndexToPatsOnName: reverting =============================
main: -- transaction_open?(nil)
main: -> 0.0000s
main: -- view_exists?(:postgres_partitions)
main: -> 0.0199s
main: -- indexes(:personal_access_tokens)
main: -> 0.0044s
main: -- execute("SET statement_timeout TO 0")
main: -> 0.0005s
main: -- remove_index(:personal_access_tokens, {:algorithm=>:concurrently, :name=>"index_personal_access_tokens_on_user_id_and_id_and_name"})
main: -> 0.0022s
main: -- execute("RESET statement_timeout")
main: -> 0.0003s
main: == 20250604004844 AddIndexToPatsOnName: reverted (0.0410s) ====================
main: == [advisory_lock_connection] object_id: 136200, pg_backend_pid: 95048
How to set up and validate locally
- Turn on SAAS Mode
- Create a group with a premium/ultimate plan
- As a top-level group owner:
- Add multiple users to the group and make them enterprise users
gdk rails console
group = Group.find(group_id)
user = User.find_by(username: 'regular_user')
user.enterprise_group_id = group.id
user.save!
- Confirm an Enterprise Users badge next to your enterprise users
- Impersonate them and make some PATs
- Change MIN_NUMBER_OF_USERS_BEFORE_OPTIMIZATION to
-1
, in order to make the credentials inventory use the optimized PAT finder. - Navigate to the group credentials inventory: https://gdk.test:3443/groups/service-accounts-group/-/security/credentials
- Test the PAT filters and sorting to confirm that its behaviour is identical to the non-optimized version
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.
Edited by Hakeem Abdul-Razak