mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 02:26:38 -06:00
perf: optimize project list queries with batch fetching for tags and media
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n id,\n project_id,\n display_order,\n media_type as \"media_type: MediaType\",\n r2_base_path,\n variants,\n blurhash,\n metadata\n FROM project_media\n WHERE project_id = ANY($1)\n ORDER BY project_id, display_order ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "project_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "display_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_type: MediaType",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "media_type",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"image",
|
||||
"video"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "r2_base_path",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "variants",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "blurhash",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1bd6b8e70790c3bc84bd42182622a417f338c179fe01d8324691e374c6f98d58"
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT pt.project_id, t.id, t.slug, t.name, t.icon, t.color\n FROM tags t\n JOIN project_tags pt ON t.id = pt.tag_id\n WHERE pt.project_id = ANY($1)\n ORDER BY pt.project_id, t.name ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "icon",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "fd27337c6fd48ad71bf840da7764907adcc47e9f69fa0d821640c2ed7cbff269"
|
||||
}
|
||||
@@ -250,6 +250,53 @@ pub async fn get_media_for_project(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Batch fetch media for multiple projects in a single query.
|
||||
/// Returns a HashMap mapping project_id to its media items.
|
||||
pub async fn get_media_for_projects(
|
||||
pool: &PgPool,
|
||||
project_ids: &[Uuid],
|
||||
) -> Result<std::collections::HashMap<Uuid, Vec<DbProjectMedia>>, sqlx::Error> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
if project_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
DbProjectMedia,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
project_id,
|
||||
display_order,
|
||||
media_type as "media_type: MediaType",
|
||||
r2_base_path,
|
||||
variants,
|
||||
blurhash,
|
||||
metadata
|
||||
FROM project_media
|
||||
WHERE project_id = ANY($1)
|
||||
ORDER BY project_id, display_order ASC
|
||||
"#,
|
||||
project_ids
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut result: HashMap<Uuid, Vec<DbProjectMedia>> = HashMap::new();
|
||||
|
||||
// Initialize empty vecs for all requested project_ids
|
||||
for &id in project_ids {
|
||||
result.entry(id).or_default();
|
||||
}
|
||||
|
||||
for media in rows {
|
||||
result.entry(media.project_id).or_default().push(media);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get single media item by ID
|
||||
pub async fn get_media_by_id(
|
||||
pool: &PgPool,
|
||||
|
||||
+44
-12
@@ -6,9 +6,9 @@ use uuid::Uuid;
|
||||
|
||||
use super::{
|
||||
ProjectStatus,
|
||||
media::{ApiProjectMedia, DbProjectMedia, get_media_for_project},
|
||||
media::{self, ApiProjectMedia, DbProjectMedia, get_media_for_project},
|
||||
slugify,
|
||||
tags::{ApiTag, DbTag, get_tags_for_project},
|
||||
tags::{self, ApiTag, DbTag, get_tags_for_project},
|
||||
};
|
||||
|
||||
// Database model
|
||||
@@ -182,13 +182,29 @@ pub async fn get_public_projects_with_tags(
|
||||
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
||||
let projects = get_public_projects(pool).await?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for project in projects {
|
||||
let tags = get_tags_for_project(pool, project.id).await?;
|
||||
let media = get_media_for_project(pool, project.id).await?;
|
||||
result.push((project, tags, media));
|
||||
if projects.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Collect project IDs for batch queries
|
||||
let project_ids: Vec<Uuid> = projects.iter().map(|p| p.id).collect();
|
||||
|
||||
// Batch fetch all tags and media in 2 queries instead of N*2
|
||||
let (tags_map, media_map) = tokio::try_join!(
|
||||
tags::get_tags_for_projects(pool, &project_ids),
|
||||
media::get_media_for_projects(pool, &project_ids),
|
||||
)?;
|
||||
|
||||
// Assemble results
|
||||
let result = projects
|
||||
.into_iter()
|
||||
.map(|project| {
|
||||
let tags = tags_map.get(&project.id).cloned().unwrap_or_default();
|
||||
let media = media_map.get(&project.id).cloned().unwrap_or_default();
|
||||
(project, tags, media)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -222,13 +238,29 @@ pub async fn get_all_projects_with_tags_admin(
|
||||
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
||||
let projects = get_all_projects_admin(pool).await?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for project in projects {
|
||||
let tags = get_tags_for_project(pool, project.id).await?;
|
||||
let media = get_media_for_project(pool, project.id).await?;
|
||||
result.push((project, tags, media));
|
||||
if projects.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Collect project IDs for batch queries
|
||||
let project_ids: Vec<Uuid> = projects.iter().map(|p| p.id).collect();
|
||||
|
||||
// Batch fetch all tags and media in 2 queries instead of N*2
|
||||
let (tags_map, media_map) = tokio::try_join!(
|
||||
tags::get_tags_for_projects(pool, &project_ids),
|
||||
media::get_media_for_projects(pool, &project_ids),
|
||||
)?;
|
||||
|
||||
// Assemble results
|
||||
let result = projects
|
||||
.into_iter()
|
||||
.map(|project| {
|
||||
let tags = tags_map.get(&project.id).cloned().unwrap_or_default();
|
||||
let media = media_map.get(&project.id).cloned().unwrap_or_default();
|
||||
(project, tags, media)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,52 @@ pub async fn get_tags_for_project(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Batch fetch tags for multiple projects in a single query.
|
||||
/// Returns a HashMap mapping project_id to its tags.
|
||||
pub async fn get_tags_for_projects(
|
||||
pool: &PgPool,
|
||||
project_ids: &[Uuid],
|
||||
) -> Result<std::collections::HashMap<Uuid, Vec<DbTag>>, sqlx::Error> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
if project_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT pt.project_id, t.id, t.slug, t.name, t.icon, t.color
|
||||
FROM tags t
|
||||
JOIN project_tags pt ON t.id = pt.tag_id
|
||||
WHERE pt.project_id = ANY($1)
|
||||
ORDER BY pt.project_id, t.name ASC
|
||||
"#,
|
||||
project_ids
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut result: HashMap<Uuid, Vec<DbTag>> = HashMap::new();
|
||||
|
||||
// Initialize empty vecs for all requested project_ids
|
||||
for &id in project_ids {
|
||||
result.entry(id).or_default();
|
||||
}
|
||||
|
||||
for row in rows {
|
||||
let tag = DbTag {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
icon: row.icon,
|
||||
color: row.color,
|
||||
};
|
||||
result.entry(row.project_id).or_default().push(tag);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_projects_for_tag(
|
||||
pool: &PgPool,
|
||||
tag_id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user