Skip to content

Commit c9f4394

Browse files
committed
Optimize our most time consuming query
Our database spends more of its time processing /api/v1/crates with no parameters other than pagination. This query is the main one hit by crawlers, and it is taking over 100ms to run, so it's at the top of our list (for posterity's sake, rust-lang#2 is copying `crate_downloads` during backups, rust-lang#3 and rust-lang#4 are the updates run from bin/update-downloads, and rust-lang#5 is the query run from the download endpoint) The query is having to perform the full join between crates and recent_downloads, and then count the results of that. Since we have no search parameters of any kind, this count is equivalent to just counting the crates table, which we can do much more quickly. We still need to do the count over the whole thing if there's any where clause, but we can optimize the case where there's no search. This implicitly relies on the fact that we're only changing the select clause in branches where we're also setting a where clause. Diesel 2 will probably have a feature that lets us avoid this. We could also refactor the "exact match" check to be client side instead of the DB and get rid of all the cases where we modify the select clause. Before: ``` Limit (cost=427.87..470.65 rows=100 width=877) (actual time=109.698..109.739 rows=100 loops=1) -> WindowAgg (cost=0.14..10119.91 rows=23659 width=877) (actual time=109.277..109.697 rows=1100 loops=1) -> Nested Loop Left Join (cost=0.14..9966.13 rows=23659 width=869) (actual time=0.051..85.429 rows=23659 loops=1) -> Index Scan using index_crates_name_ordering on crates (cost=0.08..7604.30 rows=23659 width=860) (actual time=0.037..34.975 rows=23659 loops=1) -> Index Scan using recent_crate_downloads_crate_id on recent_crate_downloads (cost=0.06..0.10 rows=1 width=12) (actual time=0.002..0.002 rows=1 loops=23659) Index Cond: (crate_id = crates.id) Planning time: 1.307 ms Execution time: 111.840 ms ``` After: ``` Limit (cost=1052.34..1094.76 rows=100 width=877) (actual time=11.536..12.026 rows=100 loops=1) InitPlan 1 (returns $0) -> Aggregate (cost=627.96..627.96 rows=1 width=8) (actual time=4.966..4.966 rows=1 loops=1) -> Index Only Scan using packages_pkey on crates crates_1 (cost=0.06..616.13 rows=23659 width=0) (actual time=0.015..3.513 rows=23659 loops=1) Heap Fetches: 811 -> Subquery Scan on t (cost=0.14..10037.11 rows=23659 width=877) (actual time=5.019..11.968 rows=1100 loops=1) -> Nested Loop Left Join (cost=0.14..9966.13 rows=23659 width=869) (actual time=0.051..6.831 rows=1100 loops=1) -> Index Scan using index_crates_name_ordering on crates (cost=0.08..7604.30 rows=23659 width=860) (actual time=0.038..3.331 rows=1100 loops=1) -> Index Scan using recent_crate_downloads_crate_id on recent_crate_downloads (cost=0.06..0.10 rows=1 width=12) (actual time=0.003..0.003 rows=1 loops=1100) Index Cond: (crate_id = crates.id) Planning time: 1.377 ms Execution time: 12.106 ms ```
1 parent 8e5d17a commit c9f4394

File tree

1 file changed

+32
-8
lines changed

1 file changed

+32
-8
lines changed

src/controllers/krate/search.rs

+32-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Endpoint for searching and discovery functionality
22
3+
use diesel::sql_types::{NotNull, Nullable};
34
use diesel_full_text_search::*;
45

56
use crate::controllers::helpers::Paginate;
@@ -41,17 +42,20 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
4142
.get("sort")
4243
.map(|s| &**s)
4344
.unwrap_or("recent-downloads");
45+
let mut has_filter = false;
4446

47+
let selection = (
48+
ALL_COLUMNS,
49+
false.into_sql::<Bool>(),
50+
recent_crate_downloads::downloads.nullable(),
51+
);
4552
let mut query = crates::table
4653
.left_join(recent_crate_downloads::table)
47-
.select((
48-
ALL_COLUMNS,
49-
false.into_sql::<Bool>(),
50-
recent_crate_downloads::downloads.nullable(),
51-
))
54+
.select(selection)
5255
.into_boxed();
5356

5457
if let Some(q_string) = params.get("q") {
58+
has_filter = true;
5559
if !q_string.is_empty() {
5660
let sort = params.get("sort").map(|s| &**s).unwrap_or("relevance");
5761
let q = plainto_tsquery(q_string);
@@ -75,6 +79,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
7579
}
7680

7781
if let Some(cat) = params.get("category") {
82+
has_filter = true;
7883
query = query.filter(
7984
crates::id.eq_any(
8085
crates_categories::table
@@ -90,6 +95,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
9095
}
9196

9297
if let Some(kw) = params.get("keyword") {
98+
has_filter = true;
9399
query = query.filter(
94100
crates::id.eq_any(
95101
crates_keywords::table
@@ -99,6 +105,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
99105
),
100106
);
101107
} else if let Some(letter) = params.get("letter") {
108+
has_filter = true;
102109
let pattern = format!(
103110
"{}%",
104111
letter
@@ -110,6 +117,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
110117
);
111118
query = query.filter(canon_crate_name(crates::name).like(pattern));
112119
} else if let Some(user_id) = params.get("user_id").and_then(|s| s.parse::<i32>().ok()) {
120+
has_filter = true;
113121
query = query.filter(
114122
crates::id.eq_any(
115123
crate_owners::table
@@ -120,6 +128,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
120128
),
121129
);
122130
} else if let Some(team_id) = params.get("team_id").and_then(|s| s.parse::<i32>().ok()) {
131+
has_filter = true;
123132
query = query.filter(
124133
crates::id.eq_any(
125134
crate_owners::table
@@ -130,6 +139,7 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
130139
),
131140
);
132141
} else if params.get("following").is_some() {
142+
has_filter = true;
133143
query = query.filter(
134144
crates::id.eq_any(
135145
follows::table
@@ -151,9 +161,23 @@ pub fn search(req: &mut dyn Request) -> CargoResult<Response> {
151161

152162
// The database query returns a tuple within a tuple, with the root
153163
// tuple containing 3 items.
154-
let data = query
155-
.paginate(limit, offset)
156-
.load::<((Crate, bool, Option<i64>), i64)>(&*conn)?;
164+
let data = if has_filter {
165+
query
166+
.paginate(limit, offset)
167+
.load::<((Crate, bool, Option<i64>), i64)>(&*conn)?
168+
} else {
169+
sql_function!(fn coalesce<T: NotNull>(value: Nullable<T>, default: T) -> T);
170+
query
171+
.select((
172+
// FIXME: Use `query.selection()` if that feature ends up in
173+
// Diesel 2.0
174+
selection,
175+
coalesce(crates::table.count().single_value(), 0),
176+
))
177+
.limit(limit)
178+
.offset(offset)
179+
.load(&*conn)?
180+
};
157181
let total = data.first().map(|&(_, t)| t).unwrap_or(0);
158182
let perfect_matches = data.iter().map(|&((_, b, _), _)| b).collect::<Vec<_>>();
159183
let recent_downloads = data

0 commit comments

Comments
 (0)