Skip to content

DB object 장부 — 표·제약·인덱스·권한·보존의 정본 (v0.18.0 D12, 이슈 #1328)

한 문장 — public 스키마의 표 77개·뷰 3개 전부에 대해 어느 도메인의 것이고, 원본/투영/참조/시스템 중 무엇이며, 누가 쓰고 누가 읽고, 얼마나 보존하고 어떻게 지워지며, 지금 판정이 무엇인지를 한 곳에서 보여 주는 장부다. 표 구조(열·PK/FK/UNIQUE/CHECK·인덱스·RLS·정책·트리거·권한)는 supabase/schema.sql(재생 DB 의 덤프)에서 기계가 뽑고, 보존·삭제·결론은 사람이 정책 파일에 적는다. 이 문서의 §3 은 생성 절이며 손으로 고치지 않는다.

D10 변경분 — 2026-09-10, v0.18.0 후보

아래 생성 절은 당시 스냅샷이다. D10의 최신 구조는 앱 supabase/contracts/db-objects.json통계 유지 작업 계약에서 확인한다. 전체 public 표 87·뷰 3·인덱스 150·FK 167, 정책 누락 0, 중복/접두 인덱스 0이며 기존 FK 예외 43개는 증가하지 않았다.

변경 객체등급·보존·조회 경로
stats_maintenance_cursors시스템 제어. 소스 4행의 cursor/watermark·순회·호출 수를 유지
stats_maintenance_requests시스템 제어. 사용자별 마지막 job/target/요청 키. 계정 삭제 cascade, job 삭제 시 참조 null. job FK 부분 인덱스
stats_maintenance_retries시스템 제어. 실패 사용자별 1행, 성공 후 삭제. (next_attempt_at,user_id)로 최대 25명
stats_maintenance_catalog시스템 제어. scope별 pending token·사용자 순회 위치, 최신 pass 전달 후 삭제. (changed_at,scope_id)
session_maintenance_cursor_idx완료 세션 PK 순회용 partial index
user_training_period_stats_updated_idx사용자별 최신 updated_at 1행 확인. 전체 사용자 MAX 집계 제거
user_pr_overview_rollover_state_due_idx(next_refresh_at,applied_catalog_version,snapshot_generation,user_id)(next_refresh_at,user_id)

원본 세션·세트는 변경하지 않는다. 새 제어표의 전 열은 앱 원본 등급 등록부의 systemTables에 system으로 등록한다. 데이터가 있는 DB의 인덱스 생성은 high 위험으로 분류하고 업데이트 기록에 populated upgrade·행/버퍼/시간 증거를 연결한다.

1. 읽는 법 — 유저 A 의 저장 한 번이 지나는 표

유저 A 가 어제 기록의 세트 하나를 고쳐 저장하면 서버는 ① session 의 개정 번호를 올리고 ② A 가 보내지 않은 옛 자식 행(exercise_set_partexercise_setsession_exercise_partsession_exercise)을 지우고 새 행을 넣는다. ③ 세트 행 하나를 지울 때마다 Postgres 는 그 행을 가리키는 FK 가 있는 표(user_exercise_records·user_exercise_session_rollups·user_exercise_strength_*·user_exercise_max_rep_observations)에서 "참조하는 행이 남았나" 를 확인한다. ④ 저장 문은 통계 잡을 큐에 넣고, 매분 worker 가 투영 표를 다시 쓴다. ⑤ 다음 화면은 투영 표를 읽는다.

이 장부는 그 길의 표마다 다음을 붙인다.

어디서 오나
등급fact = 유저 원본(열 단위 등급은 user-fact-columns.json) · projection = 통계 투영(stats-projection-writers.json) · reference = 마이그레이션이 시드하는 참조 데이터(reference-data-tables.json) · system = 운영·플랫폼·인입 staging세 계약 파일
owner 연결어느 열이 사용자를 가리키는가(직접 user_id, 또는 부모 표 경유)등급 목록의 owner 명세, 없으면 FK 로 추정
작성자INSERT/UPDATE/DELETE/TRUNCATE 문장이 있는 함수(i/u/d/t), 닫힌 출력 목록을 쓰는 동적 publisher(;dynamic), 트리거 함수()supabase/definitions/** 함수 본문과 stats_projection_tables_v1의 출력 목록
읽는 곳그 표를 읽는 함수·뷰·앱 파일(Supabase 클라이언트가 표를 직접 읽는 곳)D03 registry 의존성 + src/react/services·api·supabase/functions 스캔
보존 / 삭제 경로얼마나 두고, 무엇이 지우는가(유저 본인·계정 삭제 cascade·purge cron·없음)정책 파일(사람)
결론유지 근거 / 수정 예정(D12 마이그레이션) / 인계(다른 담당 이슈) / 확인 필요정책 파일(사람)
상태나열 = 장부에 올렸다 · 검증 = 증거(pgTAP·실행계획·PR·프로브)가 정책 파일 evidence 에 있다정책 파일(사람)

"나열" 과 "검증" 을 따로 두는 이유: 파일을 목록에 올린 것과 그 표의 제약·권한·조회가 실제로 시험된 것은 다른 일이다. D12 종료 검사(--strict)는 필수 표 전부가 검증 이고 확인 필요 가 0 이어야 통과한다.

#1432 실행 경계: claim_stats_projection_job_v1은 작업/lease를 먼저 확정하고, compute_stats_projection_job_v1은 원본을 읽어 임시 표에서 계산한 뒤 user_stats_projection_runs.payload에 미발행 결과를 보관한다. publish_stats_projection_job_v1stats_projection_tables_v1의 17개 public 출력에 delete/insert를 실행하고 job 완료 및 applied 세대 전환을 같은 트랜잭션에서 확정한다. 생성 절은 이 동적 publisher를 17개 표 모두에 표시하며 기존 privileged 유지보수 작성자도 함께 표시한다. pg_temp 계산은 public 출력의 작성자로 세지 않는다. 닫힌 목록의 정적 분석은 실제 게시·경합·롤백 검증을 대신하지 않으며, 상세 책임은 통계 DAG §0에 있다.

실행 제어 행의 보존은 db-objects-policy.jsonuser_stats_projection_runs 항목을 따른다. payload는 게시·실패 시 비우고 실행 진단 행은 연결된 작업/계정의 삭제 cascade까지 남는다. 원본 세션·세트와 직접 연결된 FK는 두지 않아 계산이 그 원본의 수정·삭제를 잠그지 않게 한다.

2. 규칙 — 새 표·새 인덱스를 만드는 PR 이 할 일

  1. 표를 만들면 같은 PR 에서 db-objects-policy.json 에 보존·삭제·결론을 적는다(npm run db:model -- --init-policy 가 빈 칸을 만들어 준다). 정책이 없는 표는 npm test 가 거부한다.
  2. npm run db:model 로 생성물과 §3 을 다시 쓴다. 생성물이 낡으면 npm test 가 거부한다.
  3. 인덱스 판정은 닫힌 목록이다(npm run db:model:check, npm test 도 같은 판정): 같은 열 집합 인덱스 쌍은 0 이어야 하고, 참조 열 인덱스가 없는 FK 와 앞부분이 다른 인덱스에 포함된 비고유 인덱스는 정책 파일 findings이유와 함께 적힌 것만 통과한다. 새 FK 를 만들면 같은 PR 에서 참조 열 인덱스를 만들거나(삭제·저장 경로), 지우지 않는 참조 데이터·드문 관리자 경로라는 이유를 findings.fkWithoutIndex 에 적는다. 고쳐져서 사라진 항목은 목록에서 빼야 한다(낡은 목록도 실패). "열 IS NOT NULL" 부분 인덱스는 그 열의 단일 FK 검사를 받는 것으로 친다.
  4. RLS 꺼짐·정책 0 인 표는 판정에 그대로 실린다(§3 첫 절) — 늘리면 PR 본문에 근거를 적는다.
  5. 원본 등급 열을 UPDATE/DELETE 하는 문장은 원본 불변 계약의 수리 티켓이 먼저다. 이 장부는 그 규칙을 바꾸지 않는다.

3. 장부 (생성 절)

<!-- db-model-ledger:generated:begin --> 생성: npm run db:model · 스냅샷 지문 f1ef28e280b9 · 표 80·뷰 3·열 1112·인덱스 139(+PK/UNIQUE 103)·FK 158·CHECK 310·정책 81·트리거 104 · 등급 fact 23·system 18·reference 18·projection 21 · 결론 유지 근거 74·인계 9 · 상태 검증 83

도메인 요약

도메인등급결론상태
workout71뷰 1·fact 6·system 1유지 근거 7·인계 1검증 8
plan01뷰 1유지 근거 1검증 1
calendar40projection 2·system 1·fact 1유지 근거 4검증 4
pr110reference 3·projection 6·fact 2유지 근거 11검증 11
volume20projection 2유지 근거 2검증 2
catalog70reference 5·projection 1·fact 1유지 근거 7검증 7
profile20fact 2유지 근거 2검증 2
social90fact 5·system 3·reference 1유지 근거 9검증 9
group50fact 4·system 1유지 근거 5검증 5
onboarding10fact 1유지 근거 1검증 1
auth10system 1인계 1검증 1
import100reference 2·system 8유지 근거 6·인계 4검증 10
stats171reference 7·projection 10·뷰 1유지 근거 16·인계 2검증 18
durable20system 1·fact 1유지 근거 2검증 2
shell10system 1유지 근거 1검증 1
db-platform10system 1인계 1검증 1

기계 판정 (schema.sql 에서 계산)

  • 같은 열 집합 인덱스 쌍 0: 없음
  • 앞부분이 다른 인덱스에 포함된 비고유 인덱스 0: 없음
  • 참조 열 인덱스가 없는 FK 42: body_metrics.import_batch_id→inbody_import_batches · content_reports.resolved_by_user_id→profiles · content_reports.target_comment_id→session_comments · content_takedowns.report_id→content_reports · content_takedowns.taken_down_by_user_id→profiles · exercise_archetypes.rep_exercise_id→exercises · exercise_business_role_memberships.exercise_id→exercises · exercise_external_mappings.exercise_id→exercises · exercise_external_mappings.exercise_id→exercises(cascade) · exercise_external_mappings.reviewed_by→auth.users · exercise_set_part.load_percent_base_exercise_id→exercises · exercise_strength_standards.exercise_id→exercises · external_exercise_interpretations.reviewed_by→auth.users · session_exercise_part.exercise_id→exercises · strength_estimation_policy_versions.curve_id+curve_version→strength_estimation_curve_versions · user_exercise_favorites.exercise_id→exercises · user_exercise_max_rep_observations.policy_id+policy_version→max_rep_estimation_policy_versions · user_exercise_max_rep_observations.exercise_id→exercises(cascade) · user_exercise_period_stats.exercise_id→exercises(cascade) · user_exercise_pr_events.exercise_id→exercises(cascade) · user_exercise_pr_states.exercise_id→exercises(cascade) · user_exercise_records.exercise_id→exercises(cascade) · user_exercise_session_rollups.exercise_id→exercises(cascade) · user_exercise_session_rollups.max_rep_policy_id+max_rep_policy_version→max_rep_estimation_policy_versions · user_exercise_set_score_states.exercise_id→exercises(cascade) · user_exercise_stats.exercise_id→exercises(cascade) · user_exercise_stats_refresh_jobs.event_type→stats_refresh_event_types · user_exercise_strength_daily.curve_id+curve_version→strength_estimation_curve_versions · user_exercise_strength_daily.exercise_id→exercises(cascade) · user_exercise_strength_daily.policy_id+policy_version→strength_estimation_policy_versions · user_exercise_strength_observations.policy_id+policy_version→strength_estimation_policy_versions · user_exercise_strength_observations.curve_id+curve_version→strength_estimation_curve_versions · user_exercise_strength_observations.exercise_id→exercises(cascade) · user_exercise_strength_states.curve_id+curve_version→strength_estimation_curve_versions · user_exercise_strength_states.exercise_id→exercises(cascade) · user_exercise_strength_states.policy_id+policy_version→strength_estimation_policy_versions · user_manual_pr_records.exercise_id→exercises · user_manual_record_metrics.exercise_id→exercises · user_pr_exercise_summary_snapshots.exercise_id→exercises(cascade) · user_suspensions.lifted_by_user_id→profiles · user_suspensions.report_id→content_reports · user_suspensions.suspended_by_user_id→profiles
  • RLS 꺼짐 2: lift_guild_admins, stats_refresh_event_types · RLS 켜졌지만 정책 0 (문 함수로만 접근) 24: client_error_events, content_takedowns, exercise_business_role_memberships, exercise_catalog_changes, exercise_strength_standards, group_chat_messages, group_notices, groups, moderation_banned_terms, pr_overview_projection_control, strength_profession_standards, user_calendar_summary_dirty_dates, user_exercise_stats_refresh_jobs, user_fact_history, user_notifications, user_pr_exercise_summary_snapshots, user_pr_overview_rollover_state, user_pr_overview_snapshot_headers, user_stats_projection_runs, user_suspensions, wodup_session_timing_materializations, workout_draft_checkpoint_fences, workout_draft_checkpoints, workout_mutation_receipts
  • 기본키 없음 0: 없음 · owner 열 없음(전역·참조·자식 표) 23: content_takedowns, exercise_archetypes, exercise_business_role_memberships, exercise_catalog_changes, exercise_external_mappings, exercise_strength_standards, exercise_synonyms, exercise_usage_stats, external_exercise_interpretation_components, external_exercise_interpretations, group_invites, max_rep_estimation_policy_versions, moderation_banned_terms, pr_overview_projection_control, set_purpose_policy_versions, stats_refresh_event_types, strength_estimation_curve_points, strength_estimation_curve_versions, strength_estimation_integer_lookup_snapshots, strength_estimation_policy_versions, strength_profession_standards, user_pr_exercise_summary_snapshots, wodup_exercises
  • 시간대 없는 시각/시간 열 1: session.scheduled_time(time without time zone) · jsonb 열 32: body_metrics.raw_payload[system], exercise_set_part.raw_payload[system], exercise_strength_standards.source_metadata, exercise_strength_standards.cutlines, exercises.target_muscles[fact], exercises.required_inputs[projection], max_rep_estimation_policy_versions.policy, profiles.onboarding_draft[fact], session.raw_payload[system], session_exercise_part.raw_payload[system], set_purpose_policy_versions.policy, strength_estimation_policy_versions.policy, strength_profession_standards.cutlines, user_exercise_period_stats.report_distributions_v1, user_exercise_stats_refresh_jobs.metadata, user_fact_history.old_row, user_notifications.payload, user_stats_projection_runs.payload, wodup_exercises.raw_payload, wodup_import_batches.row_kinds, wodup_import_batches.preflight, wodup_import_batches.raw_inspection, wodup_import_batches.normalization_summary, wodup_import_batches.progress, wodup_import_exercises.normalized, wodup_import_rows.original, wodup_import_rows.normalized, wodup_import_rows.warnings, wodup_import_sessions.original, wodup_import_sessions.normalized, wodup_import_sets.normalized, workout_draft_checkpoints.payload[fact]
  • auth.users 참조인데 cascade 가 아닌 FK 2: exercise_external_mappings.movement_external_mappings_reviewed_by_fkey(SET NULL), external_exercise_interpretations.external_exercise_interpretations_reviewed_by_fkey(SET NULL)
  • 커밋 시점에 검사되는 FK(DEFERRABLE INITIALLY DEFERRED — 문장 안에서는 위반이 드러나지 않는다) 11: exercise_external_mappings.exercise_id→exercises, exercises.owner_user_id→auth.users, session_exercise_part.exercise_id→exercises, user_exercise_favorites.exercise_id→exercises, user_exercise_pr_states.current_event_id→user_exercise_pr_events, user_exercise_records.source_set_id→exercise_set_part, user_exercise_session_rollups.best_estimated_1rm_set_id→exercise_set_part, user_exercise_stats.best_estimated_1rm_set_id→exercise_set_part, user_manual_pr_records.exercise_id→exercises, user_manual_record_metrics.exercise_id→exercises, user_pr_exercise_summary_snapshots.user_id+generation+as_of_date→user_pr_overview_snapshot_headers

범례 — 작성자 칸: 함수(i/u/d/t) = insert/update/delete/truncate, ;dynamic = stats_projection_tables_v1의 닫힌 출력 목록을 통해 실행하는 동적 SQL, ▲함수 = 트리거 함수. 동적 작성자 등재는 실제 DB 실행·원자적 발행 검증을 대신하지 않는다. 읽는 곳: 함수·뷰 이름·앱 파일(Supabase 클라이언트가 표를 직접 읽는 곳). 등급: fact = 유저 원본(열 등급은 user-fact-columns.json), projection = 통계 투영(stats-projection-writers.json), reference = 마이그레이션이 시드하는 참조 데이터, system = 운영·플랫폼·staging. 상태: 나열 = 장부에 올렸다, 검증 = 증거(pgTAP·실행계획·PR)가 있다.

workout — 운동 기록(세션 계층·완료 저장·수정·삭제·영수증)

mermaid
erDiagram
  daily_conditions {
    uuid id PK
    uuid user_id FK
    date date
    text condition
    text note
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  exercise_set {
    uuid id PK
    uuid session_exercise_id FK
    integer position
    text set_type
    timestamp_with_time_zone created_at
  }
  exercise_set_part {
    uuid id PK
    uuid session_exercise_part_id FK
    integer position
    integer reps
    numeric82 load
    text note
    integer rest_seconds
    timestamp_with_time_zone created_at
    text set_type
    boolean free_rest
    text source
    text source_ref
    timestamp_with_time_zone source_updated_at
    timestamp_with_time_zone imported_at
    text _more_ "열 19개 더"
  }
  session {
    uuid id PK
    uuid user_id FK
    date date
    text condition
    text note
    integer kudos
    timestamp_with_time_zone created_at
    text status
    text title
    text pre_note
    text post_condition
    text start_time
    text end_time
    text duration_label
    text _more_ "열 15개 더"
  }
  session_exercise {
    uuid id PK
    uuid session_id FK
    integer position
    text name
    timestamp_with_time_zone created_at
    text import_group_key
  }
  session_exercise_part {
    uuid id PK
    uuid session_id FK
    uuid exercise_id FK
    integer position
    text note
    timestamp_with_time_zone created_at
    text source
    text source_ref
    text provider_exercise_id
    timestamp_with_time_zone source_updated_at
    timestamp_with_time_zone imported_at
    jsonb raw_payload
    text entry_kind
    text entry_title
    text _more_ "열 6개 더"
  }
  workout_mutation_receipts {
    uuid user_id PK
    uuid client_mutation_id PK
    text mutation_kind
    uuid session_id
    text source
    text source_ref
    text request_hash
    text client_request_hash
    bigint server_revision
    timestamp_with_time_zone session_updated_at
    bigint stats_requested_version
    timestamp_with_time_zone committed_at
    timestamp_with_time_zone source_tombstone_superseded_at
    uuid source_tombstone_superseded_by_batch_id
  }
  auth_users ||--o{ daily_conditions : "user_id"
  session_exercise ||--o{ exercise_set : "session_exercise_id cascade"
  session_exercise_part ||--o{ exercise_set_part : "session_exercise_part_id cascade"
  exercise_set ||--o{ exercise_set_part : "exercise_set_id cascade"
  exercises ||--o{ exercise_set_part : "load_percent_base_exercise_id (catalog)"
  groups ||--o{ session : "group_id (group)"
  auth_users ||--o{ session : "user_id"
  session ||--o{ session_exercise : "session_id cascade"
  exercises ||--o{ session_exercise_part : "exercise_id (catalog)"
  session_exercise ||--o{ session_exercise_part : "session_exercise_id cascade"
  session ||--o{ session_exercise_part : "session_id cascade"
  exercise_synonyms ||--o{ session_exercise_part : "synonym_id (catalog)"
  auth_users ||--o{ workout_mutation_receipts : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
completed_session_v1 (뷰)-0--invoker-없음build_home_dashboard_core_v2, calendar_day_summary_source, enqueue_stale_user_exercise_stats_refresh_jobs, get_admin_health_report, get_calendar_day_summary, get_calendar_month_summary, get_exercise_pr_history_v3_core, get_following_calendar_month_v1, get_group_board_day_sessions_v1, get_log_table_cell_detail_v1_core, get_profile_feed_page_engine_v1, get_session_detail, get_session_presentation_mains_json, get_session_presentation_mains_json_v2_engine, get_session_search_v2_engine, group_board_completion_members_v1, record_metric_measured_sets_v1, refresh_user_calendar_day_summaries, refresh_user_calendar_day_summaries_v1_core, refresh_user_exercise_best_reps_from, refresh_user_exercise_main_reps_from, refresh_user_exercise_stats_from_base, refresh_user_max_rep_projection, refresh_user_measured_pr_projection, refresh_user_session_timing_stats_from, refresh_user_strength_estimation_projection_from_v1, refresh_user_training_period_stats_from, set_score_observations_v1, set_score_reference_v2, user_notification_visible_v1, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity뷰(보존 없음) — session status=completed 투영-유지 근거검증
daily_conditionsfactuser_id71 / 1 / 1 / 10on·42▲user_fact_block_truncate_v1, ▲protect_daily_conditions_facts_v1build_home_dashboard_core_v2, get_calendar_day_summary, get_calendar_month_summary영구(유저 원본)계정 삭제 cascade. 작성 함수 없음(#1215 뒤 session.condition 이 정본) — 잔존 표유지 근거검증
exercise_setfactsession_exercise_id51 / 0 / 1 / 21on·12attach_exercise_set_layer_v1(i), write_session_children_v5(diu), ▲user_fact_block_truncate_v1, ▲protect_exercise_set_facts_v1get_planned_session_detail_v1_engine, get_session_mutation_receipt_v2, group_board_json_v2, protect_exercise_set_part_facts_v1, strength_set_metrics_map_json, validate_session_hierarchy_v1영구(유저 원본, 세트 층)유저 본인 저장(write_session_children_v5 가 보내지 않은 행 삭제)·세션 삭제 cascade·계정 삭제 cascade유지 근거검증
exercise_set_partfactsession_exercise_part_id331 / 0 / 3 / 174on·113apply_completed_atomic_values_v1(u), import_motra_workouts_v1(i), import_wodup_batch_to_canonical_engine(di), refresh_session_effective_loads(u), write_session_children_v5(diu), ▲mark_calendar_summary_dirty_date, ▲capture_exercise_set_load_lb, ▲clamp_exercise_set_rest_seconds, ▲clear_rollup_max_rep_on_source_set_delete_v1, ▲enforce_completed_exercise_set_limits, ▲normalize_exercise_set_load_unit, ▲normalize_exercise_set_outcome_v1, ▲populate_wodup_atomic_set_values_v1, ▲project_exercise_set_part_values_v1, ▲touch_completed_session_revision_from_child_v1, ▲user_fact_block_truncate_v1, ▲protect_exercise_set_part_facts_v1, ▲attach_exercise_set_layer_v1build_session_detail_payload_v1, calendar_day_summary_source, decorate_calendar_day_atomic_sets_v1, enforce_completed_exercise_set_limits, enforce_completed_session_exercise_limit, get_admin_health_report, get_calendar_day_summary, get_exercise_pr_history_v3_core, get_group_board_day_sessions_v1, get_log_table_cell_detail_v1_core, get_planned_session_detail_v1_engine, get_session_mutation_receipt_v2, get_session_presentation_mains_json, get_session_presentation_mains_json_v2_engine, group_board_json_v2, planned_session_card_stats_v1, record_metric_measured_sets_v1, refresh_user_exercise_best_reps_from, refresh_user_exercise_main_reps_from, refresh_user_exercise_stats_from_base, refresh_user_max_rep_projection, refresh_user_measured_pr_projection, refresh_user_strength_estimation_projection_from_v1, session_set_totals_v1, set_score_observations_v1, set_score_reference_v2, set_score_session_v2, strength_display_frame_kg_v1, strength_set_metrics_map_json, validate_session_hierarchy_v1, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity, workout_set_score_preview_v1, 앱 barbelicRepository.ts영구(유저 원본, 세부 세트 층)유저 본인 저장·세션 삭제 cascade·계정 삭제 cascade. 투영 표 5개가 이 표를 FK 로 가리킴(참조 열 인덱스 없음 → 수정 예정)유지 근거검증
sessionfactuser_id291 / 0 / 2 / 107on·111backfill_wodup_bodyweight_snapshot_v1(u), backfill_wodup_session_timing_v1(u), import_motra_workouts_v1(i), import_wodup_batch_to_canonical_engine(i), touch_completed_session_revision_from_child_v1(u), ▲mark_calendar_summary_dirty_date, ▲enforce_session_exercise_owner_on_user_change, ▲enforce_session_identity_row, ▲capture_user_notification_v1, ▲derive_session_origin_v1, ▲enforce_workout_source_tombstone_v1, ▲refresh_session_effective_loads, ▲stamp_completed_session_revision_v1, ▲user_fact_block_truncate_v1, ▲protect_session_facts_v1, ▲normalize_brid_label_row_v1admin_delete_exercise_synonym_v1, build_session_detail_payload_v1, capture_user_notification_v1, decorate_calendar_day_atomic_sets_v1, decorate_calendar_sessions_v1, delete_group_board_comment_v1, delete_session_comment_v1, enforce_exercise_fact_identity, get_admin_content_reports_v1, get_group_board_v1, get_group_member_session_v1, get_planned_session_detail_v1_engine, get_session_mutation_receipt_v2, get_workout_draft_checkpoint_v2, group_board_json_v2, group_board_session_id_v1, list_my_groups_v1, mark_calendar_summary_dirty_date, planned_session_visible_v2, project_exercise_set_part_values_v1, protect_exercise_set_facts_v1, protect_exercise_set_part_facts_v1, protect_session_comments_facts_v1, protect_session_exercise_facts_v1, protect_session_exercise_part_facts_v1, protect_session_likes_facts_v1, report_content_v1, save_session_v5_engine, session_readable_v1, session_social_target_owner_v1, set_score_reference_v2, set_score_session_v2, strength_display_frame_kg_v1, strength_set_metrics_map_json, validate_completed_workout_payload_v4, validate_plan_payload_v3, 뷰 completed_session_v1, 뷰 planned_session_v1, 앱 barbelicRepository.ts영구(유저 원본)유저 본인(delete_session_v5)·계정 삭제 cascade·그룹 해산 cascade(그룹 계획). 접두 포함 인덱스 2개 정리 대상유지 근거검증
session_exercisefactsession_id61 / 0 / 1 / 22on·12attach_session_exercise_layer_v1(i), write_session_children_v5(diu), ▲user_fact_block_truncate_v1, ▲protect_session_exercise_facts_v1build_session_detail_payload_v1, get_calendar_day_summary, get_planned_session_detail_v1_engine, get_session_mutation_receipt_v2, get_session_presentation_mains_json_v2_engine, group_board_json_v2, protect_exercise_set_facts_v1, protect_session_exercise_part_facts_v1, strength_set_metrics_map_json, validate_session_hierarchy_v1영구(유저 원본, 종목 층)유저 본인 저장·세션 삭제 cascade유지 근거검증
session_exercise_partfactsession_id201 / 0 / 4 / 94on·112backfill_wodup_bodyweight_snapshot_v1(u), import_motra_workouts_v1(i), import_wodup_batch_to_canonical_engine(di), write_session_children_v5(diu), ▲mark_calendar_summary_dirty_date, ▲enforce_exercise_fact_identity, ▲rebase_wodup_training_complex_position_v2, ▲apply_wodup_training_complex_meta_v2, ▲enforce_completed_session_exercise_limit, ▲normalize_session_exercise_recording_fields_v1, ▲refresh_session_effective_loads, ▲snapshot_session_exercise_load_multiplier_v1, ▲touch_completed_session_revision_from_child_v1, ▲user_fact_block_truncate_v1, ▲protect_session_exercise_part_facts_v1, ▲attach_session_exercise_layer_v1admin_delete_exercise_synonym_v1, apply_completed_atomic_values_v1, attach_exercise_set_layer_v1, build_session_detail_payload_v1, calendar_day_summary_source, decorate_calendar_day_atomic_sets_v1, enforce_completed_exercise_set_limits, enforce_completed_session_exercise_limit, enforce_session_exercise_owner_on_user_change, enqueue_stale_user_exercise_stats_refresh_jobs, exclude_training_complex_strength_observation_v1, get_admin_health_report, get_calendar_day_summary, get_exercise_pr_history_v3_core, get_group_board_day_sessions_v1, get_log_table_cell_detail_v1_core, get_planned_session_detail_v1_engine, get_profile_feed_page_engine_v1, get_session_mutation_receipt_v2, get_session_presentation_mains_json, get_session_presentation_mains_json_v2_engine, group_board_json_v2, mark_calendar_summary_dirty_date, planned_session_card_stats_v1, populate_wodup_atomic_set_values_v1, project_exercise_set_part_values_v1, protect_exercise_set_part_facts_v1, record_metric_measured_sets_v1, refresh_session_effective_loads, refresh_user_exercise_best_reps_from, refresh_user_exercise_main_reps_from, refresh_user_exercise_stats_from_base, refresh_user_max_rep_projection, refresh_user_measured_pr_projection, refresh_user_strength_estimation_projection_from_v1, session_set_totals_v1, set_score_observations_v1, set_score_reference_v2, set_score_session_v2, strength_display_frame_kg_v1, strength_set_metrics_map_json, touch_completed_session_revision_from_child_v1, validate_completed_workout_payload_v4, validate_plan_payload_v3, validate_session_hierarchy_v1, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity, 앱 barbelicRepository.ts영구(유저 원본, 세부 종목 층)유저 본인 저장·세션 삭제 cascade. 투영 표 4개가 FK 로 가리킴(참조 열 인덱스 없음)유지 근거검증
workout_mutation_receiptssystemuser_id141 / 0 / 1 / 74on·00import_wodup_batch_to_canonical_engine(u), save_session_v5_engine(i)backfill_wodup_session_timing_v1, enforce_workout_source_tombstone_v1, get_session_mutation_receipt_v2, get_workout_recovery_receipt_v1정책 없음 — 멱등 영수증이 영구 누적(Production 1,872건, 30일 초과 513건)삭제 경로 없음·계정 삭제 cascade인계(D02(영수증 세대) — 영수증 보존 기간(재전송 창)과 purge)검증

plan — 계획 세션

mermaid
erDiagram
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
planned_session_v1 (뷰)-0--invoker-없음build_home_dashboard_core_v2, decorate_calendar_planned_sessions_v1, get_calendar_day_summary, get_calendar_month_summary, get_calendar_range_summary, get_following_calendar_month_v1뷰(보존 없음) — session status=planned 투영-유지 근거검증

calendar — 달력·하루 상세·기록표·즐겨찾기

mermaid
erDiagram
  user_calendar_day_summaries {
    uuid user_id PK
    date date PK
    integer completed_session_count
    integer exercise_count
    integer set_count
    integer total_reps
    integer main_reps
    numeric142 volume
    integer duration_minutes
    integer pr_count
    timestamp_with_time_zone source_updated_at
    timestamp_with_time_zone refreshed_at
    numeric82 average_set_intensity_percent
    integer intensity_set_count
    text _more_ "열 22개 더"
  }
  user_calendar_summary_dirty_dates {
    uuid user_id PK
    date date PK
    timestamp_with_time_zone marked_at
  }
  user_exercise_favorite_states {
    uuid user_id PK
    bigint revision
    timestamp_with_time_zone updated_at
  }
  user_exercise_favorites {
    uuid user_id PK
    uuid exercise_id PK
    smallint sort_order
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  auth_users ||--o{ user_calendar_day_summaries : "user_id"
  auth_users ||--o{ user_calendar_summary_dirty_dates : "user_id"
  auth_users ||--o{ user_exercise_favorite_states : "user_id"
  exercises ||--o{ user_exercise_favorites : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_favorites : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
user_calendar_day_summariesprojectionuser_id361 / 0 / 1 / 100on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_calendar_day_summaries_v1_core(di), refresh_user_calendar_day_summaries(u)build_home_dashboard_core_v2, calendar_strength_day_metrics_map_v1, get_calendar_day_summary, get_calendar_month_summary, get_calendar_range_summary, get_following_calendar_month_v1, get_home_year_activity, validate_user_exercise_stats_integrity_engine_v1재계산 가능(투영) — refresh 가 날짜 단위로 지우고 다시 씀refresh 함수·계정 삭제 cascade. 중복 인덱스 1쌍 정리 대상유지 근거검증
user_calendar_summary_dirty_datesprojectionuser_id31 / 0 / 1 / 00on·00mark_calendar_summary_dirty_date(i), publish_stats_projection_job_v1(di;dynamic), refresh_user_calendar_day_summaries_v1_core(d), refresh_user_measured_pr_projection(i)backfill_wodup_session_timing_v1, refresh_user_calendar_day_summaries, refresh_user_exercise_stats_from처리 때까지(더티 큐) — refresh 가 지움refresh 함수·계정 삭제 cascade유지 근거검증
user_exercise_favorite_statessystemuser_id31 / 0 / 1 / 10on·10replace_user_exercise_favorites(iu), seed_new_user_default_exercise_favorites(i), set_user_exercise_favorite(iu)build_user_exercise_favorites_payload영구(즐겨찾기 revision 상태)계정 삭제 cascade유지 근거검증
user_exercise_favoritesfactuser_id51 / 1 / 2 / 10on·13replace_user_exercise_favorites(di), seed_new_user_default_exercise_favorites(i), set_user_exercise_favorite(diu), ▲enforce_exercise_fact_identity, ▲user_fact_block_truncate_v1, ▲protect_user_exercise_favorites_facts_v1build_user_exercise_favorites_payload, report_favorite_growth_v1영구(유저 원본)본인 교체(replace/set)·계정 삭제 cascade유지 근거검증

pr — PR·1RM·수동 기록 지표·근력 기준·PR 스냅샷

mermaid
erDiagram
  exercise_strength_standards {
    bigint id PK
    text provider
    text source_slug
    text exercise_name
    text gender
    text metric
    text standard_scope
    text unit
    numeric beginner
    numeric novice
    numeric intermediate
    numeric advanced
    numeric elite
    timestamp_with_time_zone source_scraped_at
    text _more_ "열 6개 더"
  }
  pr_overview_projection_control {
    boolean singleton PK
    timestamp_with_time_zone updated_at
    bigint catalog_changes_pruned_seq
  }
  strength_profession_standards {
    text profession_key PK
    text benchmark_id
    text gender PK
    text metric
    text unit
    uuid member_exercise_ids
    jsonb cutlines
    timestamp_with_time_zone generated_at
    text generator_version
  }
  user_exercise_pr_events {
    uuid id PK
    uuid user_id FK
    uuid exercise_id FK
    smallint target_reps
    numeric82 value
    numeric82 previous_value
    numeric82 delta
    date achieved_on
    boolean is_baseline
    text source_kind
    uuid source_id
    uuid session_id
    uuid session_exercise_id
    numeric82 source_load
    text _more_ "열 5개 더"
  }
  user_exercise_pr_states {
    uuid user_id PK
    uuid exercise_id PK
    smallint target_reps PK
    numeric82 current_value
    numeric82 previous_value
    numeric82 delta
    date achieved_on
    uuid current_event_id FK
    text source_kind
    uuid source_id
    boolean is_baseline
    bigint applied_version
    timestamp_with_time_zone updated_at
  }
  user_exercise_records {
    uuid id PK
    uuid user_id FK
    uuid exercise_id FK
    uuid session_id FK
    uuid session_exercise_id FK
    text record_type
    numeric82 value
    text unit
    numeric82 load
    integer reps
    numeric82 estimated_1rm
    numeric122 volume
    integer set_count
    date recorded_at
    text _more_ "열 5개 더"
  }
  user_manual_pr_records {
    uuid id PK
    uuid user_id FK
    uuid exercise_id FK
    numeric82 value
    text unit
    date recorded_at
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  user_manual_record_metrics {
    uuid id PK
    uuid user_id FK
    uuid exercise_id FK
    text metric_kind
    numeric122 value
    numeric102 seconds
    date recorded_at
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  user_pr_exercise_summary_snapshots {
    uuid user_id PK
    bigint generation PK
    date as_of_date PK
    uuid exercise_id PK
    integer summary_rank
    boolean user_relevant
    boolean fixed_benchmark
    timestamp_with_time_zone relevant_at
    integer session_count
    integer set_count
    numeric122 total_volume
    integer best_reps
    numeric82 best_load
    integer best_load_reps
    text _more_ "열 38개 더"
  }
  user_pr_overview_rollover_state {
    uuid user_id PK
    bigint snapshot_generation
    bigint applied_catalog_version
    timestamp_with_time_zone next_refresh_at
    integer attempt_count
    timestamp_with_time_zone last_attempt_at
    timestamp_with_time_zone last_success_at
    text last_error
    timestamp_with_time_zone updated_at
  }
  user_pr_overview_snapshot_headers {
    uuid user_id PK
    bigint generation PK
    date as_of_date PK
    integer total_exercise_count
    integer trained_exercise_count
    integer summary_count
    boolean summaries_truncated
    bigint catalog_version
    uuid source_job_id
    timestamp_with_time_zone refreshed_at
  }
  exercises ||--o{ exercise_strength_standards : "exercise_id (catalog)"
  exercises ||--o{ user_exercise_pr_events : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_pr_events : "user_id"
  user_exercise_pr_events ||--o{ user_exercise_pr_states : "current_event_id cascade"
  exercises ||--o{ user_exercise_pr_states : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_pr_states : "user_id"
  exercises ||--o{ user_exercise_records : "exercise_id (catalog)"
  session_exercise_part ||--o{ user_exercise_records : "session_exercise_id (workout)"
  session ||--o{ user_exercise_records : "session_id (workout)"
  exercise_set_part ||--o{ user_exercise_records : "source_set_id (workout)"
  auth_users ||--o{ user_exercise_records : "user_id"
  exercises ||--o{ user_manual_pr_records : "exercise_id (catalog)"
  auth_users ||--o{ user_manual_pr_records : "user_id"
  exercises ||--o{ user_manual_record_metrics : "exercise_id (catalog)"
  auth_users ||--o{ user_manual_record_metrics : "user_id"
  user_pr_overview_snapshot_headers ||--o{ user_pr_exercise_summary_snapshots : "user_id,generation,as_of_date cascade"
  exercises ||--o{ user_pr_exercise_summary_snapshots : "exercise_id (catalog)"
  auth_users ||--o{ user_pr_overview_rollover_state : "user_id"
  auth_users ||--o{ user_pr_overview_snapshot_headers : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
exercise_strength_standardsreference없음(전역·참조)201 / 1 / 1 / 51on·01▲exercise_strength_standards_cutlines_guard_v1get_pr_overview_engine_v1참조 데이터(근력 기준표, 마이그레이션 시드)마이그레이션만(exercises RESTRICT)유지 근거검증
pr_overview_projection_controlreference없음(전역·참조)31 / 0 / 0 / 10on·00prune_exercise_catalog_changes_v1(u)get_exercise_catalog_changes참조 데이터(PR 스냅샷 제어 1행)마이그레이션만유지 근거검증
strength_profession_standardsreference없음(전역·참조)91 / 0 / 0 / 30on·00없음get_pr_overview_engine_v1참조 데이터(직군 기준)마이그레이션만유지 근거검증
user_exercise_pr_eventsprojectionuser_id191 / 1 / 2 / 114on·11publish_stats_projection_job_v1(di;dynamic), refresh_user_measured_pr_projection(di), ▲capture_user_notification_v1build_exercise_pr_detail_core_v3, build_home_dashboard_core_v2, build_user_pr_exercise_summary_rows, calendar_day_summary_source, get_exercise_pr_history_v3_core, get_exercise_pr_records, get_pr_overview_engine_v1, get_session_presentation_mains_json_v2_engine, get_user_exercise_nrm_states_json, get_user_recent_prs_json, get_volume_overview_v3_core, refresh_user_calendar_day_summaries, refresh_user_pr_overview_snapshot_window, report_favorite_growth_v1, strength_set_metrics_map_json, user_notification_visible_v1, user_pr_event_count_v1, validate_user_exercise_stats_integrity_engine_v1, 앱 barbelicRepository.ts재계산 가능(투영)refresh_user_measured_pr_projection(전량)·계정 삭제 cascade유지 근거검증
user_exercise_pr_statesprojectionuser_id131 / 1 / 3 / 80on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_measured_pr_projection(di)build_exercise_pr_detail_core_v3, get_user_exercise_nrm_states_json, validate_user_exercise_stats_integrity_engine_v1재계산 가능(투영)refresh·계정 삭제 cascade. 기본키와 같은 열의 중복 인덱스 정리 대상유지 근거검증
user_exercise_recordsprojectionuser_id191 / 0 / 5 / 05on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_exercise_stats_from_base(di)validate_user_exercise_stats_integrity_engine_v1, 앱 barbelicRepository.ts재계산 가능(투영) — 화면이 읽지 않는 중간 표(D01)refresh(전량)·원본 삭제 SET NULL·계정 삭제 cascade. session_exercise_id·session_id·source_set_id 참조 열 인덱스 없음(Production seq scan 3.4만 회)유지 근거검증
user_manual_pr_recordsfactuser_id81 / 0 / 2 / 22on·14complete_onboarding_v2(i), delete_manual_pr(d), save_manual_pr(iu), ▲enforce_exercise_fact_identity, ▲enqueue_manual_pr_stats_refresh, ▲user_fact_block_truncate_v1, ▲protect_user_manual_pr_records_facts_v1build_user_exercise_stats_json, get_user_manual_records_v1, get_volume_overview_v1_core, refresh_user_measured_pr_projection, set_score_observations_v1, set_score_reference_v2, validate_user_exercise_stats_integrity_engine_v1영구(유저 원본)본인(delete_manual_pr)·계정 삭제 cascade유지 근거검증
user_manual_record_metricsfactuser_id91 / 0 / 2 / 21on·14delete_manual_record_metric_v1(d), save_manual_record_metric_v1(iu), ▲enforce_exercise_fact_identity, ▲enqueue_manual_max_rep_score_refresh_v2, ▲user_fact_block_truncate_v1, ▲protect_user_manual_record_metrics_facts_v1get_user_manual_records_v1, record_metric_manual_rows_v1, set_score_observations_v1, set_score_reference_v2영구(유저 원본)본인(delete_manual_record_metric_v1)·계정 삭제 cascade유지 근거검증
user_pr_exercise_summary_snapshotsprojection없음(전역·참조)521 / 1 / 2 / 140on·00publish_stats_projection_job_v1(di;dynamic), refresh_user_pr_overview_snapshot_window(iu)get_user_home_benchmark_prs_json, get_user_pr_exercise_summaries_snapshot_json, guard_user_stats_refresh_projection_publication, run_user_pr_overview_snapshot_rollover세대 보존 — prune_user_pr_overview_snapshot_generations 가 옛 세대 헤더를 지우면 cascade헤더 삭제 cascade. 고유 키와 같은 열의 중복 인덱스 정리 대상유지 근거검증
user_pr_overview_rollover_stateprojectionuser_id91 / 0 / 1 / 31on·00initialize_user_pr_overview_snapshot(iu), publish_stats_projection_job_v1(di;dynamic), refresh_user_pr_overview_snapshot_window(i), run_user_pr_overview_snapshot_rollover(iu)없음사용자당 1행(상태)계정 삭제 cascade. 읽는 함수 없음(등록만 하고 조회하지 않음 — 확인 필요)유지 근거검증
user_pr_overview_snapshot_headersprojectionuser_id101 / 0 / 1 / 80on·00prune_user_pr_overview_snapshot_generations(d), publish_stats_projection_job_v1(di;dynamic), refresh_user_pr_overview_snapshot_window(di)get_user_pr_exercise_summaries_json, get_user_pr_exercise_summaries_snapshot_json, guard_user_stats_refresh_projection_publication, run_user_pr_overview_snapshot_rollover세대 보존 — prune 함수가 옛 세대를 지움prune 함수·계정 삭제 cascade유지 근거검증

volume — 볼륨·리포트

mermaid
erDiagram
  user_exercise_period_stats {
    uuid user_id PK
    uuid exercise_id PK
    text period_type PK
    date period_start PK
    integer session_count
    integer set_count
    numeric122 volume
    numeric82 best_load
    numeric82 best_estimated_1rm
    timestamp_with_time_zone updated_at
    integer main_reps
    numeric82 average_set_intensity_percent
    integer intensity_set_count
    integer strength_set_count
    text _more_ "열 22개 더"
  }
  user_training_period_stats {
    uuid user_id PK
    text period_type PK
    date period_start PK
    integer session_count
    integer day_count
    integer set_count
    numeric122 volume
    integer duration_minutes
    timestamp_with_time_zone updated_at
    integer main_reps
    integer duration_session_counts
    integer time_slot_counts
    boolean attendance_met
    integer attendance_week_count
    text _more_ "열 1개 더"
  }
  exercises ||--o{ user_exercise_period_stats : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_period_stats : "user_id"
  auth_users ||--o{ user_training_period_stats : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
user_exercise_period_statsprojectionuser_id361 / 0 / 2 / 71on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_exercise_main_reps_from(di), refresh_user_exercise_stats_from_base(di), refresh_user_report_distributions_v1(u), refresh_user_set_purpose_projection(u), refresh_user_set_score_period_stats_v1(u), refresh_user_strength_estimation_projection_from_v1(u)build_exercise_pr_detail_core_v3, build_home_dashboard_core_v2, build_user_pr_exercise_summary_rows, get_exercise_pr_detail, get_exercise_pr_detail_year, get_exercise_pr_detail_year, get_log_table_month, get_log_table_month_v2_core, get_report_design_v1, get_volume_overview_annual_v3, get_volume_overview_v1_core, get_volume_overview_v2_core, get_volume_overview_v3_core, validate_user_exercise_stats_integrity_engine_v1, 앱 barbelicRepository.ts재계산 가능(투영) — 매 갱신 전량 재작성(Production 삽입 685만·삭제 682만)refresh·계정 삭제 cascade. 기본키와 같은 열의 중복 인덱스 정리 대상유지 근거검증
user_training_period_statsprojectionuser_id151 / 0 / 1 / 101on·10apply_training_attendance_policy_v1(u), publish_stats_projection_job_v1(di;dynamic), refresh_user_session_timing_stats_from(u), refresh_user_training_main_reps_from(u), refresh_user_training_period_stats_from(di)build_home_dashboard_core_v2, enqueue_stale_user_exercise_stats_refresh_jobs, get_home_dashboard_engine_v1, get_report_design_v1, get_volume_overview_engine_v1, get_volume_overview_v1_core, get_volume_overview_v3_core재계산 가능(투영)refresh·계정 삭제 cascade. 기본키와 같은 열의 중복 인덱스 정리 대상(덮는 인덱스 1개는 유지)유지 근거검증

catalog — 종목 카탈로그·검색·커스텀 종목·별칭·기록 필드

mermaid
erDiagram
  exercise_archetypes {
    text id PK
    text name_ko
    text name_en
    text primary_part
    text note
    integer sort_order
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
    text pattern
    boolean is_active
    uuid rep_exercise_id FK
  }
  exercise_business_role_memberships {
    text role_key PK
    text member_key PK
    uuid exercise_id FK
    integer sort_order
    timestamp_with_time_zone created_at
  }
  exercise_catalog_changes {
    bigint seq PK
    uuid exercise_id
    uuid owner_user_id
    text op
    text name_ko
    text name_en
    timestamp_with_time_zone changed_at
  }
  exercise_external_mappings {
    uuid id PK
    text provider
    text provider_exercise_id
    uuid exercise_id FK
    text status
    numeric54 confidence
    text match_method
    text provider_name
    text provider_category
    text provider_aliases
    text provider_fields
    text notes
    uuid reviewed_by FK
    timestamp_with_time_zone reviewed_at
    text _more_ "열 2개 더"
  }
  exercise_synonyms {
    uuid id PK
    uuid exercise_id FK
    text name_ko
    text name_en
    boolean is_default
    text aliases
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  exercise_usage_stats {
    uuid exercise_id PK
    integer user_count
    bigint session_count
    timestamp_with_time_zone refreshed_at
  }
  exercises {
    uuid id PK
    text name_ko
    text name_en
    text exercise_category
    boolean track_pr
    boolean is_active
    integer sort_order
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
    text archetype_id FK
    text details
    text source
    text source_ref
    uuid owner_user_id FK
    text _more_ "열 8개 더"
  }
  exercises ||--o{ exercise_archetypes : "rep_exercise_id set null"
  exercises ||--o{ exercise_business_role_memberships : "exercise_id restrict"
  exercises ||--o{ exercise_external_mappings : "exercise_id no action"
  exercises ||--o{ exercise_external_mappings : "exercise_id cascade"
  auth_users ||--o{ exercise_external_mappings : "reviewed_by"
  exercises ||--o{ exercise_synonyms : "exercise_id cascade"
  exercises ||--o{ exercise_usage_stats : "exercise_id cascade"
  exercise_archetypes ||--o{ exercises : "archetype_id no action"
  auth_users ||--o{ exercises : "owner_user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
exercise_archetypesreference없음(전역·참조)111 / 0 / 1 / 12on·40없음create_catalog_exercise_engine, create_custom_exercise, get_admin_mapping_page, get_volume_overview_v3_core, 앱 adminMappingPageRepository.ts, 앱 barbelicRepository.ts, 앱 catalogRepository.ts참조 데이터(마이그레이션 시드)마이그레이션만유지 근거검증
exercise_business_role_membershipsreference없음(전역·참조)51 / 2 / 1 / 30on·00없음build_user_pr_exercise_summary_rows, get_pr_overview_engine_v1, get_user_home_benchmark_prs_json, get_user_pr_exercise_summaries_snapshot_json, onboarding_state_for_user참조 데이터(마이그레이션 시드)마이그레이션만(exercises RESTRICT)유지 근거검증
exercise_catalog_changesreference없음(전역·참조)71 / 0 / 0 / 12on·00log_exercise_catalog_change_v1(i), log_exercise_synonym_change_v1(i), prune_exercise_catalog_changes_v1(d)exercise_catalog_latest_seq_v1, exercise_ref_json, get_exercise_catalog_changes30일 — cron barbelic-exercise-catalog-changes-prune(매일 03:40)가 prune_exercise_catalog_changes_v1 로 지움(기기 카탈로그 증분 동기화 로그, #1238)prune cron유지 근거검증
exercise_external_mappingsreference없음(전역·참조)161 / 1 / 3 / 12on·41admin_create_catalog_exercise_with_mapping(i), import_motra_workouts_v1(i), import_wodup_batch_to_canonical_engine(i), stage_wodup_import_batch_engine(i), ▲enforce_exercise_fact_identityget_admin_mapping_page, refresh_wodup_complex_interpretations_v1, 앱 barbelicRepository.ts, 앱 catalogRepository.ts영구(외부 종목 ↔ 카탈로그 매핑, 관리자 검토 기록)exercises 삭제 cascade(2중 FK — 하나는 NO ACTION, 하나는 CASCADE: 정리 대상)유지 근거검증
exercise_synonymsreference없음(전역·참조)81 / 0 / 1 / 34on·41admin_delete_exercise_synonym_v1(d), admin_set_default_exercise_synonym_v1(u), create_catalog_exercise_engine(u), import_wodup_batch_to_canonical_engine(u), sync_exercise_default_synonym_v1(i), ▲log_exercise_synonym_change_v1build_session_detail_payload_v1, create_custom_exercise, exercise_catalog_item_json_v1, exercise_ref_json, exercise_synonym_search_text_v1, get_group_board_day_sessions_v1, refresh_wodup_complex_interpretations_v1, validated_entry_synonym_id_v1, 앱 adminMappingPageRepository.ts, 앱 catalogRepository.ts참조 데이터 + 관리자 편집(영구)관리자(admin_delete_exercise_synonym_v1)·exercises 삭제 cascade유지 근거검증
exercise_usage_statsprojection없음(전역·참조)41 / 0 / 1 / 10on·10refresh_exercise_usage_stats_v1(di)get_exercise_search_signals_v1재계산 가능 — cron barbelic-exercise-usage-stats-refresh(매시)가 전량 재작성refresh 함수(delete+insert)·exercises 삭제 cascade유지 근거검증
exercisesfactowner_user_id221 / 0 / 2 / 107on·48admin_delete_performance_detail(u), admin_rename_performance_detail(u), admin_set_default_exercise_synonym_v1(u), create_catalog_exercise_engine(i), create_catalog_exercise(u), create_custom_exercise(i), import_motra_workouts_v1(i), import_wodup_batch_to_canonical_engine(i), set_own_custom_exercise_active(u), ▲enforce_exercise_identity_row, ▲log_exercise_catalog_change_v1, ▲normalize_exercise_recording_fields_v1, ▲exercises_required_inputs_default, ▲sync_exercise_default_synonym_v1, ▲user_fact_block_truncate_v1, ▲protect_exercises_facts_v1, ▲normalize_brid_label_row_v1backfill_wodup_bodyweight_snapshot_v1, build_home_dashboard_core_v2, build_session_detail_payload_v1, build_user_pr_exercise_summary_rows, complete_onboarding_v2, enforce_exercise_fact_identity, enforce_session_exercise_owner_on_user_change, exercise_catalog_item_json_v1, exercise_ref_json, get_admin_health_report, get_admin_mapping_page, get_exercise_catalog, get_exercise_catalog_changes, get_group_board_day_sessions_v1, get_report_design_v1, get_session_presentation_mains_json_v2_engine, get_user_home_benchmark_prs_json, get_user_pr_exercise_summaries_snapshot_json, get_user_recent_prs_json, get_volume_overview_annual_v3, get_volume_overview_v1_core, get_volume_overview_v3_core, group_board_json_v2, group_validated_board_exercises_v1, list_user_notifications_v1, log_exercise_synonym_change_v1, normalize_session_exercise_recording_fields_v1, onboarding_state_for_user, protect_user_exercise_favorites_facts_v1, protect_user_manual_pr_records_facts_v1, protect_user_manual_record_metrics_facts_v1, record_metric_kind_v1, record_metric_manual_rows_v1, record_metric_measured_sets_v1, refresh_exercise_usage_stats_v1, refresh_user_exercise_best_reps_from, refresh_user_exercise_stats_from_base, refresh_user_max_rep_projection, refresh_user_measured_pr_projection, refresh_user_strength_estimation_projection_from_v1, refresh_wodup_complex_interpretations_v1, replace_user_exercise_favorites, report_favorite_growth_v1, save_manual_pr, save_manual_record_metric_v1, seed_new_user_default_exercise_favorites, set_score_observations_v1, set_score_reference_v2, set_score_session_v2, set_user_exercise_favorite, snapshot_session_exercise_load_multiplier_v1, strength_set_metrics_map_json, validate_completed_workout_payload_v4, validate_exercise_identity_scope, validate_plan_payload_v3, validate_user_max_rep_integrity, validated_entry_percent_base_v1, write_session_children_v5, 앱 adminMappingPageRepository.ts, 앱 barbelicRepository.ts, 앱 catalogRepository.ts정식 종목 = 참조 데이터(영구), 커스텀 종목 = 유저 원본(owner_user_id)정식 종목은 마이그레이션·관리자만, 커스텀은 계정 삭제 cascade(앱 삭제 경로는 보관 처리 archived)유지 근거검증

profile — 프로필·신체 데이터

mermaid
erDiagram
  body_metrics {
    uuid id PK
    uuid user_id FK
    date date
    numeric51 weight_kg
    numeric51 skeletal_muscle_kg
    numeric41 body_fat_percent
    text note
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
    numeric51 body_fat_kg
    timestamp_with_time_zone measured_at
    text source
    text source_ref
    uuid import_batch_id FK
    text _more_ "열 1개 더"
  }
  profiles {
    uuid id PK
    text display_name
    text avatar_data_url
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
    text goal
    text avatar_path
    bigint annual_volume_goal_kg
    timestamp_with_time_zone onboarding_completed_at
    integer onboarding_version
    text onboarding_step
    jsonb onboarding_draft
    timestamp_with_time_zone onboarding_updated_at
    text onboarding_completion_source
    text _more_ "열 4개 더"
  }
  inbody_import_batches ||--o{ body_metrics : "import_batch_id (import)"
  auth_users ||--o{ body_metrics : "user_id"
  auth_users ||--o{ profiles : "id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
body_metricsfactuser_id151 / 0 / 2 / 42on·42complete_onboarding_v2(i), import_inbody_body_metrics(i), save_profile_body_metric(i), ▲user_fact_block_truncate_v1, ▲protect_body_metrics_facts_v1body_weight_snapshot_for_date_v1, get_profile_workspace, update_profile_settings_v1영구(유저 원본) — 유저가 지울 때까지계정 삭제 시 auth.users cascade. 앱 삭제 경로 없음(InBody 인입은 import_batch_id SET NULL)유지 근거검증
profilesfactid181 / 0 / 1 / 101on·33apply_onboarding_progress(u), complete_onboarding_v2(u), complete_onboarding_v4(u), ensure_user_profile(i), handle_new_auth_user_profile(i), set_profile_handle_v1(u), set_profile_sex_v1(u), update_profile_settings_v1(u), withdraw_user_consent(u), ▲user_fact_block_truncate_v1, ▲protect_profiles_facts_v1, ▲seed_new_user_default_exercise_favoritesadmin_search_users_v1, admin_set_user_suspension_v1, block_user_v1, check_profile_handle_v1, create_group_v1, follow_user_v1, get_pr_overview_engine_v1, get_profile_feed_page_engine_v1, invite_to_group_v1, list_blocked_users_v1, list_following_v1, onboarding_state_for_user, plan_owner_json_v1, report_content_v1, search_user_profiles_v1, session_comment_author_json_v1, 앱 barbelicRepository.ts, 앱 profileRepository.ts영구(유저 원본)계정 삭제 cascade(auth.users)유지 근거검증

social — 소셜(피드·팔로우·댓글·좋아요·신고·차단)

mermaid
erDiagram
  content_reports {
    uuid id PK
    uuid reporter_user_id FK
    text target_type
    uuid target_session_id FK
    uuid target_comment_id FK
    uuid target_user_id FK
    text reason_code
    text detail
    text status
    text target_key
    timestamp_with_time_zone created_at
    timestamp_with_time_zone resolved_at
    uuid resolved_by_user_id FK
    text resolution_note
    text _more_ "열 1개 더"
  }
  content_takedowns {
    text target_type PK
    uuid target_id PK
    uuid taken_down_by_user_id FK
    timestamp_with_time_zone taken_down_at
    uuid report_id FK
    text note
  }
  moderation_banned_terms {
    text term PK
    text note
    timestamp_with_time_zone created_at
  }
  session_comments {
    uuid id PK
    uuid session_id FK
    uuid user_id FK
    text body
    timestamp_with_time_zone created_at
  }
  session_likes {
    uuid session_id PK
    uuid user_id PK
    timestamp_with_time_zone created_at
  }
  user_blocks {
    uuid blocker_user_id PK
    uuid blocked_user_id PK
    timestamp_with_time_zone created_at
  }
  user_follows {
    uuid follower_user_id PK
    uuid followee_user_id PK
    timestamp_with_time_zone created_at
  }
  user_notifications {
    uuid id PK
    uuid recipient_user_id FK
    uuid actor_user_id FK
    text kind
    text event_key
    uuid target_id
    uuid group_id FK
    uuid session_id FK
    jsonb payload
    timestamp_with_time_zone created_at
    timestamp_with_time_zone recorded_at
    timestamp_with_time_zone read_at
  }
  user_suspensions {
    uuid user_id PK
    timestamp_with_time_zone suspended_at
    uuid suspended_by_user_id FK
    uuid report_id FK
    text reason
    timestamp_with_time_zone lifted_at
    uuid lifted_by_user_id FK
  }
  profiles ||--o{ content_reports : "reporter_user_id (profile)"
  profiles ||--o{ content_reports : "resolved_by_user_id (profile)"
  session_comments ||--o{ content_reports : "target_comment_id set null"
  session ||--o{ content_reports : "target_session_id (workout)"
  profiles ||--o{ content_reports : "target_user_id (profile)"
  content_reports ||--o{ content_takedowns : "report_id set null"
  profiles ||--o{ content_takedowns : "taken_down_by_user_id (profile)"
  session ||--o{ session_comments : "session_id (workout)"
  profiles ||--o{ session_comments : "user_id (profile)"
  session ||--o{ session_likes : "session_id (workout)"
  profiles ||--o{ session_likes : "user_id (profile)"
  profiles ||--o{ user_blocks : "blocked_user_id (profile)"
  profiles ||--o{ user_blocks : "blocker_user_id (profile)"
  profiles ||--o{ user_follows : "followee_user_id (profile)"
  profiles ||--o{ user_follows : "follower_user_id (profile)"
  profiles ||--o{ user_notifications : "actor_user_id (profile)"
  groups ||--o{ user_notifications : "group_id (group)"
  profiles ||--o{ user_notifications : "recipient_user_id (profile)"
  session ||--o{ user_notifications : "session_id (workout)"
  profiles ||--o{ user_suspensions : "lifted_by_user_id (profile)"
  content_reports ||--o{ user_suspensions : "report_id set null"
  profiles ||--o{ user_suspensions : "suspended_by_user_id (profile)"
  profiles ||--o{ user_suspensions : "user_id (profile)"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
content_reportsfactreporter_user_id151 / 1 / 5 / 53on·22admin_resolve_content_report_v1(u), report_content_v1(i), ▲user_fact_block_truncate_v1, ▲protect_content_reports_facts_v1get_admin_content_reports_v1, get_profile_feed_page_engine_v1, group_content_hidden_v1, list_session_comments_v1, session_social_counts_v1, user_notification_visible_v1영구(신고 원본) — 처리 뒤에도 기록 유지신고자·대상 profile 삭제 cascade, 대상 세션 삭제 cascade, 대상 댓글 삭제 SET NULL유지 근거검증
content_takedownssystem없음(전역·참조)61 / 0 / 2 / 10on·00admin_resolve_content_report_v1(i)get_admin_content_reports_v1, get_group_board_day_sessions_v1, get_group_member_session_v1, get_profile_feed_page_engine_v1, get_session_detail, group_board_completion_members_v1, group_content_hidden_v1, list_session_comments_v1, session_social_counts_v1, session_social_target_owner_v1, user_notification_visible_v1영구(운영 기록)관리자만(admin_resolve_content_report_v1 insert) — 삭제 경로 없음유지 근거검증
moderation_banned_termsreference없음(전역·참조)31 / 0 / 0 / 10on·00없음contains_banned_term_v1참조 데이터(금칙어)마이그레이션만유지 근거검증
session_commentsfactuser_id51 / 0 / 2 / 12on·13add_group_board_comment_v1(i), add_session_comment_v1(i), delete_group_board_comment_v1(d), delete_session_comment_v1(d), ▲capture_user_notification_v1, ▲user_fact_block_truncate_v1, ▲protect_session_comments_facts_v1get_admin_content_reports_v1, get_group_board_v1, list_session_comments_v1, report_content_v1, session_social_counts_v1, user_notification_visible_v1영구(유저 원본)작성자 본인·세션 삭제 cascade·profile 삭제 cascade유지 근거검증
session_likesfactuser_id31 / 0 / 2 / 01on·13set_group_board_like_v1(di), set_session_like_v1(di), ▲capture_user_notification_v1, ▲user_fact_block_truncate_v1, ▲protect_session_likes_facts_v1get_group_board_v1, session_social_counts_v1, user_notification_visible_v1영구(유저 원본)본인 취소(set_session_like_v1)·세션 삭제 cascade·profile 삭제 cascade유지 근거검증
user_blocksfactblocker_user_id31 / 0 / 2 / 11on·12block_user_v1(i), unblock_user_v1(d), ▲user_fact_block_truncate_v1, ▲protect_user_blocks_facts_v1follow_user_v1, get_profile_feed_page_engine_v1, group_content_visible_v1, list_blocked_users_v1, list_following_v1, list_session_comments_v1, profile_feed_can_view_v1, report_content_v1, search_user_profiles_v1, session_social_counts_v1영구(유저 원본)본인 해제(unblock_user_v1)·profile 삭제 cascade유지 근거검증
user_followsfactfollower_user_id31 / 0 / 2 / 11on·13block_user_v1(d), follow_user_v1(i), unfollow_user_v1(d), ▲capture_user_notification_v1, ▲user_fact_block_truncate_v1, ▲protect_user_follows_facts_v1capture_user_notification_v1, get_profile_feed_page_engine_v1, list_following_v1, profile_feed_can_view_v1, search_user_profiles_v1, user_notification_visible_v1영구(유저 원본)본인 해제(unfollow_user_v1)·차단 시 block_user_v1·profile 삭제 cascade유지 근거검증
user_notificationssystemactor_user_id121 / 1 / 4 / 34on·00capture_user_notification_v1(i), read_user_notifications_v1(u)list_user_notifications_v1실제 소셜·그룹·PR 이벤트와 수신자별 읽음 기록. 수신자/행위자 계정·그룹·세션이 유지되는 동안 보존수신자·행위자 profile, group, session 삭제 시 FK cascade. 원본 이벤트 취소·차단·탈퇴·게시물 숨김은 읽기 RPC에서 가시성을 다시 판정유지 근거검증
user_suspensionssystemuser_id71 / 0 / 4 / 00on·00admin_resolve_content_report_v1(i), admin_set_user_suspension_v1(iu)create_group_v1, follow_user_v1, get_admin_content_reports_v1, get_my_moderation_state_v1, get_profile_feed_page_engine_v1, group_content_visible_v1, group_image_allowed_v1, group_require_member_v1, list_following_v1, list_session_comments_v1, profile_feed_can_view_v1, report_content_v1, respond_group_invite_v1, search_user_profiles_v1, session_social_counts_v1, session_social_target_owner_v1영구(운영 기록, lifted_at 로 해제 표시)관리자만·profile 삭제 cascade유지 근거검증

group — 그룹·그룹 보드·라운지

mermaid
erDiagram
  group_chat_messages {
    uuid id PK
    uuid group_id FK
    uuid user_id FK
    text body
    timestamp_with_time_zone created_at
  }
  group_invites {
    uuid id PK
    uuid group_id FK
    uuid inviter_user_id FK
    uuid invitee_user_id FK
    timestamp_with_time_zone created_at
  }
  group_members {
    uuid group_id PK
    uuid user_id PK
    text role
    timestamp_with_time_zone created_at
  }
  group_notices {
    uuid id PK
    uuid group_id FK
    uuid author_user_id FK
    text body
    timestamp_with_time_zone created_at
  }
  groups {
    uuid id PK
    text name
    timestamp_with_time_zone created_at
    text photo_path
  }
  groups ||--o{ group_chat_messages : "group_id cascade"
  profiles ||--o{ group_chat_messages : "user_id (profile)"
  groups ||--o{ group_invites : "group_id cascade"
  profiles ||--o{ group_invites : "invitee_user_id (profile)"
  profiles ||--o{ group_invites : "inviter_user_id (profile)"
  groups ||--o{ group_members : "group_id cascade"
  profiles ||--o{ group_members : "user_id (profile)"
  profiles ||--o{ group_notices : "author_user_id (profile)"
  groups ||--o{ group_notices : "group_id cascade"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
group_chat_messagesfactuser_id51 / 0 / 2 / 12on·02add_group_chat_message_v1(i), delete_group_chat_message_v1(d), ▲user_fact_block_truncate_v1, ▲protect_group_chat_messages_facts_v1get_admin_content_reports_v1, get_group_lounge_v1, report_content_v1영구(유저 원본)작성자 본인(delete_group_chat_message_v1)·그룹 해산 cascade·profile 삭제 cascade유지 근거검증
group_invitessystem없음(전역·참조)51 / 1 / 3 / 12on·11block_user_v1(d), create_group_v1(i), invite_to_group_v1(i), respond_group_invite_v1(d), ▲capture_user_notification_v1list_my_groups_v1, user_notification_visible_v1응답 때까지 — respond_group_invite_v1·block_user_v1 이 지움응답·차단·그룹 해산 cascade·profile 삭제 cascade유지 근거검증
group_membersfactuser_id41 / 0 / 2 / 12on·13create_group_v1(i), group_members_departure_v1(u), respond_group_invite_v1(i), ▲group_members_departure_v1, ▲user_fact_block_truncate_v1, ▲protect_group_members_facts_v1get_group_board_day_sessions_v1, get_group_lounge_v1, group_board_completion_members_v1, group_image_allowed_v1, group_member_role_v1, invite_to_group_v1, list_my_groups_v1, planned_session_visible_v2, protect_group_chat_messages_facts_v1, protect_group_members_facts_v1, protect_group_notices_facts_v1, protect_groups_facts_v1, protect_session_comments_facts_v1, session_readable_v1영구(유저 원본)탈퇴(group_members_departure_v1)·그룹 해산 cascade·profile 삭제 cascade유지 근거검증
group_noticesfactauthor_user_id51 / 0 / 2 / 12on·02add_group_notice_v1(i), ▲user_fact_block_truncate_v1, ▲protect_group_notices_facts_v1get_admin_content_reports_v1, get_group_lounge_v1, list_my_groups_design_v1, report_content_v1영구(유저 원본) — add_group_notice_v1 이 최신 1건만 두고 지움작성자 교체·그룹 해산 cascade·profile 삭제 cascade유지 근거검증
groupsfactid41 / 0 / 0 / 20on·02create_group_v1(i), disband_group_v1(d), group_members_departure_v1(d), update_group_profile_v1(u), ▲user_fact_block_truncate_v1, ▲protect_groups_facts_v1get_group_lounge_v1, list_my_groups_design_v1, list_my_groups_v1, list_user_notifications_v1, protect_group_chat_messages_facts_v1, protect_group_members_facts_v1, protect_group_notices_facts_v1, respond_group_invite_v1영구(유저 원본)소유자 해산(disband_group_v1)·마지막 소유자 탈퇴 시 departure 함수유지 근거검증

onboarding — 온보딩·동의

mermaid
erDiagram
  user_consents {
    uuid id PK
    uuid user_id FK
    text consent_type
    text document_version
    timestamp_with_time_zone accepted_at
    timestamp_with_time_zone withdrawn_at
    text source
    timestamp_with_time_zone created_at
  }
  auth_users ||--o{ user_consents : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
user_consentsfactuser_id81 / 0 / 1 / 42on·12apply_onboarding_consents(iu), withdraw_user_consent(u), ▲user_fact_block_truncate_v1, ▲protect_user_consents_facts_v1complete_onboarding_v2, onboarding_state_for_user, 앱 barbelicRepository.ts영구(동의 원본, 철회도 행 유지 withdraw_user_consent update)계정 삭제 cascade유지 근거검증

auth — 인증·auth.users 연동·관리자 판별

mermaid
erDiagram
  lift_guild_admins {
    uuid id PK
    uuid user_id FK
    text email
    text role
    boolean is_active
    text note
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  auth_users ||--o{ lift_guild_admins : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
lift_guild_adminssystemuser_id81 / 2 / 1 / 22off·00없음is_lift_guild_admin, is_lift_guild_admin_core_v1영구(관리자 명단, 운영 설정)운영자 수동·계정 삭제 cascade. RLS 꺼짐(문 함수 is_lift_guild_admin 로만 읽음)인계(B01(관리자 경계) — RLS·권한 경계 판정)검증

import — 인입(Wodup·Motra·InBody)

mermaid
erDiagram
  external_exercise_interpretation_components {
    uuid interpretation_id PK
    integer position PK
    uuid exercise_id FK
    text source_name
    text source_name_normalized
    text display_name
    text details
    integer reps_per_sequence
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  external_exercise_interpretations {
    uuid id PK
    text provider
    text provider_exercise_id
    text provider_name
    text provider_name_fingerprint
    integer version
    integer parser_version
    text status
    text target_kind
    text repetition_semantics
    text load_semantics
    text review_method
    text notes
    uuid reviewed_by FK
    text _more_ "열 3개 더"
  }
  inbody_import_batches {
    uuid id PK
    uuid user_id FK
    text status
    text storage_bucket
    text storage_path
    text file_name
    bigint file_size
    text content_type
    text file_sha256
    integer row_count
    text error_message
    timestamp_with_time_zone uploaded_at
    timestamp_with_time_zone completed_at
    timestamp_with_time_zone created_at
    text _more_ "열 1개 더"
  }
  wodup_exercises {
    text id PK
    text name
    text category
    text aliases
    text fields
    jsonb raw_payload
    boolean is_active
    text source_ref
    timestamp_with_time_zone imported_at
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  wodup_import_batches {
    uuid id PK
    uuid user_id FK
    text source
    text status
    text storage_bucket
    text storage_path
    text file_name
    bigint file_size
    text content_type
    text schema_version
    text exporter_version
    text wodup_user_id
    timestamp_with_time_zone exported_at
    integer inspected_lines
    text _more_ "열 31개 더"
  }
  wodup_import_exercises {
    uuid id PK
    uuid batch_id FK
    uuid user_id FK
    uuid row_id FK
    text wodup_session_id
    text wodup_result_id
    text session_exercise_key
    integer position
    text provider_exercise_key
    text provider_exercise_id
    text wodup_exercise_id
    text wodup_exercise_scope
    text wodup_exercise_owner_id
    text wodup_exercise_name
    text _more_ "열 5개 더"
  }
  wodup_import_rows {
    uuid id PK
    uuid batch_id FK
    uuid user_id FK
    integer line_number
    text kind
    text source
    text schema_version
    jsonb original
    jsonb normalized
    jsonb warnings
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  wodup_import_sessions {
    uuid id PK
    uuid batch_id FK
    uuid user_id FK
    uuid row_id FK
    text wodup_session_id
    date session_date
    text title
    text timezone
    timestamp_with_time_zone started_at
    timestamp_with_time_zone ended_at
    text source_ref
    jsonb original
    jsonb normalized
    timestamp_with_time_zone created_at
    text _more_ "열 1개 더"
  }
  wodup_import_sets {
    uuid id PK
    uuid batch_id FK
    uuid user_id FK
    uuid row_id FK
    text session_exercise_key
    text synthetic_set_id
    text wodup_session_id
    text wodup_result_id
    text wodup_exercise_id
    integer position
    integer reps
    numeric124 load
    text load_unit
    numeric124 duration
    text _more_ "열 14개 더"
  }
  wodup_session_timing_materializations {
    uuid session_id PK
    uuid user_id FK
    text source_ref
    timestamp_with_time_zone source_started_at
    timestamp_with_time_zone source_ended_at
    text source_timezone
    text effective_timezone
    text materialized_start_time
    text materialized_end_time
    integer duration_minutes
    text disposition
    timestamp_with_time_zone materialized_at
    timestamp_with_time_zone created_at
  }
  external_exercise_interpretations ||--o{ external_exercise_interpretation_components : "interpretation_id cascade"
  exercises ||--o{ external_exercise_interpretation_components : "exercise_id (catalog)"
  auth_users ||--o{ external_exercise_interpretations : "reviewed_by"
  auth_users ||--o{ inbody_import_batches : "user_id"
  auth_users ||--o{ wodup_import_batches : "user_id"
  wodup_import_batches ||--o{ wodup_import_exercises : "batch_id cascade"
  wodup_import_batches ||--o{ wodup_import_exercises : "batch_id,user_id cascade"
  wodup_import_rows ||--o{ wodup_import_exercises : "row_id cascade"
  auth_users ||--o{ wodup_import_exercises : "user_id"
  wodup_import_batches ||--o{ wodup_import_rows : "batch_id cascade"
  wodup_import_batches ||--o{ wodup_import_rows : "batch_id,user_id cascade"
  auth_users ||--o{ wodup_import_rows : "user_id"
  wodup_import_batches ||--o{ wodup_import_sessions : "batch_id cascade"
  wodup_import_batches ||--o{ wodup_import_sessions : "batch_id,user_id cascade"
  wodup_import_rows ||--o{ wodup_import_sessions : "row_id cascade"
  auth_users ||--o{ wodup_import_sessions : "user_id"
  wodup_import_batches ||--o{ wodup_import_sets : "batch_id cascade"
  wodup_import_batches ||--o{ wodup_import_sets : "batch_id,user_id cascade"
  wodup_import_rows ||--o{ wodup_import_sets : "row_id cascade"
  auth_users ||--o{ wodup_import_sets : "user_id"
  session ||--o{ wodup_session_timing_materializations : "session_id (workout)"
  auth_users ||--o{ wodup_session_timing_materializations : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
external_exercise_interpretation_componentsreference없음(전역·참조)101 / 0 / 2 / 51on·10refresh_wodup_complex_interpretations_v1(i)apply_wodup_training_complex_meta_v2, stage_wodup_import_batch_engine, 앱 wodup-import-worker.ts참조 데이터(Wodup 복합 해석, refresh_wodup_complex_interpretations_v1 이 재생성)부모 해석 삭제 cascade유지 근거검증
external_exercise_interpretationsreference없음(전역·참조)171 / 1 / 1 / 92on·10refresh_wodup_complex_interpretations_v1(i)apply_wodup_training_complex_meta_v2, stage_wodup_import_batch_engine, 앱 wodup-import-worker.ts참조 데이터(Wodup 복합 해석)재생성 함수만유지 근거검증
inbody_import_batchessystemuser_id151 / 1 / 1 / 42on·10import_inbody_body_metrics(u)get_profile_workspace, 앱 profileRepository.ts영구(인입 배치 기록)계정 삭제 cascade유지 근거검증
wodup_exercisessystem없음(전역·참조)111 / 0 / 0 / 13on·40stage_wodup_import_batch_engine(i)get_admin_mapping_page, refresh_wodup_complex_interpretations_v1, 앱 barbelicRepository.ts정책 없음 — Wodup 종목 카탈로그 사본(staging 이 채움, 영구)삭제 경로 없음인계(D09(인입) — 완료 배치 staging·사본 보존 정책)검증
wodup_import_batchessystemuser_id451 / 2 / 1 / 163on·40enqueue_wodup_import_batch(u), mark_wodup_import_batch_failed_v1(u), mark_wodup_import_batch_uploaded_v1(u), start_wodup_import_batch(u)get_admin_health_report, get_profile_workspace, import_wodup_batch_to_canonical_engine, purge_wodup_import_failed_staging_v1, requeue_wodup_import_batch_v1, stage_wodup_import_batch_engine, 앱 barbelicRepository.ts, 앱 wodup-import-worker.ts, 앱 index.ts, 앱 index.ts영구(인입 배치 기록·상태 기계)계정 삭제 cascade유지 근거검증
wodup_import_exercisessystemuser_id191 / 1 / 4 / 03on·10purge_wodup_import_failed_staging_v1(d), stage_wodup_import_batch_engine(di)import_wodup_batch_to_canonical_engine실패 배치 7일(cron barbelic-wodup-failed-staging-purge) — 완료 배치는 정책 없음(Production 4,827행 영구)purge cron(실패만)·배치 삭제 cascade·계정 삭제 cascade인계(D09(인입) — 완료 배치 staging 보존 정책)검증
wodup_import_rowssystemuser_id121 / 1 / 3 / 21on·10purge_wodup_import_failed_staging_v1(d), stage_wodup_import_batch_engine(di)없음실패 배치 7일 — 완료 배치는 정책 없음(Production 2,243행·original/normalized jsonb 12MB 영구)purge cron(실패만)·배치 삭제 cascade·계정 삭제 cascade. 고유 키와 같은 열의 중복 인덱스 정리 대상. 배치 소유자와 행 소유자를 잇는 제약 없음(Phase 1 TODO → Phase 3)유지 근거(D09(인입) — 완료 배치 staging 보존 정책)검증
wodup_import_sessionssystemuser_id151 / 1 / 4 / 03on·10purge_wodup_import_failed_staging_v1(d), stage_wodup_import_batch_engine(di)import_wodup_batch_to_canonical_engine, wodup_session_timing_source_v1실패 배치 7일 — 완료 배치는 정책 없음(Production 2,243행 영구)purge cron(실패만)·배치 삭제 cascade·계정 삭제 cascade인계(D09(인입) — 완료 배치 staging 보존 정책)검증
wodup_import_setssystemuser_id281 / 1 / 4 / 03on·10purge_wodup_import_failed_staging_v1(d), stage_wodup_import_batch_engine(di)import_wodup_batch_to_canonical_engine실패 배치 7일 — 완료 배치는 정책 없음(Production 20,694행·25MB 영구)purge cron(실패만)·배치 삭제 cascade·계정 삭제 cascade인계(D09(인입) — 완료 배치 staging 보존 정책)검증
wodup_session_timing_materializationssystemuser_id131 / 1 / 2 / 30on·00backfill_wodup_session_timing_v1(i)refresh_user_session_timing_stats_from백필 기록(backfill_wodup_session_timing_v1 1회성) — Production 0행세션 삭제 cascade·계정 삭제 cascade. 고유 키와 같은 열의 중복 인덱스 정리 대상. 읽는 함수 1개(백필 자기 자신) — 잔존 표 확인 필요유지 근거검증

stats — 통계 계산·refresh 잡·정책·투영

mermaid
erDiagram
  max_rep_estimation_policy_versions {
    text policy_id PK
    text policy_version PK
    text rpe_scale_version
    text docs_path
    boolean is_active
    jsonb policy
    timestamp_with_time_zone created_at
  }
  set_purpose_policy_versions {
    text policy_id PK
    text policy_version PK
    text docs_path
    boolean is_active
    jsonb policy
    timestamp_with_time_zone created_at
  }
  stats_refresh_event_types {
    text id PK
    text source_table
    text source_operation
    text dirty_range_strategy
    text exercise_scope
    text description
    boolean is_active
    integer sort_order
    timestamp_with_time_zone created_at
    timestamp_with_time_zone updated_at
  }
  strength_estimation_curve_points {
    text curve_id PK
    text curve_version PK
    numeric84 load_percent PK
    numeric104 mean_repetitions
    numeric104 sd_repetitions
    boolean is_product_anchor
  }
  strength_estimation_curve_versions {
    text curve_id PK
    text curve_version PK
    text source_name
    text source_sha256
    text canonical_csv_sha256
    text derived_artifact_sha256
    text curve_breaking_fingerprint_sha256
    numeric1816 population_reference_z
    text notes
    timestamp_with_time_zone created_at
  }
  strength_estimation_integer_lookup_snapshots {
    text curve_id PK
    text curve_version PK
    smallint effective_reps PK
    numeric106 load_pct_reference_low
    numeric106 load_pct_reference_center
    numeric106 load_pct_reference_high
  }
  strength_estimation_policy_versions {
    text policy_id PK
    text policy_version PK
    text curve_id FK
    text curve_version FK
    text rpe_scale_version
    numeric62 min_effective_reps
    numeric62 max_effective_reps
    integer stale_after_days
    text docs_path
    text breaking_fingerprint_sha256
    boolean is_active
    jsonb policy
    timestamp_with_time_zone created_at
  }
  user_exercise_max_rep_observations {
    uuid source_set_id PK
    uuid user_id FK
    uuid exercise_id FK
    uuid session_id FK
    uuid session_exercise_id FK
    date observed_on
    timestamp_with_time_zone session_created_at
    integer set_position
    text set_type
    text measurement_type
    numeric82 stats_load_kg
    integer raw_reps
    integer target_reps
    text set_result
    text _more_ "열 16개 더"
  }
  user_exercise_session_rollups {
    uuid session_exercise_id PK
    uuid user_id FK
    uuid exercise_id FK
    uuid session_id FK
    date recorded_at
    timestamp_with_time_zone session_created_at
    text session_title
    boolean track_pr
    integer set_count
    numeric122 volume
    numeric82 best_load
    integer best_load_reps
    numeric82 best_estimated_1rm
    numeric82 best_estimated_1rm_load
    text _more_ "열 31개 더"
  }
  user_exercise_set_score_states {
    uuid user_id PK
    uuid exercise_id PK
    numeric82 recent_performed_1rm_kg
    numeric82 recent_performed_max_reps
    bigint strength_applied_version
    bigint max_rep_applied_version
    timestamp_with_time_zone updated_at
  }
  user_exercise_stats {
    uuid user_id PK
    uuid exercise_id PK
    integer session_count
    integer set_count
    numeric122 total_volume
    numeric82 best_load
    integer best_load_reps
    uuid best_load_session_id FK
    date best_load_recorded_at
    numeric82 best_estimated_1rm
    numeric82 best_estimated_1rm_load
    integer best_estimated_1rm_reps
    uuid best_estimated_1rm_session_id FK
    date best_estimated_1rm_recorded_at
    text _more_ "열 5개 더"
  }
  user_exercise_stats_refresh_jobs {
    uuid id PK
    uuid user_id FK
    text event_type FK
    date from_date
    uuid exercise_ids
    text reason
    text source_ref
    text status
    timestamp_with_time_zone locked_at
    text lock_owner
    integer attempts
    text error_message
    jsonb metadata
    timestamp_with_time_zone created_at
    text _more_ "열 3개 더"
  }
  user_exercise_strength_daily {
    uuid user_id PK
    uuid exercise_id PK
    date observed_on PK
    numeric82 e1rm_lower_kg
    numeric82 e1rm_representative_kg
    numeric82 e1rm_upper_kg
    text e1rm_confidence
    text e1rm_estimate_kind
    boolean upper_bound_open
    uuid representative_source_set_id FK
    uuid source_set_ids
    integer session_vote_count
    boolean accepted_into_state
    numeric82 reference_e1rm_kg
    text _more_ "열 10개 더"
  }
  user_exercise_strength_observations {
    uuid source_set_id PK
    uuid user_id FK
    uuid exercise_id FK
    uuid session_id FK
    uuid session_exercise_id FK
    date observed_on
    timestamp_with_time_zone session_created_at
    timestamp_with_time_zone session_order_at
    text session_intent
    integer set_position
    text set_type
    numeric82 stats_load_kg
    integer raw_reps
    integer target_reps
    text _more_ "열 38개 더"
  }
  user_exercise_strength_states {
    uuid user_id PK
    uuid exercise_id PK
    date current_observed_on
    uuid current_source_set_id FK
    numeric82 current_e1rm_lower_kg
    numeric82 current_e1rm_representative_kg
    numeric82 current_e1rm_upper_kg
    text current_confidence
    text current_estimate_kind
    numeric82 trusted_reference_e1rm_kg
    date trusted_reference_observed_on
    uuid trusted_reference_source_set_id FK
    text trusted_reference_confidence
    date stale_after
    text _more_ "열 6개 더"
  }
  user_stats_projection_runs {
    uuid job_id PK
    uuid user_id FK
    uuid lease_token
    timestamp_with_time_zone lease_until
    bigint target_version
    bigint base_version
    date snapshot_anchor_date
    text phase
    jsonb payload
    text error_code
    text error_message
    numeric compute_ms
    numeric publish_ms
    timestamp_with_time_zone created_at
    text _more_ "열 1개 더"
  }
  user_stats_refresh_state {
    uuid user_id PK
    bigint requested_version
    bigint applied_version
    date dirty_from
    timestamp_with_time_zone requested_at
    timestamp_with_time_zone applied_at
    text last_error
    timestamp_with_time_zone last_error_at
    timestamp_with_time_zone updated_at
  }
  strength_estimation_curve_versions ||--o{ strength_estimation_curve_points : "curve_id,curve_version cascade"
  strength_estimation_curve_versions ||--o{ strength_estimation_integer_lookup_snapshots : "curve_id,curve_version cascade"
  strength_estimation_curve_versions ||--o{ strength_estimation_policy_versions : "curve_id,curve_version no action"
  max_rep_estimation_policy_versions ||--o{ user_exercise_max_rep_observations : "policy_id,policy_version no action"
  exercises ||--o{ user_exercise_max_rep_observations : "exercise_id (catalog)"
  session_exercise_part ||--o{ user_exercise_max_rep_observations : "session_exercise_id (workout)"
  session ||--o{ user_exercise_max_rep_observations : "session_id (workout)"
  exercise_set_part ||--o{ user_exercise_max_rep_observations : "source_set_id (workout)"
  auth_users ||--o{ user_exercise_max_rep_observations : "user_id"
  exercise_set_part ||--o{ user_exercise_session_rollups : "best_estimated_1rm_set_id (workout)"
  exercises ||--o{ user_exercise_session_rollups : "exercise_id (catalog)"
  max_rep_estimation_policy_versions ||--o{ user_exercise_session_rollups : "max_rep_policy_id,max_rep_policy_version no action"
  exercise_set_part ||--o{ user_exercise_session_rollups : "estimated_max_reps_source_set_id (workout)"
  session_exercise_part ||--o{ user_exercise_session_rollups : "session_exercise_id (workout)"
  session ||--o{ user_exercise_session_rollups : "session_id (workout)"
  auth_users ||--o{ user_exercise_session_rollups : "user_id"
  exercises ||--o{ user_exercise_set_score_states : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_set_score_states : "user_id"
  session ||--o{ user_exercise_stats : "best_estimated_1rm_session_id (workout)"
  session ||--o{ user_exercise_stats : "best_load_session_id (workout)"
  exercise_set_part ||--o{ user_exercise_stats : "best_estimated_1rm_set_id (workout)"
  exercises ||--o{ user_exercise_stats : "exercise_id (catalog)"
  auth_users ||--o{ user_exercise_stats : "user_id"
  stats_refresh_event_types ||--o{ user_exercise_stats_refresh_jobs : "event_type no action"
  auth_users ||--o{ user_exercise_stats_refresh_jobs : "user_id"
  strength_estimation_curve_versions ||--o{ user_exercise_strength_daily : "curve_id,curve_version no action"
  exercises ||--o{ user_exercise_strength_daily : "exercise_id (catalog)"
  strength_estimation_policy_versions ||--o{ user_exercise_strength_daily : "policy_id,policy_version no action"
  exercise_set_part ||--o{ user_exercise_strength_daily : "representative_source_set_id (workout)"
  auth_users ||--o{ user_exercise_strength_daily : "user_id"
  strength_estimation_policy_versions ||--o{ user_exercise_strength_observations : "policy_id,policy_version no action"
  strength_estimation_curve_versions ||--o{ user_exercise_strength_observations : "curve_id,curve_version no action"
  exercises ||--o{ user_exercise_strength_observations : "exercise_id (catalog)"
  session_exercise_part ||--o{ user_exercise_strength_observations : "session_exercise_id (workout)"
  session ||--o{ user_exercise_strength_observations : "session_id (workout)"
  exercise_set_part ||--o{ user_exercise_strength_observations : "source_set_id (workout)"
  auth_users ||--o{ user_exercise_strength_observations : "user_id"
  exercise_set_part ||--o{ user_exercise_strength_states : "current_source_set_id (workout)"
  strength_estimation_curve_versions ||--o{ user_exercise_strength_states : "curve_id,curve_version no action"
  exercises ||--o{ user_exercise_strength_states : "exercise_id (catalog)"
  strength_estimation_policy_versions ||--o{ user_exercise_strength_states : "policy_id,policy_version no action"
  exercise_set_part ||--o{ user_exercise_strength_states : "trusted_reference_source_set_id (workout)"
  auth_users ||--o{ user_exercise_strength_states : "user_id"
  user_exercise_stats_refresh_jobs ||--o{ user_stats_projection_runs : "job_id cascade"
  auth_users ||--o{ user_stats_projection_runs : "user_id"
  auth_users ||--o{ user_stats_refresh_state : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
max_rep_estimation_policy_versionsreference없음(전역·참조)71 / 0 / 0 / 01on·10없음validate_user_max_rep_integrity참조 데이터(정책 버전, 영구)마이그레이션만유지 근거검증
set_purpose_policy_versionsreference없음(전역·참조)61 / 0 / 0 / 01on·10없음validate_user_max_rep_integrity참조 데이터(정책 버전)마이그레이션만유지 근거검증
stats_refresh_event_typesreference없음(전역·참조)101 / 0 / 0 / 20off·00없음enqueue_user_exercise_stats_refresh_job_core참조 데이터(잡 유형)마이그레이션만. RLS 꺼짐(참조 표, 앱 권한 없음)유지 근거검증
strength_estimation_curve_pointsreference없음(전역·참조)61 / 0 / 1 / 30on·10없음strength_estimation_load_percent_v1참조 데이터(추정 곡선)마이그레이션만유지 근거검증
strength_estimation_curve_versionsreference없음(전역·참조)101 / 0 / 0 / 40on·10없음strength_estimation_load_percent_v1, validate_user_exercise_stats_integrity_engine_v1참조 데이터(곡선 버전)마이그레이션만유지 근거검증
strength_estimation_integer_lookup_snapshotsreference없음(전역·참조)61 / 0 / 1 / 20on·10없음validate_user_exercise_stats_integrity_engine_v1참조 데이터(정수 조회 표)마이그레이션만유지 근거검증
strength_estimation_policy_versionsreference없음(전역·참조)131 / 0 / 1 / 21on·10없음validate_user_exercise_stats_integrity_engine_v1참조 데이터(정책 버전)마이그레이션만유지 근거검증
user_exercise_max_rep_observationsprojectionuser_id301 / 0 / 6 / 134on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_max_rep_projection(diu)get_volume_overview_v3_core, report_favorite_growth_v1, strength_set_metrics_map_json, user_exercise_max_rep_pr_timeline_v1, validate_user_max_rep_integrity재계산 가능(투영)refresh(전량)·원본 세트/세션 삭제 cascade·계정 삭제 cascade. session_exercise_id·session_id 참조 열 인덱스 없음유지 근거검증
user_exercise_session_rollupsprojectionuser_id451 / 0 / 7 / 55on·10clear_rollup_max_rep_on_source_set_delete_v1(u), publish_stats_projection_job_v1(di;dynamic), refresh_user_exercise_main_reps_from(u), refresh_user_exercise_stats_from_base(di), refresh_user_max_rep_projection(u), refresh_user_set_purpose_projection(u)build_home_dashboard_core_v2, enqueue_stale_user_exercise_stats_refresh_jobs, get_admin_health_report, get_calendar_month_summary, get_exercise_pr_history_v3_core, get_profile_feed_page_engine_v1, get_session_search_v2_engine, refresh_user_training_main_reps_from, refresh_user_training_period_stats_from, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity재계산 가능(투영)refresh(전량)·원본 삭제 SET NULL/cascade·계정 삭제 cascade. best_estimated_1rm_set_id·session_id 참조 열 인덱스 없음, 접두 포함 인덱스 1개유지 근거검증
user_exercise_set_score_statesprojectionuser_id71 / 0 / 2 / 20on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_max_rep_projection(iu), refresh_user_strength_estimation_projection_from_v1(iu)없음재계산 가능(투영)refresh·계정 삭제 cascade유지 근거검증
user_exercise_statsprojectionuser_id191 / 0 / 5 / 44on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_exercise_best_reps_from(u), refresh_user_exercise_stats_from_base(di)build_exercise_pr_detail_core_v3, build_user_exercise_stats_json, build_user_pr_exercise_summary_rows, get_admin_health_report, get_exercise_search_signals_v1, get_volume_overview_v1_core, refresh_exercise_usage_stats_v1, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity, 앱 barbelicRepository.ts재계산 가능(투영)refresh(전량)·원본 삭제 SET NULL·계정 삭제 cascade유지 근거검증
user_exercise_stats_refresh_job_observability (뷰)-0--definer-없음없음뷰(보존 없음) — 잡 표 투영. 처리 시간이 항상 0 으로 계산되는 결함(G04)-인계(D08(worker·관측) — duration_ms 계산 결함)검증
user_exercise_stats_refresh_jobsprojectionuser_id171 / 0 / 2 / 44on·03claim_stats_projection_job_v1(u), compute_stats_projection_job_v1(u), enqueue_user_exercise_stats_refresh_job_core(iu), process_user_exercise_stats_refresh_jobs_for_user_now(u), process_user_exercise_stats_refresh_jobs(u), publish_stats_projection_job_v1(u), requeue_retryable_user_exercise_stats_refresh_jobs(u), ▲guard_user_stats_refresh_projection_publication, ▲set_user_stats_refresh_projection_context, ▲sync_user_stats_refresh_state_from_jobget_admin_health_report, process_user_exercise_stats_refresh_jobs_for_user, refresh_user_exercise_stats_from, refresh_user_max_rep_projection, refresh_user_strength_estimation_projection_from_v1, sync_user_stats_refresh_state_from_job, 뷰 user_exercise_stats_refresh_job_observability정책 없음 — 완료 잡이 영구 누적(Production 완료 1,646·실패 3, 2026-07-27~)삭제 경로 없음·계정 삭제 cascade인계(D08(worker) — 완료 잡 보존 기간·purge)검증
user_exercise_strength_dailyprojectionuser_id241 / 0 / 5 / 41on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_strength_estimation_projection_from_v1(d)get_exercise_pr_detail_year, validate_user_exercise_stats_integrity_engine_v1재계산 가능(투영)refresh(전량)·원본 세트 삭제 cascade·계정 삭제 cascade. representative_source_set_id 참조 열 인덱스 없음, 기본키와 같은 열의 중복 인덱스유지 근거검증
user_exercise_strength_observationsprojectionuser_id521 / 0 / 7 / 86on·11publish_stats_projection_job_v1(di;dynamic), refresh_user_set_purpose_projection(u), refresh_user_strength_estimation_projection_from_v1(diu), ▲exclude_training_complex_strength_observation_v1calendar_strength_session_metrics_map_v1, refresh_user_calendar_day_summaries, refresh_user_report_distributions_v1, strength_set_metrics_map_json, validate_user_exercise_stats_integrity_engine_v1, validate_user_set_purpose_integrity재계산 가능(투영) — 매 갱신 전량 재작성(Production 삽입 562만·삭제 559만)refresh·원본 삭제 cascade·계정 삭제 cascade. session_exercise_id·session_id 참조 열 인덱스 없음유지 근거검증
user_exercise_strength_statesprojectionuser_id201 / 0 / 6 / 52on·10publish_stats_projection_job_v1(di;dynamic), refresh_user_strength_estimation_projection_from_v1(di)get_exercise_pr_detail_year, validate_user_exercise_stats_integrity_engine_v1재계산 가능(투영)refresh·원본 세트 삭제 cascade·계정 삭제 cascade. current/trusted 소스 세트 참조 열 인덱스 없음유지 근거검증
user_stats_projection_runsprojectionuser_id151 / 0 / 2 / 12on·00claim_stats_projection_job_v1(iu), compute_stats_projection_job_v1(u), publish_stats_projection_job_v1(u)없음작업별 durable lease·실행시간·결과. 미게시 파생 payload는 게시 또는 실패 시 즉시 비운다연결된 통계 작업 또는 계정 삭제 시 cascade. 원본 세션·세트에 대한 FK는 없어 계산이 원본 쓰기를 잠그지 않는다유지 근거(D08 #1412 — 기존 유지보수 worker 진입점 통합과 세부 workload 분할)검증
user_stats_refresh_stateprojectionuser_id91 / 0 / 1 / 30on·11enqueue_user_exercise_stats_refresh_job_core(i), sync_user_stats_refresh_state_from_job(i), ▲prune_user_pr_overview_snapshot_generationsbackfill_wodup_bodyweight_snapshot_v1, backfill_wodup_session_timing_v1, claim_stats_projection_job_v1, compute_stats_projection_job_v1, get_home_year_activity, get_user_pr_exercise_summaries_json, get_user_stats_refresh_freshness, initialize_user_pr_overview_snapshot, process_user_exercise_stats_refresh_jobs_for_user, publish_stats_projection_job_v1, refresh_user_exercise_stats_from, refresh_user_max_rep_projection, refresh_user_strength_estimation_projection_from_v1, run_user_pr_overview_snapshot_rollover, validate_user_exercise_stats_integrity_engine_v1, validate_user_max_rep_integrity사용자당 1행(요청/반영 세대)계정 삭제 cascade유지 근거검증

durable — 기기 대기열·초안 체크포인트

mermaid
erDiagram
  workout_draft_checkpoint_fences {
    uuid user_id PK
    bigint sequence
    text retired_operation_id
    timestamp_with_time_zone retired_at
    timestamp_with_time_zone updated_at
  }
  workout_draft_checkpoints {
    uuid user_id PK
    jsonb payload
    text operation_id
    timestamp_with_time_zone saved_at
    timestamp_with_time_zone expires_at
    timestamp_with_time_zone updated_at
  }
  auth_users ||--o{ workout_draft_checkpoint_fences : "user_id"
  auth_users ||--o{ workout_draft_checkpoints : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
workout_draft_checkpoint_fencessystemuser_id51 / 0 / 1 / 10on·00clear_workout_draft_checkpoint_v2(iu), retire_workout_draft_checkpoint_operation_v1(iu), save_workout_draft_checkpoint_v2(iu)get_workout_draft_checkpoint_v2사용자당 1행(진행 중 운동 초안의 서버 순번/은퇴 펜스, S08 #1326) — 계정이 있는 동안계정 삭제 시 auth.users cascade. 앱 삭제 경로 없음(펜스는 덮어쓴다)유지 근거검증
workout_draft_checkpointsfactuser_id61 / 0 / 1 / 20on·02clear_workout_draft_checkpoint_v2(d), get_workout_draft_checkpoint_v2(d), save_workout_draft_checkpoint_v2(i), ▲user_fact_block_truncate_v1, ▲protect_workout_draft_checkpoints_facts_v1없음완료 저장 때까지(기기 초안 체크포인트) — clear/get 함수가 지움clear_workout_draft_checkpoint_v1·get 시 소비·계정 삭제 cascade유지 근거검증

shell — 앱 셸 공용(클라이언트 오류 기록)

mermaid
erDiagram
  client_error_events {
    uuid id PK
    uuid user_id FK
    uuid client_event_id
    smallint contract_version
    timestamp_with_time_zone occurred_at
    timestamp_with_time_zone received_at
    text kind
    text source
    text severity
    text surface
    text release
    text error_name
    text error_code
    text diagnostic_key
    text _more_ "열 11개 더"
  }
  auth_users ||--o{ client_error_events : "user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
client_error_eventssystemuser_id251 / 0 / 1 / 103on·00purge_client_error_events_v1(d), record_client_error_event(i)get_admin_operations_checklist90일 — cron barbelic-client-error-purge(매일 04:17)가 purge_client_error_events_v1 로 지움purge cron·계정 삭제 cascade유지 근거검증

db-platform — DB 플랫폼(확장·스키마·기본 권한·원본 보호·BRID·공용 도우미)

mermaid
erDiagram
  user_fact_history {
    bigint id PK
    text table_name
    text row_id
    text operation
    jsonb old_row
    text changed_columns
    uuid actor_user_id
    text repair_ticket
    uuid owner_user_id FK
    timestamp_with_time_zone archived_at
  }
  auth_users ||--o{ user_fact_history : "owner_user_id"
object등급owner 연결PK / UNIQUE / FK / CHECK인덱스RLS·정책트리거작성자(함수)읽는 곳보존삭제 경로결론상태
user_fact_historysystemowner_user_id101 / 0 / 1 / 22on·02protect_body_metrics_facts_v1(i), protect_content_reports_facts_v1(i), protect_daily_conditions_facts_v1(i), protect_exercise_set_facts_v1(i), protect_exercise_set_part_facts_v1(i), protect_exercises_facts_v1(i), protect_group_chat_messages_facts_v1(i), protect_group_members_facts_v1(i), protect_group_notices_facts_v1(i), protect_groups_facts_v1(i), protect_profiles_facts_v1(i), protect_session_comments_facts_v1(i), protect_session_exercise_facts_v1(i), protect_session_exercise_part_facts_v1(i), protect_session_facts_v1(i), protect_session_likes_facts_v1(i), protect_user_blocks_facts_v1(i), protect_user_consents_facts_v1(i), protect_user_exercise_favorites_facts_v1(i), protect_user_follows_facts_v1(i), protect_user_manual_pr_records_facts_v1(i), protect_user_manual_record_metrics_facts_v1(i), protect_workout_draft_checkpoints_facts_v1(i), ▲user_fact_history_is_append_only_v1, ▲user_fact_block_truncate_v1restore_user_fact_v1정책 없음 — 원본 보호 트리거의 append-only 이력(#1236), 영구 누적삭제·갱신 트리거로 금지, 계정 삭제 cascade 만인계(D13(원본 보호) — 이력 보존 기간)검증

<!-- db-model-ledger:generated:end -->

4. D12 결과표 — 발견 → 결론

Production 실측(2026-09-07, 읽기 전용 집계)은 이슈 #1328 계획 댓글. 아래는 그 발견 11건이 어디로 갔는지다. "수정" 은 마이그레이션 20260913030100_d12_db_model_quality(랜딩 시 번호 확정), "인계" 는 담당 이슈에 댓글로 넘긴 것.

4-1. 발견 → 결론

#발견결론근거·증거
1같은 열 집합 인덱스 9쌍 + 접두 포함 인덱스 8수정 — 비고유 쪽 9 + 접두 7 drop(exercise_external_mappings 는 접두 쌍 중 소비자 없는 넓은 쪽을 drop). 기계 판정 9/9 → 0/0, db:model:check 가 0 을 강제§3 기계 판정 · Production idx_scan(남는 PK/UNIQUE 로 planner 가 옮겨감: user_exercise_period_stats 79만 회 → pkey) · §4-2 전후 실행계획에 seq scan 증가 없음
2참조 열 인덱스 없는 FK 65/147수정 — 저장·삭제·계정 삭제 경로 25개에 인덱스(nullable 열은 is not null 부분 인덱스). 나머지 42개는 지우지 않는 참조 데이터·드문 관리자 경로라는 이유를 정책 파일 findings 에 적어 닫힌 목록으로 고정(새 FK 는 인덱스 또는 이유 없이는 npm test 실패). "열 IS NOT NULL" 부분 인덱스·복합 FK 의 선두 열 인덱스는 덮는 것으로 판정§4-2 write.delete_* 전후 · populated upgrade 증거(마이그레이션 머리 -- upgrade-evidence:)
3투영 표 전체 읽기(seq scan)수정(2)·인계 — 저장·삭제 경로의 전체 읽기는 2 로 해소. 화면 함수 안의 전체 읽기(get_exercise_catalog 종목 803행·get_session_search 읽은 행 21,361·get_volume_overview 57,143)는 함수 본문(D04·D08·A03 소유)의 조회 형태라 실행계획을 붙여 인계§4-2 표 · 인계 댓글
4물리 크기 부풀음(session TOAST 16배, 투영 표 churn)인계 — D08(worker 재작성 방식)·D11·R05(운영 VACUUM). D12 는 인덱스 수를 줄여 재작성 1회당 인덱스 갱신을 줄인다(투영 표 5개에서 중복 5 drop, FK 인덱스 13 추가 — 순증 +8, 크기 §4-3)계획 댓글 표 4 · §4-3 쓰기 비용
5한 번도 안 쓰인 인덱스 24수정(13)·유지(11) — 함수 본문·앱 조회·pgTAP 전수 대조로 소비자 0 인 13개 drop(body_metrics_created_at_idx·client_error_events_fingerprint_idx·exercise_external_mappings_admin_status_idx·profiles_created_at_idx·session_exercise_part_provider_exercise_idx·stats_refresh_event_types_active_sort_idx·user_exercise_favorites_user_order_idx·user_exercise_records_session_pr_idx·…_refresh_jobs_exercise_ids_gin_idx·…_refresh_jobs_pending_source_ref_idx·user_manual_pr_records_user_exercise_date_idx·user_pr_overview_rollover_state_{catalog,generation}_idx). 유지 11 은 실제 소비자가 있는 것(예: body_metrics_user_date_idxbody_weight_snapshot_for_date_v1, session_origin_kind_ref_idx ← 그룹 보드 하루 세션, user_fact_history_owner_idx ← 계정 삭제 FK)소비자 표(이슈 #1328 Phase 3 보고)
6보존·삭제 정책이 object 단위로 없음표 완성·인계 — 80 object 전부 정책 파일에 보존·삭제 경로 기재. 삭제 실행이 필요한 것(완료 staging·영수증 30일 초과·완료 잡)은 D09·D02·D13 에 인계, 이 트랙은 데이터를 지우지 않음§3 각 object 행
7JSON 이중 권위(jsonb 29열)유지(등급 확정)·인계 — 원본 등급 열(profiles.onboarding_draft·exercises.target_muscles·workout_draft_checkpoints.payload)은 값 불변, raw_payload 3열은 system 등급으로 "쓰기 시 채우고 화면 함수 5곳이 읽는" 사실을 장부에 고정. 열로 승격할 후보(raw_payload->'details')는 D02(저장 함수 소유) 인계§3 jsonb 열 목록
8RLS·권한 시험 편차수정(시험) — Phase 1 pgTAP 6파일(98 assert)로 6 도메인의 본인/타인/비로그인/service_role 경계 고정. 인입 staging 표의 소유자 직접 UPDATE 정책(ALL)은 사실로 고정하고 I01/D09 인계supabase/tests/database/d12_*_constraints.test.sql
9시간대·정밀도·단위유지 근거 — 시각 열 전부 timestamptz, session.scheduled_time(벽시계) 1개는 의도. numeric 스케일·0≠null·업무 날짜(앱이 보낸 date 가 정본)는 Phase 1 fixture 로 고정Phase 1 pgTAP
10층 규칙 예외 24 중 D12 몫 3수정 — 앱 역할 권한이 없는 내부용 쌍둥이 exercise_target_muscles_valid_core_v1·is_lift_guild_admin_core_v1 을 두고 엔진·내부 함수가 그쪽을 부른다. 문(exercise_target_muscles_valid_v1 는 CHECK 제약이 authenticated 로서 부르고, is_lift_guild_admin 은 B01 소유)은 그대로. 두 쌍이 같은 답을 내는 것은 pgTAP d12_layer_helpers 가 고정. exceptions.json 24 → 21sql:check 예외 목록 일치
11카탈로그 읽기 비용(get_exercise_catalog 평균 275ms)인계(A03) — 실행계획: 종목 803행 seq scan + 별칭·변경 로그 정렬 804회(2.5초/822KB, 샌드박스). 기기 사본 증분 동기화(get_exercise_catalog_changes)도 813행 정렬 506회§4-2 표
+(Phase 1 발견) 인입 staging 행과 배치의 소유자를 잇는 제약 없음수정wodup_import_{rows,sessions,exercises,sets} 에 복합 FK (batch_id, user_id) → wodup_import_batches(id, user_id)(NOT VALID + VALIDATE). Production 불일치 0행 확인 뒤 적용pgTAP d12_import_constraints 7번(TODO → 실제 단언)
+(Phase 1 발견) 커밋 시점 검사 FK 11유지 근거 — 정체성 병합·저장 문 안의 일시적 재배정을 위해 의도된 것(#1175·#1215). 목록은 §3 기계 판정에 실어 늘지 않게 본다§3
+(Phase 1 발견) exercise_external_mappings.exercise_id 이중 FK유지 근거 — cascade 가 먼저 지워 NO ACTION 검사가 빈 집합을 보므로 실질 cascade, DEFERRABLE 쪽은 병합 트랜잭션용. 둘 중 하나를 빼면 의미가 사라져 바꾸지 않는다정책 파일 evidence

4-2. 대표 조회 실행계획 — 전 → 후

<!-- d12-explain:begin --> 측정: 2026-09-07, 샌드박스(사용자 11명·완료 세션 5,209·세트 62,508 = G04 history-10y + multi-owner-10 + npm run db:fixture:domain), npm run db:explain -- --label before|after(3회 반복, 표는 warm 값·ms 는 최소~최대). "전" 은 main 48f249c2 스키마, "후" 는 마이그레이션 적용 뒤. 증거 파일 explain-before.json·explain-after.json 은 PR #1328 본문에 첨부 경로. 같은 목록을 R04 가 릴리스 전후 비교에 그대로 쓴다(supabase/contracts/representative-queries.json).

읽는 법 — "읽은 행" 은 seq scan·index scan 노드의 (실제 행 × 반복) 합, "seq scan" 은 표 전체를 읽은 노드와 그 행 수. 인덱스를 지운 조회에서 seq scan 이 늘지 않았는지, 인덱스를 더한 쓰기 경로에서 seq scan 이 사라졌는지가 판정 기준이다. ms 는 같은 PC 에서 다른 샌드박스의 재생·harness 가 동시에 돌던 구간이 있어 ±30% 는 잡음이다(버퍼 수가 일의 양).

조회역할ms 전 → 후읽은 행 전 → 후seq scan 전 (표: 행)seq scan 후인덱스 수 전→후페이지
profile.workspaceowner0.7~2.3 → 0.6~2.122 → 22body_metrics: 20, wodup_import_batches: 1, inbody_import_batches: 1body_metrics: 20, wodup_import_batches: 1, inbody_import_batches: 10→0
profile.ensureowner0.6~2.5 → 0.8~2.42 → 2users: 1, profiles: 1users: 1, profiles: 10→0
profile.handle_checkextra0.3~1.5 → 0.6~2.30 → 0moderation_banned_terms: 0, profiles: 0moderation_banned_terms: 0, profiles: 00→0
social.search_profilesextra0.9~2.9 → 1.4~4.645 → 45profiles: 39, user_follows: 5, user_blocks: 1 +1profiles: 39, user_follows: 5, user_blocks: 1 +10→0
social.feed_pageextra583.6~612.7 → 611.8~653.93,700 → 3,700user_follows: 45, content_reports: 20, user_blocks: 0 +3user_follows: 45, content_reports: 20, user_blocks: 0 +313→1265쪽·1300건·중복 0·누락 0/1300
social.following_feedextra0.0~0.0 → 0.0~0.00 → 0user_follows: 0, user_blocks: 0, user_suspensions: 0user_follows: 0, user_blocks: 0, user_suspensions: 00→0
social.commentsowner2.8~4.4 → 2.3~3.817 → 17profiles: 4, user_suspensions: 0, content_takedowns: 0 +3profiles: 4, user_suspensions: 0, content_takedowns: 0 +33→3
social.following_listextra0.8~1.3 → 0.7~0.813 → 13user_follows: 5, user_blocks: 1, user_suspensions: 0user_follows: 5, user_blocks: 1, user_suspensions: 01→1
social.moderation_stateowner0.2~0.4 → 0.2~0.30 → 0user_suspensions: 0user_suspensions: 00→0
social.can_viewextra0.3~0.4 → 0.3~0.40 → 0user_follows: 0, user_blocks: 0, user_suspensions: 0user_follows: 0, user_blocks: 0, user_suspensions: 00→0
group.loungeowner41.3~86.8 → 71.9~93.3334 → 334profiles: 111, group_members: 21, groups: 1 +5profiles: 111, group_members: 21, groups: 1 +51→1
group.boardowner1.2~2.6 → 1.1~2.41 → 1group_members: 1, user_suspensions: 0group_members: 1, user_suspensions: 01→1
group.my_groupsowner1.5~2.8 → 1.8~2.627 → 27group_members: 21, profiles: 4, groups: 2 +1group_members: 21, profiles: 4, groups: 2 +12→2
catalog.fullowner2480.9~2843.5 → 2260.8~2373.44,870 → 4,870exercises: 803exercises: 8034→4
catalog.changesowner1834.1~2032.3 → 1891.0~2180.84,759 → 4,759exercises: 893, exercise_catalog_changes: 813, pr_overview_projection_control: 1exercises: 893, exercise_catalog_changes: 813, pr_overview_projection_control: 14→4
catalog.search_signalsowner0.8~2.0 → 0.7~1.526 → 65user_exercise_stats: 26, exercise_usage_stats: 0exercise_usage_stats: 39, user_exercise_stats: 260→0
admin.is_adminowner0.4~0.6 → 0.5~0.70 → 0lift_guild_admins: 0lift_guild_admins: 00→0
admin.search_usersadmin1.1~1.7 → 1.0~1.7103 → 103users: 51, profiles: 51, lift_guild_admins: 1users: 51, profiles: 51, lift_guild_admins: 10→0
admin.content_reportsadmin7.2~8.7 → 5.8~6.6101 → 101profiles: 40, content_reports: 40, lift_guild_admins: 1 +5profiles: 40, content_reports: 40, lift_guild_admins: 1 +53→3
import.staging_rows_by_batchpostgres0.3~0.6 → 0.3~0.52,000 → 2,000없음없음1→1
import.staging_sets_by_sessionpostgres0.3~0.5 → 0.2~0.34 → 4wodup_import_sets: 4wodup_import_sets: 40→0
import.batches_by_ownerowner0.1~0.1 → 0.1~0.21 → 1wodup_import_batches: 1wodup_import_batches: 10→0
home.dashboardowner30.4~81.5 → 36.7~76.13,474 → 3,474user_calendar_day_summaries: 2,609, exercise_business_role_memberships: 0, user_exercise_pr_events: 0 +1user_calendar_day_summaries: 2,609, exercise_business_role_memberships: 0, user_exercise_pr_events: 0 +118→15
calendar.monthowner11.9~18.8 → 16.4~24.25,518 → 5,527user_calendar_day_summaries: 2,609, daily_conditions: 0user_calendar_day_summaries: 2,609, daily_conditions: 011→9
calendar.dayowner94.6~128.1 → 170.4~214.64,347 → 38,377strength_estimation_curve_points: 1,296, user_exercise_pr_events: 88, strength_estimation_curve_versions: 36 +4strength_estimation_curve_points: 1,296, user_exercise_pr_events: 88, strength_estimation_curve_versions: 36 +517→15
workout.session_detailowner42.7~60.2 → 42.8~67.71,546 → 1,546strength_estimation_curve_points: 1,296, user_exercise_pr_events: 88, strength_estimation_curve_versions: 36 +4strength_estimation_curve_points: 1,296, user_exercise_pr_events: 88, strength_estimation_curve_versions: 36 +412→11
workout.search_pageowner2660.3~2819.7 → 3123.6~3609.521,361 → 21,361user_exercise_pr_events: 960, user_follows: 0, user_blocks: 0 +1user_exercise_pr_events: 960, user_follows: 0, user_blocks: 0 +110→922쪽·2609건·중복 0·누락 0/2609
pr.overviewowner0.0~0.0 → 0.0~0.02 → 2profiles: 1, user_pr_overview_snapshot_headers: 0, exercise_business_role_memberships: 0profiles: 1, user_pr_overview_snapshot_headers: 0, exercise_business_role_memberships: 03→3
pr.exercise_detailowner25.9~39.9 → 30.5~42.211,476 → 11,476session_exercise_part: 3,609, user_exercise_pr_events: 1, user_exercise_pr_states: 0 +1session_exercise_part: 3,609, user_exercise_pr_events: 1, user_exercise_pr_states: 0 +17→7
pr.records_pageowner1.6~3.7 → 1.5~2.45 → 5user_exercise_pr_events: 1user_exercise_pr_events: 12→21쪽·0건·중복 0
volume.overviewowner447.5~498.6 → 483.3~592.057,143 → 57,143session_exercise_part: 15,627, user_exercise_max_rep_observations: 12,040, user_training_period_stats: 3,304 +5session_exercise_part: 15,627, user_exercise_max_rep_observations: 12,040, user_training_period_stats: 3,304 +58→8
write.delete_set_part_cascadepostgres16.7~29.5 → 5.8~13.3117 → 117users: 106, user_exercise_records: 0, user_exercise_session_rollups: 0 +3users: 1067→13
write.delete_session_cascadepostgres189.3~219.1 → 41.6~75.6246 → 248users: 128, user_fact_history: 30, user_exercise_records: 9 +8users: 128, user_fact_history: 30, content_reports: 1 +322→32
write.delete_account_cascadepostgres36293.6~36293.6 → 8373.8~8373.815,666 → 17,257user_fact_history: 1,994, body_metrics: 20, group_chat_messages: 20 +7user_fact_history: 2,058, user_follows: 38, users: 1 +415→16

판정

  • 쓰기 경로(목표) — 세트 1개 삭제: 투영 표 5개 seq scan → 0, 버퍼 3,394 hit + 3,793 read → 160 hit(45배 감소), 17~30ms → 6~13ms. 세션 삭제: 버퍼 96,710 → 3,280(30배), 190~220ms → 42~76ms. 계정 삭제 연쇄(1년 이력): 버퍼 2,950만 → 808만, 중첩 문장 14,880 → 3,763, 36.3초 → 8.4초. 남은 큰 몫은 user_fact_history(원본 이력 표) seq scan 2,058행 — 보호 트리거가 행마다 table_name, row_id 로 마지막 이력을 찾는 경로는 인덱스(user_fact_history_row_idx)가 있고, seq scan 은 계정 삭제 FK cascade 의 owner_user_id 검사(인덱스 유지)와 이력 삽입 쪽이라 D13/D02 인계.
  • 읽기 경로(회귀 검사) — 인덱스 29개를 지운 뒤 seq scan 이 새로 생긴 조회 없음(calendar.daysession: 2 는 계획 세션 조인의 2행). calendar.day 의 "읽은 행" 4,347 → 38,419 는 실제 일이 늘어난 것이 아니라 계수 방식 차이다: 달력 함수 2곳(decorate_calendar_planned_sessions_v1·기록표 칸 상세)이 session.id::text = item->>'id' 로 조인해 기본키를 못 쓰고 user_id 로 사용자의 세션 2,621행을 전부 훑는데, 전에는 지운 session_user_id_idx 의 Index Scan(필터 뒤 1행으로 계수), 후에는 session_user_date_status_idx 의 Bitmap Index Scan(필터 전 2,621행으로 계수)이 골라졌다. 버퍼 5,718 → 6,408(+12%), 조용한 재측정 91~113ms(전 95~128ms). 근본 수리는 (item->>'id')::uuid = session.id 로 바꿔 기본키를 쓰는 것 — 함수 소유자(D06 홈/달력) 인계.
  • 화면 함수 안의 큰 읽기(인계)get_exercise_catalog 종목 803행 seq scan + 정렬 804회(2.3~2.8초/822KB) 와 get_exercise_catalog_changes(813행 정렬 506회) 는 A03(카탈로그 동기화), get_session_search 첫 쪽 읽은 행 21,361·정렬 1,080(2.7~3.1초/491KB, PR 이벤트 960행 seq scan) 과 get_volume_overview 읽은 행 57,143(세션 종목 15,627 seq scan·최대 반복수 관측 12,040) 은 D04/D08, get_profile_feed 첫 쪽 584~654ms/86KB(정렬 181회) 는 D07 — 실행계획·버퍼 수치를 인계 댓글에 붙였다. 페이지 조회 3종(피드 65쪽·검색 22쪽·PR 기록)은 마지막 쪽까지 중복 0·누락 0. <!-- d12-explain:end -->

4-3. 추가 인덱스의 쓰기 비용

<!-- d12-write-cost:begin --> 투영 표에 더한 FK 참조 열 인덱스 15개(그리고 지운 중복 인덱스 7개)가 투영 재작성에 얹는 비용을 같은 샌드박스에서 쟀다: 10년 이력 사용자 1명의 통계 투영 전량 재계산(refresh_user_exercise_stats_from_base, 트랜잭션 안에서 실행 뒤 롤백, WAL 은 pg_wal_lsn_diff).

WAL시간
전(새 인덱스 15개 없음 + 중복 인덱스 7개 있음)28.4 MB1,820 ms
후(새 인덱스 15개 + 중복 7개 제거)37.5 MB1,551 ms
  • 전량 재계산 1회당 WAL +9.1MB(+32%). 시간은 줄었다(참조 검사가 인덱스를 탐). 이것은 최악 사례(10년 이력 전량)이고, 매분 worker 는 바뀐 날짜부터의 증분만 다시 쓰므로 회당 증가분은 훨씬 작다. 읽기 개선(세트 삭제 45배·세션 삭제 30배·계정 삭제 4.3배)과 저장 지연(세트 삭제 1개당 10~17ms 절감 × 저장당 지운 세트 수)이 사용자가 느끼는 쪽이라 채택. WAL 예산(G04 기준선 cron WAL 46GB/20일)에 이 증가분이 얹히는 것은 R05 릴리스 리허설에서 다시 재고, 넘으면 관측 표 2개(user_exercise_strength_observations·user_exercise_max_rep_observations, 인덱스 25MB·5.7MB) 의 FK 인덱스 4개를 먼저 되돌린다(되돌리기 = drop, 저장 경로는 나머지 11개로 대부분 덮인다).
  • populated upgrade(4년 이력 fixture, D13 harness): 마이그레이션 적용 337ms · WAL 1.07MB · 잠금 대기 0 · 옛 앱 프로브 오류 0/15 · 중단(37/74 문장) 뒤 재실행 사실 보존 · 구/신 공존 break 0 — 마이그레이션 머리의 -- upgrade-evidence: 줄.
  • 투영 표 인덱스 크기(후, 샌드박스): user_exercise_period_stats 45MB(재작성 churn 으로 부푼 것 — VACUUM/REINDEX 는 운영 결정, R05)·user_exercise_strength_observations 25MB·user_exercise_session_rollups 7.7MB·user_exercise_records 6.1MB·user_exercise_max_rep_observations 5.7MB·user_training_period_stats 2.6MB·user_exercise_strength_daily 2.2MB·user_exercise_stats 96KB·user_exercise_strength_states 56KB. <!-- d12-write-cost:end -->