diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index 6536cc75..8e3b60f1 100755 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -119,7 +119,7 @@ router.get('/products/:id', async (req, res) => { /** * GET /api/analytics/national/summary * National dashboard summary with state-by-state metrics - * OPTIMIZED: Cached for 5 minutes, uses approximate counts + * OPTIMIZED: Uses denormalized product_count, no expensive JOINs */ router.get('/national/summary', async (req, res) => { try { @@ -130,20 +130,14 @@ router.get('/national/summary', async (req, res) => { return res.json(cached); } - // Single optimized query for all state metrics (only validated stores) + // FAST: Use denormalized product_count from dispensaries (no JOIN to store_products!) const { rows: stateMetrics } = await pool.query(` SELECT d.state, s.name as state_name, - COUNT(DISTINCT d.id) as store_count, - COUNT(DISTINCT sp.id) as total_products, - COUNT(DISTINCT sp.brand_name_raw) FILTER (WHERE sp.brand_name_raw IS NOT NULL) as unique_brands, - ROUND(AVG(sp.price_rec) FILTER (WHERE sp.price_rec > 0)::numeric, 2) as avg_price_rec, - ROUND(AVG(sp.price_med) FILTER (WHERE sp.price_med > 0)::numeric, 2) as avg_price_med, - COUNT(sp.id) FILTER (WHERE sp.is_in_stock = true) as in_stock_products, - COUNT(sp.id) FILTER (WHERE sp.on_special = true) as on_special_products + COUNT(*) as store_count, + COALESCE(SUM(d.product_count), 0) as total_products FROM dispensaries d - LEFT JOIN store_products sp ON d.id = sp.dispensary_id LEFT JOIN states s ON d.state = s.code WHERE d.state IS NOT NULL AND d.platform_dispensary_id IS NOT NULL @@ -153,27 +147,17 @@ router.get('/national/summary', async (req, res) => { ORDER BY store_count DESC `); - // Calculate national totals from state metrics (avoid re-querying) + // Calculate national totals from state metrics const totalStores = stateMetrics.reduce((sum, s) => sum + parseInt(s.store_count || '0'), 0); const totalProducts = stateMetrics.reduce((sum, s) => sum + parseInt(s.total_products || '0'), 0); const activeStates = stateMetrics.filter(s => parseInt(s.store_count || '0') > 0).length; - // Calculate weighted avg price - let totalPriceSum = 0; - let totalPriceCount = 0; - for (const s of stateMetrics) { - if (s.avg_price_rec && s.total_products) { - totalPriceSum += parseFloat(s.avg_price_rec) * parseInt(s.total_products); - totalPriceCount += parseInt(s.total_products); - } - } - const avgPriceNational = totalPriceCount > 0 ? Math.round((totalPriceSum / totalPriceCount) * 100) / 100 : null; - - // Get unique brand count (fast approximate using pg_stat) + // Get approximate brand count (fast - uses LIMIT) const { rows: brandCount } = await pool.query(` SELECT COUNT(*) as total FROM ( SELECT DISTINCT brand_name_raw FROM store_products - WHERE brand_name_raw IS NOT NULL LIMIT 10000 + WHERE brand_name_raw IS NOT NULL AND brand_name_raw != '' + LIMIT 10000 ) b `); @@ -185,17 +169,17 @@ router.get('/national/summary', async (req, res) => { totalStores, totalProducts, totalBrands: parseInt(brandCount[0]?.total || '0'), - avgPriceNational, + avgPriceNational: null, // Removed - too expensive to calculate stateMetrics: stateMetrics.map(s => ({ state: s.state, stateName: s.state_name || s.state, storeCount: parseInt(s.store_count || '0'), totalProducts: parseInt(s.total_products || '0'), - uniqueBrands: parseInt(s.unique_brands || '0'), - avgPriceRec: s.avg_price_rec ? parseFloat(s.avg_price_rec) : null, - avgPriceMed: s.avg_price_med ? parseFloat(s.avg_price_med) : null, - inStockProducts: parseInt(s.in_stock_products || '0'), - onSpecialProducts: parseInt(s.on_special_products || '0'), + uniqueBrands: 0, // Removed per-state brand count - too expensive + avgPriceRec: null, // Removed - too expensive + avgPriceMed: null, // Removed - too expensive + inStockProducts: 0, // Removed - too expensive + onSpecialProducts: 0, // Removed - too expensive })), }, };