1---
2stage: none
3group: unassigned
4info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
5---
6
7# Cached queries guidelines
8
9Rails provides an [SQL query cache](https://guides.rubyonrails.org/caching_with_rails.html#sql-caching)
10which is used to cache the results of database queries for the duration of a request.
11When Rails encounters the same query again within the same request, it uses the cached
12result set instead of running the query against the database again.
13
14The query results are only cached for the duration of that single request, and
15don't persist across multiple requests.
16
17## Why cached queries are considered bad
18
19Cached queries help by reducing the load on the database, but they still:
20
21- Consume memory.
22- Require Rails to re-instantiate each `ActiveRecord` object.
23- Require Rails to re-instantiate each relation of the object.
24- Make us spend additional CPU cycles to look into a list of cached queries.
25
26Although cached queries are cheaper from a database perspective, they are potentially
27more expensive from a memory perspective. They could mask
28[N+1 query problems](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations),
29so you should treat them the same way you treat regular N+1 queries.
30
31In cases of N+1 queries masked by cached queries, the same query is executed N times.
32It doesn't hit the database N times but instead returns the cached results N times.
33This is still expensive because you need to re-initialize objects each time at a
34greater expense to the CPU and memory resources. Instead, you should use the same
35in-memory objects whenever possible.
36
37When you introduce a new feature, you should:
38
39- Avoid N+1 queries.
40- Minimize the [query count](merge_request_performance_guidelines.md#query-counts).
41- Pay special attention to ensure
42  [cached queries](merge_request_performance_guidelines.md#cached-queries) are not
43  masking N+1 problems.
44
45## How to detect cached queries
46
47### Detect potential offenders by using Kibana
48
49GitLab.com, logs entries with the number of executed cached queries in the
50`pubsub-redis-inf-gprd*` index as
51[`db_cached_count`](https://log.gprd.gitlab.net/goto/77d18d80ad84c5df1bf1da5c2cd35b82).
52You can filter by endpoints that have a large number of executed cached queries. For
53example, an endpoint with a `db_cached_count` greater than 100 can indicate an N+1 problem which
54is masked by cached queries. You should investigate this endpoint further to determine
55if it is indeed executing duplicated cached queries.
56
57For more Kibana visualizations related to cached queries, read
58[issue #259007, 'Provide metrics that would help us to detect the potential N+1 CACHED SQL calls'](https://gitlab.com/gitlab-org/gitlab/-/issues/259007).
59
60### Inspect suspicious endpoints using the Performance Bar
61
62When building features, use the
63[performance bar](../administration/monitoring/performance/performance_bar.md)
64to view the list of database queries, including cached queries. The
65performance bar shows a warning when the number of total executed and cached queries is
66greater than 100.
67
68To learn more about the statistics available to you, read the
69[Performance Bar documentation](../administration/monitoring/performance/performance_bar.md).
70
71## What to look for
72
73Using [Kibana](#detect-potential-offenders-by-using-kibana), you can look for a large number
74of executed cached queries. Endpoints with a large `db_cached_count` could suggest a large number
75of duplicated cached queries, which often indicates a masked N+1 problem.
76
77When you investigate a specific endpoint, use
78the [performance bar](#inspect-suspicious-endpoints-using-the-performance-bar)
79to identify similar and cached queries, which may also indicate an N+1 query issue
80(or a similar kind of query batching problem).
81
82### An example
83
84For example, let's debug the "Group Members" page. In the left corner of the
85performance bar, **Database queries** shows the total number of database queries
86and the number of executed cached queries:
87
88![Performance Bar Database Queries](img/performance_bar_members_page.png)
89
90The page included 55 cached queries. Clicking the number displays a modal window
91with more details about queries. Cached queries are marked with the `cached` label
92below the query. You can see multiple duplicate cached queries in this modal window:
93
94![Performance Bar Cached Queries Modal](img/performance_bar_cached_queries.png)
95
96Click **...** to expand the actual stack trace:
97
98```ruby
99[
100  "app/models/group.rb:305:in `has_owner?'",
101  "ee/app/views/shared/members/ee/_license_badge.html.haml:1",
102  "app/helpers/application_helper.rb:19:in `render_if_exists'",
103  "app/views/shared/members/_member.html.haml:31",
104  "app/views/groups/group_members/index.html.haml:75",
105  "app/controllers/application_controller.rb:134:in `render'",
106  "ee/lib/gitlab/ip_address_state.rb:10:in `with'",
107  "ee/app/controllers/ee/application_controller.rb:44:in `set_current_ip_address'",
108  "app/controllers/application_controller.rb:493:in `set_current_admin'",
109  "lib/gitlab/session.rb:11:in `with_session'",
110  "app/controllers/application_controller.rb:484:in `set_session_storage'",
111  "app/controllers/application_controller.rb:478:in `set_locale'",
112  "lib/gitlab/error_tracking.rb:52:in `with_context'",
113  "app/controllers/application_controller.rb:543:in `sentry_context'",
114  "app/controllers/application_controller.rb:471:in `block in set_current_context'",
115  "lib/gitlab/application_context.rb:54:in `block in use'",
116  "lib/gitlab/application_context.rb:54:in `use'",
117  "lib/gitlab/application_context.rb:21:in `with_context'",
118  "app/controllers/application_controller.rb:463:in `set_current_context'",
119  "lib/gitlab/jira/middleware.rb:19:in `call'"
120]
121```
122
123The stack trace shows an N+1 problem, because the code repeatedly executes
124`group.has_owner?(current_user)` for each group member. To solve this issue,
125move the repeated line of code outside of the loop, passing the result to each rendered member instead:
126
127```erb
128- current_user_is_group_owner = @group && @group.has_owner?(current_user)
129
130= render  partial: 'shared/members/member',
131          collection: @members, as: :member,
132          locals: { membership_source: @group,
133                    group: @group,
134                    current_user_is_group_owner: current_user_is_group_owner }
135```
136
137After [fixing the cached query](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44626/diffs#27c2761d66e496495be07d0925697f7e62b5bd14), the performance bar now shows only
1386 cached queries:
139
140![Performance Bar Fixed Cached Queries](img/performance_bar_fixed_cached_queries.png)
141
142## How to measure the impact of the change
143
144Use the [memory profiler](performance.md#using-memory-profiler) to profile your code.
145For [this example](#an-example), wrap the profiler around the `Groups::GroupMembersController#index` action. Before the fix, the application had
146the following statistics:
147
148- Total allocated: 7133601 bytes (84858 objects)
149- Total retained: 757595 bytes (6070 objects)
150- `db_count`: 144
151- `db_cached_count`: 55
152- `db_duration`: 303ms
153
154The fix reduced the allocated memory, and the number of cached queries. These
155factors help improve the overall execution time:
156
157- Total allocated: 5313899 bytes (65290 objects), 1810KB (25%) less
158- Total retained: 685593 bytes (5278 objects), 72KB (9%) less
159- `db_count`: 95 (34% less)
160- `db_cached_count`: 6 (89% less)
161- `db_duration`: 162ms (87% faster)
162
163## For more information
164
165- [Metrics that would help us detect the potential N+1 Cached SQL calls](https://gitlab.com/gitlab-org/gitlab/-/issues/259007)
166- [Merge Request performance guidelines for cached queries](merge_request_performance_guidelines.md#cached-queries)
167- [Improvements for biggest offenders](https://gitlab.com/groups/gitlab-org/-/epics/4508)
168