gendesign/data/sql/74_dedupe_unified_views.sql
lekss361 52bcf9d30c feat(db): canonical complexes + unified views + naming consistency
* Task 2+3: complexes.obj_class и district_name из Objective
  (350 ЖК с классом, 1525 с district; objective приоритетнее)

* Task 1 (fuzzy match orphan Objective ↔ domrf_kn):
  - pg_trgm extension
  - auto-merge 14 high-confidence (similarity ≥ 0.85)
  - v_complex_match_review для остальных 82 кандидатов
  - complexes 1752 → 1740 · cross-source 114 → 124

* Task 4 (naming unification без RENAME COLUMN):
  - GENERATED ALWAYS AS (...) STORED для синонимов
  - region_id в 15 таблиц + 9 partitions
  - developer_id в 7, developer_name в 5
  - COMMENT 'DEPRECATED' на region_cd/region_code/dev_id/dev_name

* Task 5 (unified scrape dashboard):
  - GET /api/v1/admin/scrape/all/runs + /all/logs (поверх v_scrape_runs_unified)
  - new page /admin/scrape/all с filter табами + 4 stat-плитки
  - +tab «📊 Все скраперы» в admin layout

Файлы: data/sql/75_unification_naming.sql · backend/app/api/v1/admin_scrape.py
       · frontend/src/app/admin/scrape/all/page.tsx · admin/layout.tsx
2026-05-10 21:11:20 +03:00

