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:
@@ -68,97 +68,43 @@ pub async fn list_authors(
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.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!(
|
||||
r#"
|
||||
WITH all_authors AS (
|
||||
SELECT DISTINCT UNNEST(
|
||||
WITH author_books AS (
|
||||
SELECT UNNEST(
|
||||
COALESCE(
|
||||
NULLIF(authors, '{{}}'),
|
||||
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
|
||||
UNION
|
||||
SELECT DISTINCT UNNEST(authors) AS name
|
||||
FROM series_metadata
|
||||
WHERE authors != '{{}}'
|
||||
),
|
||||
filtered AS (
|
||||
SELECT name FROM all_authors
|
||||
WHERE ($1::text IS NULL OR name ILIKE $1)
|
||||
),
|
||||
book_counts AS (
|
||||
author_agg AS (
|
||||
SELECT
|
||||
f.name AS author_name,
|
||||
COUNT(DISTINCT b.id) AS book_count
|
||||
FROM filtered f
|
||||
LEFT JOIN books b ON (
|
||||
f.name = ANY(
|
||||
COALESCE(
|
||||
NULLIF(b.authors, '{{}}'),
|
||||
CASE WHEN b.author IS NOT NULL AND b.author != '' THEN ARRAY[b.author] ELSE ARRAY[]::text[] END
|
||||
author_name AS name,
|
||||
COUNT(DISTINCT book_id) AS book_count,
|
||||
COUNT(DISTINCT (library_id, series)) AS series_count
|
||||
FROM author_books
|
||||
WHERE ($1::text IS NULL OR author_name ILIKE $1)
|
||||
GROUP BY author_name
|
||||
)
|
||||
)
|
||||
)
|
||||
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
|
||||
SELECT name, book_count, series_count, COUNT(*) OVER() AS total
|
||||
FROM author_agg
|
||||
ORDER BY {order_clause}
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
);
|
||||
|
||||
let count_sql = r#"
|
||||
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)
|
||||
let rows = sqlx::query(&sql)
|
||||
.bind(q_pattern.as_deref())
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool),
|
||||
sqlx::query(count_sql)
|
||||
.bind(q_pattern.as_deref())
|
||||
.fetch_one(&state.pool)
|
||||
);
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("authors query failed: {e}")))?;
|
||||
|
||||
let rows = rows.map_err(|e| ApiError::internal(format!("authors query failed: {e}")))?;
|
||||
let total: i64 = count_row
|
||||
.map_err(|e| ApiError::internal(format!("authors count failed: {e}")))?
|
||||
.get("total");
|
||||
let total: i64 = rows.first().map(|r| r.get("total")).unwrap_or(0);
|
||||
|
||||
let items: Vec<AuthorItem> = rows
|
||||
.iter()
|
||||
|
||||
8
infra/migrations/0066_add_authors_index.sql
Normal file
8
infra/migrations/0066_add_authors_index.sql
Normal 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);
|
||||
Reference in New Issue
Block a user