-- 222_db_audit_cleanup.sql -- Уборка по итогам ручного аудита схемы tradein (2026-08-06). Три независимых -- части, порядок между ними не важен (разные объекты, нет пересекающихся -- зависимостей). Идемпотентно целиком: IF EXISTS везде, CREATE OR REPLACE VIEW. -- -- ── A. Временные таблицы разовой чистки 02.07 ───────────────────────────────── -- tmp_purged_junk_houses_0702 / tmp_purged_junk_links_0702 — снэпшоты записей, -- вычищенных вручную 2026-07-02. Проверено на проде перед этой миграцией: -- * обе существуют под этими именами, суммарно 2.9 МБ -- (tmp_purged_junk_houses_0702 = 1608 kB, tmp_purged_junk_links_0702 = 1272 kB); -- * pg_constraint: ни один FK НЕ ссылается на них (confrelid пусто); -- * pg_depend: ни view, ни другой объект их не использует (только сами по себе). -- Дальше не нужны — были just-in-case снэпшотом на случай отката чистки, месяц -- прошёл без претензий. -- -- ── B. Пять строгих дублей индексов ─────────────────────────────────────────── -- Строгий дубль = тот же access method + тот же УПОРЯДОЧЕННЫЙ список колонок -- (включая ASC/DESC/NULLS) + тот же частичный предикат (или оба NULL) + тот же -- opclass, независимо от UNIQUE-флага и имени. Проверено запросом по -- pg_index/pg_stat_user_indexes/pg_opclass на проде — найдено РОВНО 5 пар (не 6, -- см. примечание ниже), в каждой паре оставляем индекс, несущий UNIQUE-constraint -- (дропнуть его нельзя без дропа constraint'а), дропаем чистый btree-дубль: -- -- agents: DROP agents_source_ext_idx -- (дубль agents_ext_source_ext_agent_id_key, btree (ext_source, ext_agent_id); -- 2 скана за всё время — планировщик и так предпочитал unique-версию) -- ekb_geoportal_buildings: DROP ix_ekb_geoportal_buildings_street_house -- (дубль ekb_geoportal_buildings_street_norm_house_norm_key, -- btree (street_norm, house_norm); 13 945 сканов, но unique-версия того же -- определения покрывает те же запросы — 70 702 скана на ней) -- house_placement_history: DROP hph_source_item_idx -- (дубль house_placement_history_source_ext_item_id_key, -- btree (source, ext_item_id); 0 сканов — полностью мёртв) -- house_reviews: DROP hr_source_ext_idx -- (дубль house_reviews_source_ext_review_id_key, btree (source, ext_review_id); -- 15 сканов) -- sellers: DROP sellers_source_idx -- (дубль sellers_source_ext_seller_id_key, btree (source, ext_seller_id); -- 2 скана) -- -- ПРИМЕЧАНИЕ (расхождение с ожиданием «шесть»): при систематической проверке -- (3 независимых метода: нормализованный DDL-текст, сравнение indkey/indoption, -- сравнение opclass) строгих дублей найдено 5, не 6. Два похожих кандидата -- ЦЕЛЕНАПРАВЛЕННО исключены — их «дубль» только по списку колонок, а порядок -- сортировки различается (ASC,ASC у unique-версии против ASC,DESC у второй), -- то есть это ТОТ ЖЕ класс исключения, что explicitly подтверждённые -- idx_lss_source_date/listings_snapshots_listing_date_idx (#2607, см. миграцию 225): -- * offer_price_history: oph_listing_time_idx (listing_id ASC, change_time DESC) -- против offer_price_history_listing_change_uq (listing_id ASC, change_time ASC) -- * houses_price_dynamics: hpd_house_dim_idx (..., month_date DESC) -- против houses_price_dynamics_dim_key (..., month_date ASC) -- Оба НЕ тронуты. Если «шесть» подразумевали один из них — нужно явное -- подтверждение, что смешанный порядок сортировки в конкретном запросе не -- используется (тем же способом, каким для idx_lss_source_date подтверждено -- обратное). -- -- ── C. v_data_quality — явный список колонок вместо SELECT * ───────────────── -- Сейчас: `WITH active_listings AS (SELECT * FROM listings WHERE is_active = true)`. -- Postgres разворачивает `*` в CREATE VIEW time в полный список колонок listings -- (89 на момент миграции) и фиксирует pg_depend на КАЖДУЮ из них — это то самое -- уже задокументированное в 214/216 предупреждение («View-зависимость: v_data_quality -- содержит SELECT * FROM listings, что фиксирует column-level зависимость на ВСЕ -- колонки»), которое обязывало делать DROP VIEW → DROP COLUMN → CREATE VIEW при -- каждой чистке listings. -- Фактически используются только 6 колонок active_listings ниже по телу view: -- id — IN (SELECT active_listings.id FROM active_listings) -- lat — pct_geocoded -- cadastral_number — pct_cadastr -- description — pct_description -- house_id_fk — JOIN houses h ON h.id = l.house_id_fk (pct_year_built) -- is_active — WHERE-фильтр самого CTE (создаёт зависимость даже не будучи -- в SELECT-списке, поэтому перечислен явно для наглядности) -- Поведение view НЕ меняется — только явный список вместо *. Тело SELECT (16 -- выходных колонок) скопировано без изменений из 216_dead_code_sweep.sql. -- COMMENT ON VIEW сохраняется автоматически (CREATE OR REPLACE VIEW не сбрасывает -- комментарий). -- -- Dependencies: 002_core_tables.sql (listings), 214_drop_dead_run_metrics.sql, -- 216_dead_code_sweep.sql (последний DDL v_data_quality). -- Идемпотентно: DROP TABLE IF EXISTS / DROP INDEX IF EXISTS / CREATE OR REPLACE VIEW. BEGIN; -- ── A ────────────────────────────────────────────────────────────────────── DROP TABLE IF EXISTS tmp_purged_junk_houses_0702, tmp_purged_junk_links_0702; -- ── B ────────────────────────────────────────────────────────────────────── DROP INDEX IF EXISTS agents_source_ext_idx; DROP INDEX IF EXISTS ix_ekb_geoportal_buildings_street_house; DROP INDEX IF EXISTS hph_source_item_idx; DROP INDEX IF EXISTS hr_source_ext_idx; DROP INDEX IF EXISTS sellers_source_idx; -- ── C ────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW v_data_quality AS WITH active_listings AS ( SELECT id, lat, cadastral_number, description, house_id_fk, is_active FROM listings WHERE is_active = true ) SELECT (SELECT count(*) FROM houses) AS houses_total, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id)) AS houses_with_source, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source = 'avito')) AS houses_with_avito, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source LIKE 'cian%')) AS houses_with_cian, (SELECT count(*) FROM houses h WHERE EXISTS (SELECT 1 FROM house_sources hs WHERE hs.house_id = h.id AND hs.ext_source = 'yandex')) AS houses_with_yandex, (SELECT count(*) FROM ( SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 2 ) sub) AS houses_2plus_sources, (SELECT count(*) FROM ( SELECT house_id FROM house_sources GROUP BY house_id HAVING count(*) >= 3 ) sub) AS houses_3plus_sources, (SELECT count(*) FROM active_listings) AS listings_active, (SELECT count(*) FROM ( SELECT listing_id FROM listing_sources WHERE listing_id IN (SELECT id FROM active_listings) GROUP BY listing_id HAVING count(*) >= 2 ) sub) AS listings_dedup_2sources, (SELECT count(*) FROM active_listings WHERE lat IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_geocoded, (SELECT count(*) FROM active_listings WHERE cadastral_number IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_cadastr, (SELECT count(*) FROM active_listings WHERE description IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_description, (SELECT count(*) FROM active_listings l JOIN houses h ON h.id = l.house_id_fk WHERE h.year_built IS NOT NULL) * 100.0 / NULLIF((SELECT count(*) FROM active_listings), 0) AS pct_year_built, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'avito') AS avito_last_scrape_ago, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'cian') AS cian_last_scrape_ago, NOW() - (SELECT max(scraped_at) FROM listings WHERE source = 'yandex') AS yandex_last_scrape_ago; COMMIT;