gendesign/tradein-mvp/backend/data/sql/222_db_audit_cleanup.sql
lekss361 0535fa209a
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m2s
Deploy Trade-In / build-backend (push) Successful in 30s
Deploy Trade-In / deploy (push) Successful in 2m3s
chore(tradein/db): уборка временных таблиц, дублей индексов и звёздочки в v_data_quality (#2746)
Миграции 222 (DROP 2 tmp-таблиц + 5 строгих дублей индексов + v_data_quality с явным списком колонок) и 225 (CREATE INDEX CONCURRENTLY под FK listing_source_snapshots.run_id).

Проверено на прод-БД в BEGIN…ROLLBACK и на чистой схеме (полный bootstrap 225 миграций в одноразовом контейнере).
2026-08-06 18:27:39 +00:00

140 lines
11 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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