-- 046_views.sql -- Purpose: Analytical / monitoring views for Phase 1.1. -- - v_price_divergence (Cross_Source_Matching sec 2.2 + 4.6) — cross-source price disagreement -- - v_cross_source_health (Master Plan sec 8.4) — listing-level spread metrics -- - v_data_quality (Master Plan sec 8.1) — KPI dashboard rows -- Dependencies: house_sources, listing_sources, houses, listings, house_placement_history. -- All CREATE OR REPLACE — idempotent. BEGIN; -- ───────────────────────────────────────────────────────────────── -- v_price_divergence — Cross_Source sec 2.2: 5%+ spread between sources for same canonical listing -- ───────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW v_price_divergence AS SELECT listing_id, ARRAY_AGG(DISTINCT ext_source) AS sources, MAX(price_rub) - MIN(price_rub) AS price_spread, (MAX(price_rub) - MIN(price_rub))::float / NULLIF(MIN(price_rub), 0) * 100 AS spread_pct, COUNT(*) AS source_count FROM listing_sources WHERE price_rub IS NOT NULL GROUP BY listing_id HAVING (MAX(price_rub) - MIN(price_rub))::float / NULLIF(MIN(price_rub), 0) > 0.05; COMMENT ON VIEW v_price_divergence IS 'Listings with cross-source price spread > 5%. Used by quality monitor + manual review queue.'; -- ───────────────────────────────────────────────────────────────── -- v_cross_source_health — Master Plan sec 8.4: listing-level spread breakdown -- ───────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW v_cross_source_health AS SELECT listing_id, array_agg(ext_source ORDER BY price_rub) AS sources, array_agg(price_rub ORDER BY price_rub) AS prices, max(price_rub) - min(price_rub) AS spread, (max(price_rub) - min(price_rub))::float / NULLIF(min(price_rub), 0) * 100 AS spread_pct, count(*) AS source_count FROM listing_sources WHERE price_rub IS NOT NULL GROUP BY listing_id HAVING count(*) >= 2; COMMENT ON VIEW v_cross_source_health IS 'Per-listing cross-source price aggregation. Alert if spread_pct > 15% on > 100 listings.'; -- ───────────────────────────────────────────────────────────────── -- v_data_quality — Master Plan sec 8.1: single-row KPI snapshot -- ───────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW v_data_quality AS WITH active_listings AS ( SELECT * FROM listings WHERE is_active = true ) SELECT (SELECT count(*) FROM houses) AS houses_total, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id)) AS houses_with_source, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source = 'avito')) AS houses_with_avito, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source LIKE 'cian%')) AS houses_with_cian, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source = 'yandex')) AS houses_with_yandex, (SELECT count(*) FROM ( SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 2 ) sub) AS houses_2plus_sources, (SELECT count(*) FROM ( SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 3 ) sub) AS houses_3plus_sources, (SELECT count(*) FROM active_listings) AS listings_active, (SELECT count(*) FROM ( SELECT listing_id FROM listing_sources WHERE listing_id IN (SELECT id FROM active_listings) GROUP BY listing_id HAVING count(*) >= 2 ) sub) AS listings_dedup_2sources, (SELECT count(*) FROM active_listings WHERE lat IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_geocoded, (SELECT count(*) FROM active_listings WHERE kadastr_num IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_cadastr, (SELECT count(*) FROM active_listings WHERE description IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_description, (SELECT count(*) FROM active_listings l JOIN houses h ON h.id = l.house_id_fk WHERE h.year_built IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_year_built, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'avito') AS avito_last_scrape_ago, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'cian') AS cian_last_scrape_ago, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'yandex') AS yandex_last_scrape_ago, (SELECT count(*) FROM v_price_divergence) AS price_disagreements_count, (SELECT count(*) FROM listings WHERE is_outlier = true) AS outliers_flagged; COMMENT ON VIEW v_data_quality IS 'KPI snapshot. Refreshed on-demand by /api/v1/admin/data-quality endpoint (Master Plan sec 8.1).'; COMMIT;