-- 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;