- data/sql/99_nspd_entities_denorm.sql: 2 tables (cad_num PK) + 6 indexes (quarter_cad, GIST geom, partial WHERE purpose ILIKE '%многокв%') - nspd_denorm.py: denorm_parcel_feature / denorm_building_feature / denorm_dump - SAVEPOINT per row via with db.begin_nested() - ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:geom), 3857), 4326) - json.dumps(geom, ensure_ascii=False) for psycopg bind - All CAST(:x AS type) — no ❌:type - nspd_sync.py: inline denorm after _upsert_dump in harvest_quarter (non-fatal) - nspd_denorm_backfill.py: Celery one-shot task (per-quarter commit) - admin_etl.py: POST /api/v1/admin/etl/nspd-denorm-backfill (AdminTokenAuth) - 19 tests: coerce helpers, denorm parcel/building, aggregate counts Foundation for downstream features (МКД neighbors lookup, parcel attrs). Part of #94
171 lines
8.9 KiB
PL/PgSQL
171 lines
8.9 KiB
PL/PgSQL
-- 99_nspd_entities_denorm.sql
|
||
-- Context : Denormalized entity tables для NSPD parcel и building features.
|
||
-- nspd_quarter_dumps.features_json хранит все feature'ы в JSONB; эти
|
||
-- таблицы дают быстрый lookup по cad_num без json-unpacking на query time.
|
||
-- Источник: layer="parcels" (NSPD layer 36048) и layer="buildings" (36049)
|
||
-- из features_json. Геометрия трансформируется 3857→4326 при UPSERT.
|
||
--
|
||
-- Dependencies: 88_nspd_quarter_dumps.sql (logical; таблица должна существовать до backfill)
|
||
-- Deploy order: после 98_nspd_quarter_dumps_opportunity_flag.sql
|
||
-- Idempotent: yes — IF NOT EXISTS на всех объектах
|
||
-- Related:
|
||
-- - backend/app/services/scrapers/nspd_denorm.py :: denorm_parcel_feature / denorm_building_feature
|
||
-- - backend/app/workers/tasks/nspd_denorm_backfill.py :: backfill_all_dumps
|
||
-- - backend/app/workers/tasks/nspd_sync.py :: harvest_quarter (inline denorm)
|
||
-- Issue: #94 PR3
|
||
|
||
BEGIN;
|
||
|
||
-- ── nspd_parcels ──────────────────────────────────────────────────────────────
|
||
-- Denormalized parcel features из NSPD quarter dumps (layer="parcels", id 36048).
|
||
-- Каждая строка = один земельный участок из любого квартала.
|
||
-- PK cad_num: один участок может пересекать несколько кварталов, берём
|
||
-- последний harvest (ON CONFLICT DO UPDATE).
|
||
|
||
CREATE TABLE IF NOT EXISTS nspd_parcels (
|
||
-- Кадастровый номер участка (4-сегментный, e.g. '66:41:0204016:10').
|
||
-- Первичный ключ — natural, стабильный идентификатор в ЕГРН.
|
||
cad_num TEXT PRIMARY KEY,
|
||
|
||
-- Квартал из которого была взята последняя версия этого участка.
|
||
quarter_cad TEXT NOT NULL,
|
||
|
||
-- ВРИ (вид разрешённого использования) — properties.permitted_use из NSPD.
|
||
-- Nullable: НСПД не всегда возвращает ВРИ для старых записей.
|
||
permitted_use TEXT,
|
||
|
||
-- Категория земель (properties.land_category).
|
||
-- Nullable: может отсутствовать в ответе.
|
||
land_category TEXT,
|
||
|
||
-- Кадастровая стоимость ЗУ (properties.cost_value), руб.
|
||
cost_value NUMERIC,
|
||
|
||
-- Кадастровая стоимость за м² = cost_value / area_sqm.
|
||
-- Вычисляется при UPSERT; NULL если area_sqm = 0 или cost_value = NULL.
|
||
cost_per_m2 NUMERIC,
|
||
|
||
-- Площадь участка (properties.area), м².
|
||
area_sqm NUMERIC,
|
||
|
||
-- Адрес участка (properties.address).
|
||
address TEXT,
|
||
|
||
-- Геометрия участка в WGS84 (трансформируется из EPSG:3857 при UPSERT).
|
||
-- Geometry не Polygon т.к. НСПД иногда возвращает MultiPolygon или Point.
|
||
geom GEOMETRY(Geometry, 4326),
|
||
|
||
-- Дата snapshot'а (день harvest'а).
|
||
snapshot_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||
|
||
-- Timestamp последнего UPSERT.
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
COMMENT ON TABLE nspd_parcels IS
|
||
'Denormalized parcel features из NSPD quarter dumps (layer=parcels, id 36048). '
|
||
'PK = cad_num (последний harvest wins). Заполняется Celery task backfill_all_dumps '
|
||
'или inline в harvest_quarter. Источник: features_json JSONB колонка nspd_quarter_dumps. '
|
||
'Геометрия трансформирована EPSG:3857→4326. (#94 PR3)';
|
||
|
||
COMMENT ON COLUMN nspd_parcels.cad_num IS
|
||
'Кадастровый номер участка (4-сегментный). Natural PK из НСПД.';
|
||
|
||
COMMENT ON COLUMN nspd_parcels.quarter_cad IS
|
||
'3-сегментный квартал последнего harvest'а. Используется для debug/audit.';
|
||
|
||
COMMENT ON COLUMN nspd_parcels.cost_per_m2 IS
|
||
'Кадастровая стоимость за м² (cost_value / area_sqm). '
|
||
'NULL если area_sqm = 0 или cost_value отсутствует. Вычисляется в Python при UPSERT.';
|
||
|
||
-- Индекс для lookup по кварталу (join с nspd_quarter_dumps).
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_quarter_cad
|
||
ON nspd_parcels (quarter_cad);
|
||
|
||
-- GIST индекс на геометрию для spatial queries (соседние участки, ST_DWithin).
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_geom_gist
|
||
ON nspd_parcels USING GIST (geom);
|
||
|
||
-- Partial B-tree на cost_per_m2 для ценовых аналитических запросов.
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_parcels_cost_per_m2
|
||
ON nspd_parcels (cost_per_m2)
|
||
WHERE cost_per_m2 IS NOT NULL;
|
||
|
||
|
||
-- ── nspd_buildings ────────────────────────────────────────────────────────────
|
||
-- Denormalized building features из NSPD quarter dumps (layer="buildings", id 36049).
|
||
-- Каждая строка = одно здание/сооружение ЕГРН.
|
||
|
||
CREATE TABLE IF NOT EXISTS nspd_buildings (
|
||
-- Кадастровый номер здания (5-сегментный, e.g. '66:41:0204016:10:1').
|
||
cad_num TEXT PRIMARY KEY,
|
||
|
||
-- Квартал последнего harvest'а.
|
||
quarter_cad TEXT NOT NULL,
|
||
|
||
-- Назначение здания (properties.purpose).
|
||
-- Ключевые значения: 'Многоквартирный дом', 'Нежилое здание', etc.
|
||
purpose TEXT,
|
||
|
||
-- Количество надземных этажей (properties.floors_above_ground).
|
||
floors INTEGER,
|
||
|
||
-- Количество подземных этажей (properties.floors_underground).
|
||
floors_underground INTEGER,
|
||
|
||
-- Год постройки (properties.year_built).
|
||
year_built INTEGER,
|
||
|
||
-- Кадастровая стоимость здания (properties.cost_value), руб.
|
||
cost_value NUMERIC,
|
||
|
||
-- Площадь по данным ГКН (properties.build_record_area), м².
|
||
build_record_area NUMERIC,
|
||
|
||
-- Адрес здания (properties.address).
|
||
address TEXT,
|
||
|
||
-- Геометрия здания в WGS84 (трансформируется из EPSG:3857 при UPSERT).
|
||
geom GEOMETRY(Geometry, 4326),
|
||
|
||
-- Дата snapshot'а.
|
||
snapshot_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||
|
||
-- Timestamp последнего UPSERT.
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
COMMENT ON TABLE nspd_buildings IS
|
||
'Denormalized building features из NSPD quarter dumps (layer=buildings, id 36049). '
|
||
'PK = cad_num (последний harvest wins). Заполняется Celery task backfill_all_dumps '
|
||
'или inline в harvest_quarter. Ключевой use-case: МКД-соседи для P2-scoring. '
|
||
'Геометрия трансформирована EPSG:3857→4326. (#94 PR3)';
|
||
|
||
COMMENT ON COLUMN nspd_buildings.purpose IS
|
||
'Назначение здания (properties.purpose). Для МКД = "Многоквартирный дом". '
|
||
'Используется в partial index idx_nspd_buildings_mkd для быстрой фильтрации.';
|
||
|
||
COMMENT ON COLUMN nspd_buildings.floors IS
|
||
'Количество надземных этажей (properties.floors_above_ground из НСПД). '
|
||
'Nullable: старые ОКС в ЕГРН не всегда имеют этажность.';
|
||
|
||
-- Индекс для lookup по кварталу.
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_quarter_cad
|
||
ON nspd_buildings (quarter_cad);
|
||
|
||
-- GIST индекс на геометрию (МКД-соседи через ST_DWithin).
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_geom_gist
|
||
ON nspd_buildings USING GIST (geom);
|
||
|
||
-- Partial index для МКД (основной use-case P2-scoring).
|
||
-- ILIKE не работает в partial index WHERE clause — используем точный строковый prefixmatch.
|
||
-- Здания с purpose IS NULL или другим purpose — в idx_nspd_buildings_geom_gist.
|
||
CREATE INDEX IF NOT EXISTS idx_nspd_buildings_mkd
|
||
ON nspd_buildings (quarter_cad)
|
||
WHERE purpose = 'Многоквартирный дом';
|
||
|
||
COMMENT ON INDEX idx_nspd_buildings_mkd IS
|
||
'Partial index для быстрого lookup МКД по кварталу (P2-scoring neighbors). '
|
||
'WHERE purpose = exact строка из НСПД. (#94 PR3)';
|
||
|
||
COMMIT;
|