-- 214_drop_dead_run_metrics.sql -- Purpose (#2674): удалить показатели, которые не могут быть ненулевыми. -- -- Один класс находок: колонка есть, админка её показывает, писателя нет ни одного — -- и ноль читается как «всё чисто», а не как «мы это не считаем». Такой показатель -- хуже отсутствующего: он создаёт ложную уверенность. Числа с прода 2026-08-06. -- -- 1. listings.is_outlier + v_data_quality.outliers_flagged -- 93 408 строк listings, is_outlier = false у ВСЕХ, NULL у нуля — то есть -- только DEFAULT из 002, ни одного UPDATE за всю историю. Писателя нет и не -- задумывалось: «выброс» у эстиматора вычисляется Tukey-фильтром по КОНКРЕТНОЙ -- подборке аналогов (estimator._filter_outliers) и живёт ровно один запрос — -- один и тот же лот выброс для одной оценки и нормальный аналог для соседней. -- Persist-флаг на объявлении такое отношение выразить не может в принципе, -- поэтому реализовать «пометку» нечем: это остаток отменённой затеи -- («помечено модели после aggregation», 002_core_tables.sql), а не недоделка. -- -- 2. scrape_runs.run_type -- 3244 прогона, ровно одно значение 'city_sweep' — DEFAULT из 051; ни одно -- место кода run_type не задаёт. Колонка подписывала «city_sweep» прогоны, -- которые никаким sweep не были: proxy_healthcheck (1630 шт.), -- deactivate_stale_*, sber_index_pull, rosreestr_dkp_import. Что именно бежало, -- честно называет source — второй, вырожденной оси не нужно. -- -- 3. scrape_runs.http_requests / http_errors / returning_count / disappeared_count -- Ноль во всех 3244 прогонах с миграции 015, писателя нет вообще. -- HTTP-запросы не считает никто ни в одном фетчере — заполнить нечем без -- сквозной инструментации всех путей. Ошибки и «пропало/вернулось» УЖЕ -- считает тот, кто их знает, и кладёт в counters jsonb: errors_count у -- pipeline, deactivated/revived у deactivate_stale_*. Отдельные колонки были -- бы вторым, иначе определённым счётчиком того же слова. -- -- ⚠️ View-зависимость: v_data_quality (последний DDL — 095_dead_schema.sql) читает -- listings.is_outlier и содержит CTE `SELECT * FROM listings`, что фиксирует -- column-level зависимость на все колонки. Порядок тот же, что в 095: -- DROP VIEW → DROP COLUMN → CREATE VIEW (уже без outliers_flagged). -- -- Dependencies: 002_core_tables.sql, 015_scrape_runs.sql, 051_scrape_runs_extend.sql, -- 095_dead_schema.sql (последний DDL v_data_quality). -- Идемпотентно: DROP VIEW IF EXISTS / DROP COLUMN IF EXISTS / CREATE OR REPLACE VIEW. BEGIN; -- ── 1. is_outlier: снять зависимый view, дропнуть колонку, пересоздать view ─── DROP VIEW IF EXISTS v_data_quality; ALTER TABLE IF EXISTS listings DROP COLUMN IF EXISTS is_outlier; -- DDL идентичен 095, минус строка outliers_flagged (см. п.1 шапки). 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 cadastral_number 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; -- Комментарий из 095 утверждал, что view «refreshed on-demand by /api/v1/admin/ -- data-quality endpoint». Это неправда с момента переписывания ручки: живой -- /api/v1/admin/scraper/data-quality строит собственный запрос по listings/houses и -- этого view не касается, читателей в коде нет ни одного (проверено #2674). PR, -- тезис которого «ложный показатель хуже отсутствующего», не имеет права нести -- ложное утверждение о читателе — пишем как есть. COMMENT ON VIEW v_data_quality IS 'KPI-снимок для РУЧНЫХ psql-запросов. Читателей в коде нет (проверено #2674): ' '/api/v1/admin/scraper/data-quality считает свои метрики сам и этот view не трогает. ' '#2674: outliers_flagged убран — is_outlier не писал никто, «выброс» определён только ' 'внутри одной подборки аналогов (estimator._filter_outliers), не на объявлении.'; -- ── 2-3. scrape_runs: вырожденный run_type + четыре счётчика без писателя ───── ALTER TABLE IF EXISTS scrape_runs DROP COLUMN IF EXISTS run_type, DROP COLUMN IF EXISTS http_requests, DROP COLUMN IF EXISTS http_errors, DROP COLUMN IF EXISTS returning_count, DROP COLUMN IF EXISTS disappeared_count; COMMIT;