-- 80_complexes_backfill_nulls.sql -- Quick-win backfill для public.complexes через spatial joins + lookups. -- Закрывает NULL'ы без новых scrapers. -- -- Что делает: -- A. cad_quarter — spatial join ST_Contains(cad_quarters_geom.geom, complexes.geom) -- B. cad_building_num — колонка 100% NULL, никто не пишет в неё. -- Выбрана Опция A: колонка дропается, relation живёт через -- cad_buildings.complex_id (FK 1:N, уже существует). -- Создаётся VIEW v_complex_buildings. -- v_complex_full пересоздаётся без cad_building_num + с -- cad_building_nums TEXT[] из агрегации по cad_buildings. -- C. district_name — spatial join ST_Contains(ekb_districts.geom, complexes.geom) -- (только EKB-покрытие, ~9 районов) -- D. developer_id — fuzzy match lower(trim(developer_name)) с domrf_developers. -- Ambiguous (>1 match по имени) — пропускаем, берём только -- уникальные совпадения. -- E. obj_class — источник данных domrf_kn_objects.obj_class = NULL для -- всех 4548 строк; objective_lots.class даёт только 2 complexes. -- Шаг выполняется (закрывает эти 2), но большого эффекта нет. -- F. wall_type — domrf_kn_objects.wall_type = NULL для всех строк; -- objective_lots не содержит wall_type. -- Источника данных нет — шаг пропускается. -- -- Dependencies: -- - public.complexes (таблица) -- - public.cad_quarters_geom (GIST индекс cad_quarters_geom_gist) -- - public.ekb_districts (GIST индекс ekb_districts_geom_idx) -- - public.domrf_developers (btree на developer_id PK) -- - public.objective_lots (FK complex_id, поле class) -- - public.cad_buildings (FK complex_id) -- - public.v_complex_full (пересоздаётся — DROP CASCADE + CREATE) -- -- Apply order: после 78_drop_use_rosreestr2coord.sql -- -- Идемпотентность: -- - UPDATE ... WHERE column IS NULL — повторный запуск не перезапишет NOT NULL. -- - DROP TABLE IF EXISTS / CREATE OR REPLACE VIEW — безопасны. -- - DROP COLUMN IF EXISTS — безопасен. BEGIN; -- ============================================================ -- A. cad_quarter — spatial join -- ============================================================ -- Spatial indexes используются (EXPLAIN: Index Scan on cad_quarters_geom). -- LIMIT 1 защищает от boundary случаев когда точка лежит на двух кварталах. UPDATE complexes c SET cad_quarter = ( SELECT q.cad_number FROM cad_quarters_geom q WHERE ST_Contains(q.geom, c.geom) LIMIT 1 ) WHERE c.cad_quarter IS NULL AND c.geom IS NOT NULL; -- ============================================================ -- B. cad_building_num — Опция A: дропаем колонку, relation через cad_buildings.complex_id -- ============================================================ -- Шаг B.1: пересоздать v_complex_full без cad_building_num. -- Вместо неё добавляем cad_building_nums TEXT[] (агрегация из cad_buildings). -- DROP CASCADE нужен потому что PostgreSQL не позволяет CREATE OR REPLACE VIEW -- если изменяется список/порядок колонок (cad_building_num убираем, cad_building_nums добавляем). DROP VIEW IF EXISTS v_complex_full CASCADE; CREATE OR REPLACE VIEW v_complex_full AS SELECT c.id AS complex_id, c.canonical_name, c.developer_name, c.region_id, c.district_name, c.obj_class, c.address, c.latitude, c.longitude, (c.geom IS NOT NULL) AS has_geom, c.flat_count, c.square_living, c.site_status, c.cad_quarter, -- cad_building_nums заменяет cad_building_num: агрегат из cad_buildings.cad_num ( SELECT array_agg(b.cad_num ORDER BY b.cad_num) FROM cad_buildings b WHERE b.complex_id = c.id ) AS cad_building_nums, ( SELECT COUNT(*) FROM cad_buildings b WHERE b.complex_id = c.id ) AS cad_buildings_n, ( SELECT array_agg(DISTINCT cs.source ORDER BY cs.source) FROM complex_sources cs WHERE cs.complex_id = c.id ) AS sources, ( SELECT COUNT(*) FROM complex_sources cs WHERE cs.complex_id = c.id ) AS source_links_n, ( SELECT COUNT(DISTINCT cs.source) FROM complex_sources cs WHERE cs.complex_id = c.id ) AS source_kinds_n, ( SELECT array_agg(cs.source_external_id ORDER BY cs.source_external_id) FROM complex_sources cs WHERE cs.complex_id = c.id AND cs.source = 'domrf_kn' ) AS domrf_obj_ids, ( SELECT cs.source_id FROM complex_sources cs WHERE cs.complex_id = c.id AND cs.source = 'objective' LIMIT 1 ) AS objective_project_name, ( SELECT COUNT(*) FROM domrf_kn_flats f WHERE f.obj_id IN ( SELECT cs.source_external_id FROM complex_sources cs WHERE cs.complex_id = c.id AND cs.source = 'domrf_kn' AND cs.source_external_id IS NOT NULL ) ) AS kn_flats_n, ( SELECT COUNT(*) FROM objective_lots ol WHERE ol.complex_id = c.id ) AS objective_lots_n, c.primary_source, c.created_at, c.updated_at FROM complexes c; -- Шаг B.2: дропаем колонку cad_building_num (100% NULL, индекс дропается автоматически) ALTER TABLE complexes DROP COLUMN IF EXISTS cad_building_num; -- Шаг B.3: VIEW v_complex_buildings — удобный агрегат complex → buildings CREATE OR REPLACE VIEW v_complex_buildings AS SELECT c.id AS complex_id, c.canonical_name, array_agg(DISTINCT b.cad_num ORDER BY b.cad_num) AS cad_building_nums, COUNT(b.cad_num) AS buildings_count FROM complexes c LEFT JOIN cad_buildings b ON b.complex_id = c.id GROUP BY c.id, c.canonical_name; -- ============================================================ -- C. district_name — spatial join (EKB coverage only) -- ============================================================ -- EXPLAIN: Index Scan on ekb_districts_geom_idx — GIST index используется. -- Закрывает только complexes внутри 9 районов EKB (geom IS NOT NULL). -- Complexes без geom или за пределами EKB остаются NULL. UPDATE complexes c SET district_name = ( SELECT d.district_name FROM ekb_districts d WHERE d.geom IS NOT NULL AND ST_Contains(d.geom, c.geom) LIMIT 1 ) WHERE c.district_name IS NULL AND c.geom IS NOT NULL; -- ============================================================ -- D. developer_id — fuzzy name match с domrf_developers -- ============================================================ -- Берём только случаи с ровно одним совпадением по нормализованному имени -- (lower + trim). Ambiguous (29 complexes с ≥2 candidates) — пропускаем. UPDATE complexes c SET developer_id = ( SELECT d.developer_id FROM domrf_developers d WHERE lower(trim(d.developer_name)) = lower(trim(c.developer_name)) LIMIT 1 ) WHERE c.developer_id IS NULL AND c.developer_name IS NOT NULL AND ( -- только если ровно одна запись в domrf_developers с таким именем SELECT COUNT(DISTINCT d2.developer_id) FROM domrf_developers d2 WHERE lower(trim(d2.developer_name)) = lower(trim(c.developer_name)) ) = 1; -- ============================================================ -- E. obj_class — via objective_lots.class (2 complexes покрывается) -- ============================================================ -- domrf_kn_objects.obj_class = NULL для всех строк (источник не заполнен). -- objective_lots.class — единственный источник; покрывает ~2 complexes с NULL obj_class. -- Берём mode (наиболее частый класс) по complex_id. UPDATE complexes c SET obj_class = ( SELECT ol.class FROM objective_lots ol WHERE ol.complex_id = c.id AND ol.class IS NOT NULL GROUP BY ol.class ORDER BY COUNT(*) DESC LIMIT 1 ) WHERE c.obj_class IS NULL AND EXISTS ( SELECT 1 FROM objective_lots ol2 WHERE ol2.complex_id = c.id AND ol2.class IS NOT NULL ); -- ============================================================ -- F. wall_type — источника данных нет -- ============================================================ -- domrf_kn_objects.wall_type = NULL для всех 4548 строк. -- objective_lots не содержит wall_type. -- Шаг пропускается — нет источника для backfill. COMMIT;