perf: optimise la query des auteurs — single pass + index GIN

- Remplace les 5 CTEs + double query (données + count) par une seule
  requête avec COUNT(*) OVER() pour le total
- Calcule book_count et series_count directement depuis UNNEST, sans
  re-JOIN sur les tables
- Ajoute des index GIN sur books.authors et series_metadata.authors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 10:20:48 +01:00
parent f5139b3598
commit 1af565bf29
2 changed files with 29 additions and 75 deletions

View File

@@ -68,97 +68,43 @@ pub async fn list_authors(
.filter(|s| !s.trim().is_empty()) .filter(|s| !s.trim().is_empty())
.map(|s| format!("%{s}%")); .map(|s| format!("%{s}%"));
// Aggregate unique authors from books.authors + books.author + series_metadata.authors // Single query: collect all authors with counts using a windowed total
let sql = format!( let sql = format!(
r#" r#"
WITH all_authors AS ( WITH author_books AS (
SELECT DISTINCT UNNEST( SELECT UNNEST(
COALESCE( COALESCE(
NULLIF(authors, '{{}}'), NULLIF(authors, '{{}}'),
CASE WHEN author IS NOT NULL AND author != '' THEN ARRAY[author] ELSE ARRAY[]::text[] END CASE WHEN author IS NOT NULL AND author != '' THEN ARRAY[author] ELSE ARRAY[]::text[] END
) )
) AS name ) AS author_name, id AS book_id, library_id, series
FROM books FROM books
UNION
SELECT DISTINCT UNNEST(authors) AS name
FROM series_metadata
WHERE authors != '{{}}'
), ),
filtered AS ( author_agg AS (
SELECT name FROM all_authors
WHERE ($1::text IS NULL OR name ILIKE $1)
),
book_counts AS (
SELECT SELECT
f.name AS author_name, author_name AS name,
COUNT(DISTINCT b.id) AS book_count COUNT(DISTINCT book_id) AS book_count,
FROM filtered f COUNT(DISTINCT (library_id, series)) AS series_count
LEFT JOIN books b ON ( FROM author_books
f.name = ANY( WHERE ($1::text IS NULL OR author_name ILIKE $1)
COALESCE( GROUP BY author_name
NULLIF(b.authors, '{{}}'),
CASE WHEN b.author IS NOT NULL AND b.author != '' THEN ARRAY[b.author] ELSE ARRAY[]::text[] END
) )
) SELECT name, book_count, series_count, COUNT(*) OVER() AS total
) FROM author_agg
GROUP BY f.name
),
series_counts AS (
SELECT
f.name AS author_name,
COUNT(DISTINCT (sm.library_id, sm.name)) AS series_count
FROM filtered f
LEFT JOIN series_metadata sm ON (
f.name = ANY(sm.authors) AND sm.authors != '{{}}'
)
GROUP BY f.name
)
SELECT
f.name,
COALESCE(bc.book_count, 0) AS book_count,
COALESCE(sc.series_count, 0) AS series_count
FROM filtered f
LEFT JOIN book_counts bc ON bc.author_name = f.name
LEFT JOIN series_counts sc ON sc.author_name = f.name
ORDER BY {order_clause} ORDER BY {order_clause}
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"# "#
); );
let count_sql = r#" let rows = sqlx::query(&sql)
WITH all_authors AS (
SELECT DISTINCT UNNEST(
COALESCE(
NULLIF(authors, '{}'),
CASE WHEN author IS NOT NULL AND author != '' THEN ARRAY[author] ELSE ARRAY[]::text[] END
)
) AS name
FROM books
UNION
SELECT DISTINCT UNNEST(authors) AS name
FROM series_metadata
WHERE authors != '{}'
)
SELECT COUNT(*) AS total
FROM all_authors
WHERE ($1::text IS NULL OR name ILIKE $1)
"#;
let (rows, count_row) = tokio::join!(
sqlx::query(&sql)
.bind(q_pattern.as_deref()) .bind(q_pattern.as_deref())
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(&state.pool), .fetch_all(&state.pool)
sqlx::query(count_sql) .await
.bind(q_pattern.as_deref()) .map_err(|e| ApiError::internal(format!("authors query failed: {e}")))?;
.fetch_one(&state.pool)
);
let rows = rows.map_err(|e| ApiError::internal(format!("authors query failed: {e}")))?; let total: i64 = rows.first().map(|r| r.get("total")).unwrap_or(0);
let total: i64 = count_row
.map_err(|e| ApiError::internal(format!("authors count failed: {e}")))?
.get("total");
let items: Vec<AuthorItem> = rows let items: Vec<AuthorItem> = rows
.iter() .iter()

View File

@@ -0,0 +1,8 @@
-- GIN index for efficient array lookups on books.authors
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_books_authors_gin ON books USING GIN (authors);
-- Index on author column for legacy single-author field
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_books_author ON books (author) WHERE author IS NOT NULL AND author != '';
-- GIN index on series_metadata.authors
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_series_metadata_authors_gin ON series_metadata USING GIN (authors);