80_complexes_backfill_nulls.sql: - cad_quarter: +722 rows via ST_Contains spatial join on cad_quarters_geom - developer_id: +247 rows via fuzzy name match on domrf_developers - obj_class: +2 rows via objective_lots.class mode - cad_building_num TEXT dropped (100% NULL; relation lives in cad_buildings.complex_id) - v_complex_full rebuilt: cad_building_nums TEXT[] replaces dead column - v_complex_buildings created: complex_id → cad_building_nums[], buildings_count 81_job_settings.sql: - New table job_settings (job_type PK, enabled, queue_name, cron_schedule, rate_ms, max_retries, max_concurrency, extra_config JSONB) - Seeded 4 rows: scrape_kn (queue=scrape_kn, '15 4 * * mon'), nspd_geo (queue=geo, rate_ms=600), objective_etl, objective_sync (cron from prod objective_sync_config) - objective_sync_config marked DEPRECATED via COMMENT — kept for backward compat until backend refactor Both already applied on prod via postgres MCP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
223 lines
9.7 KiB
PL/PgSQL
223 lines
9.7 KiB
PL/PgSQL
-- 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;
|