256 lines
13 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Phase B + D: унификация дублирующихся таблиц через VIEW (UNION ALL).
--
-- Стратегия: НЕ дропаем источники (они продолжают писать своими скраперами/
-- импортёрами), но создаём поверх единые view для удобных запросов из
-- аналитики и UI. Это zero-risk миграция: можно делать в любой момент
-- и откатывать DROP VIEW без потери данных.
--
-- После того как код в backend перейдёт на use views — старые имена таблиц
-- можно будет либо превратить в views поверх objединённой структуры, либо
-- мигрировать данные целиком. Сейчас — promotion path.
--
-- Применено в проде через mcp postgres 2026-05-10.
-- Идемпотентно через CREATE OR REPLACE VIEW.
-- ── 1. ekb_districts: merge geom-таблицы в основную ─────────────────────────
-- ekb_districts (9, dimension с метриками) + ekb_districts_geom (8, polygons)
-- → одна таблица ekb_districts с PostGIS-колонкой geom
-- + view-alias ekb_districts_geom для backward-compat.
ALTER TABLE ekb_districts
ADD COLUMN IF NOT EXISTS geom geometry(MultiPolygon, 4326),
ADD COLUMN IF NOT EXISTS osm_id BIGINT,
ADD COLUMN IF NOT EXISTS geom_fetched_at timestamptz;
UPDATE ekb_districts d
SET geom = g.geom, osm_id = g.osm_id, geom_fetched_at = g.fetched_at
FROM ekb_districts_geom g
WHERE g.district_name = d.district_name;
CREATE INDEX IF NOT EXISTS ekb_districts_geom_idx
ON ekb_districts USING GIST(geom) WHERE geom IS NOT NULL;
DROP TABLE IF EXISTS ekb_districts_geom;
CREATE OR REPLACE VIEW ekb_districts_geom AS
SELECT district_name, geom, osm_id, geom_fetched_at AS fetched_at
FROM ekb_districts WHERE geom IS NOT NULL;
-- ── 2. v_mortgage_unified — 4 источника ипотечных метрик в long-формате ─────
CREATE OR REPLACE VIEW v_mortgage_unified AS
SELECT
'cbr' AS source,
NULL::date AS snapshot_date,
period AS period_label,
region,
NULL::text AS bank_name,
title AS metric_kind,
'total' AS segment,
value,
NULL::numeric AS delta_pct,
series_id AS metric_id
FROM cbr_mortgage_series
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'total', total_credit_avg_rate, total_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'primary', primary_credit_avg_rate, primary_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'secondary', secondary_credit_avg_rate, secondary_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'total', total_credit_count::numeric, total_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'primary', primary_credit_count::numeric, primary_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'secondary', secondary_credit_count::numeric, secondary_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'total', total_credit_amount, total_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'primary', primary_credit_amount, primary_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'secondary', secondary_credit_amount, secondary_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_avg_rub', currency, credit_amount_avg, credit_amount_avg_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_avg_period_months', currency, credit_avg_period, credit_avg_period_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_debts_amount_rub', currency, credit_debts_amount, credit_debts_amount_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'overdue_pct', currency, credit_debts_overdue_percent, credit_debts_overdue_percent_delta, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'primary', primary_rate, NULL, NULL FROM domrf_mortgage_rates
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'secondary', secondary_rate, NULL, NULL FROM domrf_mortgage_rates
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'refinance', refinance_rate, NULL, NULL FROM domrf_mortgage_rates;
COMMENT ON VIEW v_mortgage_unified IS
'Long-формат всех 4 источников ипотечных метрик. metric_kind: avg_rate_pct '
'/ credit_count / credit_amount_rub / overdue_pct / etc. segment: primary / '
'secondary / total. ~3385 rows total. Источники не дропаются.';
-- ── 3. v_construction_metrics_unified — 5 launch/commissioning ──────────────
CREATE OR REPLACE VIEW v_construction_metrics_unified AS
SELECT 'planned_commissioning' AS source, snapshot_date, plan_year::text AS period_label,
'РФ' AS region, 'planned_total_th_sqm' AS metric_kind,
NULL::text AS breakdown_kind, NULL::text AS breakdown_value,
total_th_sqm AS value,
jsonb_build_object('escrow', escrow_th_sqm, 'fund', fund_th_sqm,
'no_attraction', no_attraction_th_sqm) AS extra
FROM domrf_planned_commissioning
UNION ALL
SELECT 'commissioning', snapshot_date, rep_year::text, region_name,
'fact_area_total', NULL, NULL, accumulated_fact_area,
jsonb_build_object('multifamily', accumulated_fact_area_multifamily,
'private', accumulated_fact_area_private,
'change', accumulated_fact_area_change,
'change_share', accumulated_fact_area_change_share)
FROM domrf_commissioning
UNION ALL
SELECT 'launch_monthly', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'launch_app_value', 'calc_type', calc_type, app_value,
jsonb_build_object('rnv_value', rnv_value)
FROM domrf_launch_monthly
UNION ALL
SELECT 'launch_monthly', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'launch_rnv_value', 'calc_type', calc_type, rnv_value, NULL
FROM domrf_launch_monthly
UNION ALL
SELECT 'launch_top', snapshot_date, rep_year::text, entity_name,
metric_type, dim_type, entity_id, value,
jsonb_build_object('calc_type', calc_type)
FROM domrf_launch_top
UNION ALL
SELECT 'launch_obj_class', snapshot_date, rep_year::text, 'РФ',
'launch_app_value', 'obj_class', obj_class_desc, app_value,
jsonb_build_object('rnv_value', rnv_value, 'calc_type', calc_type,
'obj_class_cd', obj_class_cd)
FROM domrf_launch_obj_class
UNION ALL
SELECT 'launch_obj_class', snapshot_date, rep_year::text, 'РФ',
'launch_rnv_value', 'obj_class', obj_class_desc, rnv_value,
jsonb_build_object('calc_type', calc_type)
FROM domrf_launch_obj_class;
COMMENT ON VIEW v_construction_metrics_unified IS
'Long-формат для 5 таблиц про launch/commissioning. metric_kind: '
'planned_total_th_sqm / fact_area_total / launch_app_value / launch_rnv_value. '
'breakdown_kind: calc_type / obj_class / dim_type. ~1579 rows.';
-- ── 4. v_sold_ready_unified — 4 sold_ready / sold_out ──────────────────────
CREATE OR REPLACE VIEW v_sold_ready_unified AS
SELECT 'sold_out' AS source, snapshot_date, NULL::text AS period_label,
territory_name AS scope, NULL::text AS breakdown_kind, NULL::text AS breakdown_value,
'sold_pct' AS metric_kind, sold_pct AS value,
jsonb_build_object('total_area_th_sqm', total_area_th_sqm,
'sold_area_th_sqm', sold_area_th_sqm,
'unsold_pct', unsold_pct,
'avg_price_per_sqm', avg_price_per_sqm,
'attracted_funds_mln_rub', attracted_funds_mln_rub) AS extra
FROM domrf_sold_out
UNION ALL
SELECT 'sold_ready_index', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', NULL, NULL, 'sold_ready_perc', sold_ready_perc,
jsonb_build_object('square_sum', square_sum, 'sold_sum', sold_sum,
'sold_perc', sold_perc, 'ready_perc', ready_perc)
FROM domrf_sold_ready_index
UNION ALL
SELECT 'sold_ready_dynamics', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'dynamic_chart_type', dynamic_chart_type,
dynamic_chart_type, value, NULL
FROM domrf_sold_ready_dynamics
UNION ALL
SELECT 'sold_ready_breakdown', snapshot_date,
(rep_year::text || '-' || LPAD(COALESCE(rep_month, 0)::text, 2, '0')),
COALESCE(NULLIF(dev_group_value, ''), 'РФ'),
chart_type, entity_key, 'sold_perc', sold_perc,
jsonb_build_object('ready_perc', ready_perc, 'sold_ready_perc', sold_ready_perc,
'square_sum', square_sum, 'fo_cd', fo_cd, 'region_cd', region_cd,
'obj_class', obj_class, 'ready_year', ready_year,
'city_population', city_population)
FROM domrf_sold_ready_breakdown;
COMMENT ON VIEW v_sold_ready_unified IS
'Long-формат 4 sold_ready таблиц. metric_kind: sold_pct / sold_ready_perc / '
'<dynamic_chart_type>. Big source: sold_ready_breakdown (~39k) — фильтруй '
'по chart_type/entity_key. Total: ~42858 rows.';
-- ── 5. v_scrape_runs_unified — 3 скрапера в одном журнале ───────────────────
CREATE OR REPLACE VIEW v_scrape_runs_unified AS
SELECT 'kn' AS scraper_type, run_id, started_at, finished_at, heartbeat_at,
status, error, NULL::text AS triggered_by,
region_codes::text AS scope, requests_count,
objects_count AS items_ok, NULL::integer AS items_failed,
flats_count AS sub_items_ok,
progress_obj_index AS progress_idx, total_obj_count AS progress_total,
jsonb_build_object('developers', developer_ids, 'snapshot_date', snapshot_date,
'resumed_from', resumed_from_run_id, 'params', params) AS extra
FROM kn_scrape_runs
UNION ALL
SELECT 'nspd', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, region_code::text, requests_count,
quarters_ok, quarters_failed, buildings_ok,
NULL, pending_count,
jsonb_build_object('waf_429_count', waf_429_count)
FROM nspd_scrape_runs
UNION ALL
SELECT 'objective', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, group_name, requests_count,
reports_ok, reports_failed, rows_lots,
NULL, NULL,
jsonb_build_object('rows_corpus_room', rows_corpus_room, 'rows_history', rows_history)
FROM objective_scrape_runs;
COMMENT ON VIEW v_scrape_runs_unified IS
'Унифицированный журнал прогонов для всех 3 скраперов. Полиморфные общие '
'поля (items_ok / sub_items_ok / scope) — для UI; специфичные — в extra '
'jsonb. Используй для общего admin /scrape dashboard вместо 3 отдельных endpoint-ов.';
-- ── 6. v_scrape_log_unified — kn + nspd логи ────────────────────────────────
CREATE OR REPLACE VIEW v_scrape_log_unified AS
SELECT 'kn' AS scraper_type, log_id, run_id, ts, level, stage,
obj_id::text AS entity_id, message
FROM kn_scrape_log
UNION ALL
SELECT 'nspd', log_id, run_id, ts, level, stage, cad_number, message
FROM nspd_scrape_log;
COMMENT ON VIEW v_scrape_log_unified IS
'Per-step log двух скраперов. entity_id = obj_id для kn / cad_number для nspd. '
'objective scraper пока не пишет log — добавим если понадобится debug.';