| | 147 | |
| | 148 | == View 6: vw_fact_check_summary |
| | 149 | |
| | 150 | ** Purpose ** |
| | 151 | |
| | 152 | This view provides a summarized fact-check overview for each article. It is primarily used by the admin panel and editorial dashboard to quickly see the fact-checking status of any article, how many checks have been run, how many resulted in each verdict (true, false, misleading), what the current approval status breakdown is, and when the most recent review occurred. |
| | 153 | |
| | 154 | ** Design Decisions ** |
| | 155 | |
| | 156 | * Filters WHERE fc.is_active = 1 so only active (non-retracted) fact checks are included in the summary. |
| | 157 | * Uses simple COUNT(CASE WHEN ...) expressions (not COUNT DISTINCT) since each fact check row has a unique id and a single verdict/status value. No deduplication is necessary. |
| | 158 | * Groups by article_id only — the simplest possible GROUP BY for this aggregation. |
| | 159 | * Returns MAX(reviewed_at) as last_reviewed_at so admins can see at a glance whether an article has been reviewed recently. |
| | 160 | |
| | 161 | ** Performance & Indexing ** |
| | 162 | |
| | 163 | ||= Scenario =||= Before index =||= After index =||= Improvement =|| |
| | 164 | ||Fact check summary for article|| ~68ms || - || No index needed || |
| | 165 | |
| | 166 | This view performed well at baseline (68 ms) without requiring additional indexing. The is_active filter and article_id grouping are both served by the existing FK index on fact_checks(article_id). The fact_checks table is also significantly smaller than tables like article_views or notifications, so full-scan costs are inherently lower. |
| | 167 | |
| | 168 | == View 7: vw_article_metadata |
| | 169 | |
| | 170 | ** Purpose ** |
| | 171 | |
| | 172 | This view aggregates SEO and metadata for each article, specifically, the tags associated with it and the sources it cites. It is used by the application to build structured metadata (Open Graph tags, JSON-LD, sitemaps) and to display tag and source information on the article page. Keeping this separate from the article detail view keeps that view focused on content and avoids heavy STRING_AGG operations on every article detail load. |
| | 173 | |
| | 174 | ** Design Decisions ** |
| | 175 | |
| | 176 | * Uses STRING_AGG(DISTINCT t.name, ', ') and STRING_AGG(DISTINCT s.url, ', ') / STRING_AGG(DISTINCT s.title, ', ') to collapse multiple tags and sources into comma-separated strings, suitable for meta tag rendering without needing application-side join logic. |
| | 177 | * DISTINCT is used inside STRING_AGG to avoid duplicates that could arise from the cross-join between article_tags and article_source. |
| | 178 | * LEFT JOINs all four metadata tables (article_tags, tag, article_source, source) so articles with no tags or no sources still appear in the result. |
| | 179 | * Groups by a.id, a.slug, a.title, the minimal set needed to uniquely identify an article and return its key identifiers. |
| | 180 | |
| | 181 | ** Performance & Indexing ** |
| | 182 | |
| | 183 | ||= Scenario =||= Before index =||= After index =||= Improvement =|| |
| | 184 | ||Article metadata by article_id|| ~230ms || ~16ms || ~93% faster || |
| | 185 | |
| | 186 | One index was added: idx_article_source_article on article_source(article_id). The article_source join table did not have an index on article_id, so PostgreSQL was performing a sequential scan across it to find sources for a given article. Adding this index turned that into a direct lookup, reducing the query from 230 ms to 16 ms — a 93% improvement. The article_tags table already had an FK index on article_id from the schema definition, so no additional index was needed there. |
| | 187 | |
| | 188 | == Summary |
| | 189 | |
| | 190 | ||= View =||= Primary use =||= Before =||= After =||= Index =|| |
| | 191 | || vw_article_feed || Homepage / category browse || ~400 ms || ~13 ms || 2 indexes || |
| | 192 | || vw_article_detail || Single article page || ~66 ms || ~40 ms || 1 index || |
| | 193 | || vw_journalist_profile || Journalist profile page || ~2.7 s (v1) / ~2.0 s (v2) || ~2.5 s (v1) / ~600 ms (v2) || 2 indexes + rewrite || |
| | 194 | || vw_comment_thread || Article comment section || ~68 ms || ~68 ms || None || |
| | 195 | || vw_user_dashboard || User dashboard || ~1600 ms || ~15 ms || 2 indexes || |
| | 196 | || vw_fact_check_summary || Admin fact-check panel || ~68 ms || ~68 ms || None || |
| | 197 | || vw_article_metadata || SEO / article metadata || ~230 ms || ~16 ms || 1 index || |