mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 06:26:44 -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
|
.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
|
/// Get single media item by ID
|
||||||
pub async fn get_media_by_id(
|
pub async fn get_media_by_id(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
|
|||||||
+44
-12
@@ -6,9 +6,9 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
media::{ApiProjectMedia, DbProjectMedia, get_media_for_project},
|
media::{self, ApiProjectMedia, DbProjectMedia, get_media_for_project},
|
||||||
slugify,
|
slugify,
|
||||||
tags::{ApiTag, DbTag, get_tags_for_project},
|
tags::{self, ApiTag, DbTag, get_tags_for_project},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database model
|
// Database model
|
||||||
@@ -182,13 +182,29 @@ pub async fn get_public_projects_with_tags(
|
|||||||
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
||||||
let projects = get_public_projects(pool).await?;
|
let projects = get_public_projects(pool).await?;
|
||||||
|
|
||||||
let mut result = Vec::new();
|
if projects.is_empty() {
|
||||||
for project in projects {
|
return Ok(Vec::new());
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,13 +238,29 @@ pub async fn get_all_projects_with_tags_admin(
|
|||||||
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
) -> Result<Vec<(DbProject, Vec<DbTag>, Vec<DbProjectMedia>)>, sqlx::Error> {
|
||||||
let projects = get_all_projects_admin(pool).await?;
|
let projects = get_all_projects_admin(pool).await?;
|
||||||
|
|
||||||
let mut result = Vec::new();
|
if projects.is_empty() {
|
||||||
for project in projects {
|
return Ok(Vec::new());
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,52 @@ pub async fn get_tags_for_project(
|
|||||||
.await
|
.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(
|
pub async fn get_projects_for_tag(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
tag_id: Uuid,
|
tag_id: Uuid,
|
||||||
|
|||||||
Reference in New Issue
Block a user