Skip to content

Commit 674a571

Browse files
Merge #1379
1379: Remove the N+1 queries from crates search r=jtgeibel I've been noticing response times spiking towards 300ms when a particular crawler hits us. Specifically one that sets `per_page=100`. The number of queries executed to load badges is the only thing that would dramatically change in timing based on the number of rows. This will load the badges in 1 query per request instead of 1 query per crate per request.
2 parents 47a876d + 74878e9 commit 674a571

File tree

3 files changed

+42
-22
lines changed

3 files changed

+42
-22
lines changed

src/controllers/krate/search.rs

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use diesel_full_text_search::*;
55
use controllers::helpers::Paginate;
66
use controllers::prelude::*;
77
use views::EncodableCrate;
8-
use models::{Badge, Crate, OwnerKind, Version};
8+
use models::{Crate, CrateBadge, OwnerKind, Version};
99
use schema::*;
1010

1111
use models::krate::{canon_crate_name, ALL_COLUMNS};
@@ -147,50 +147,47 @@ pub fn search(req: &mut Request) -> CargoResult<Response> {
147147
query = query.then_order_by(crates::name.asc())
148148
}
149149

150-
// The database query returns a tuple within a tuple , with the root
150+
// The database query returns a tuple within a tuple, with the root
151151
// tuple containing 3 items.
152152
let data = query
153153
.paginate(limit, offset)
154154
.load::<((Crate, bool, Option<i64>), i64)>(&*conn)?;
155155
let total = data.first().map(|&(_, t)| t).unwrap_or(0);
156-
let crates = data.iter()
157-
.map(|&((ref c, _, _), _)| c.clone())
158-
.collect::<Vec<_>>();
159-
let perfect_matches = data.clone()
160-
.into_iter()
161-
.map(|((_, b, _), _)| b)
162-
.collect::<Vec<_>>();
163-
let recent_downloads = data.clone()
164-
.into_iter()
165-
.map(|((_, _, s), _)| s.unwrap_or(0))
156+
let perfect_matches = data.iter().map(|&((_, b, _), _)| b).collect::<Vec<_>>();
157+
let recent_downloads = data.iter()
158+
.map(|&((_, _, s), _)| s.unwrap_or(0))
166159
.collect::<Vec<_>>();
160+
let crates = data.into_iter().map(|((c, _, _), _)| c).collect::<Vec<_>>();
167161

168162
let versions = Version::belonging_to(&crates)
169163
.load::<Version>(&*conn)?
170164
.grouped_by(&crates)
171165
.into_iter()
172166
.map(|versions| Version::max(versions.into_iter().map(|v| v.num)));
173167

168+
let badges = CrateBadge::belonging_to(&crates)
169+
.select((badges::crate_id, badges::all_columns))
170+
.load::<CrateBadge>(&conn)?
171+
.grouped_by(&crates)
172+
.into_iter()
173+
.map(|badges| badges.into_iter().map(|cb| cb.badge).collect());
174+
174175
let crates = versions
175176
.zip(crates)
176177
.zip(perfect_matches)
177178
.zip(recent_downloads)
179+
.zip(badges)
178180
.map(
179-
|(((max_version, krate), perfect_match), recent_downloads)| {
180-
// FIXME: If we add crate_id to the Badge enum we can eliminate
181-
// this N+1
182-
let badges = badges::table
183-
.filter(badges::crate_id.eq(krate.id))
184-
.load::<Badge>(&*conn)?;
185-
Ok(krate.minimal_encodable(
181+
|((((max_version, krate), perfect_match), recent_downloads), badges)| {
182+
krate.minimal_encodable(
186183
&max_version,
187184
Some(badges),
188185
perfect_match,
189186
Some(recent_downloads),
190-
))
187+
)
191188
},
192189
)
193-
.collect::<Result<_, ::diesel::result::Error>>()?;
190+
.collect();
194191

195192
#[derive(Serialize)]
196193
struct R {

src/models/badge.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use diesel::associations::HasTable;
12
use diesel::pg::Pg;
23
use diesel::prelude::*;
34
use serde_json;
@@ -7,6 +8,19 @@ use models::Crate;
78
use schema::badges;
89
use views::EncodableBadge;
910

11+
/// A combination of a `Badge` and a crate ID.
12+
///
13+
/// We don't typically care about the crate ID when dealing with badges.
14+
/// However, when we are eager loading all badges for a group of crates, we need
15+
/// the crate ID to group badges by their owner.
16+
#[derive(Debug, Queryable, Associations)]
17+
#[belongs_to(Crate)]
18+
#[table_name = "badges"]
19+
pub struct CrateBadge {
20+
pub crate_id: i32,
21+
pub badge: Badge,
22+
}
23+
1024
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
1125
#[serde(rename_all = "kebab-case", tag = "badge_type", content = "attributes")]
1226
pub enum Badge {
@@ -63,6 +77,15 @@ pub enum MaintenanceStatus {
6377
Deprecated,
6478
}
6579

80+
// FIXME: Derive this in Diesel 1.3.
81+
impl HasTable for CrateBadge {
82+
type Table = badges::table;
83+
84+
fn table() -> Self::Table {
85+
badges::table
86+
}
87+
}
88+
6689
impl Queryable<badges::SqlType, Pg> for Badge {
6790
type Row = (i32, String, serde_json::Value);
6891

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use self::badge::{Badge, MaintenanceStatus};
1+
pub use self::badge::{Badge, CrateBadge, MaintenanceStatus};
22
pub use self::category::{Category, CrateCategory, NewCategory};
33
pub use self::crate_owner_invitation::{CrateOwnerInvitation, NewCrateOwnerInvitation};
44
pub use self::dependency::{Dependency, DependencyKind, ReverseDependency};

0 commit comments

Comments
 (0)