ES /docs

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'capture_ids' in 'field list'

RCA: ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column in field list

Overview#

What Happened#

tesla (cupixworks-api) 의 entity 검색 인덱싱 경로에서 Record.where(id: ...).pick(:name) 쿼리가 실행되는데, records 테이블에는 name 컬럼이 존재하지 않아 Mysql2::Error: Unknown column 'name' in 'field list' 가 매번 발생한다. 예외는 상위 rescue StandardError 에서 즉시 nil 로 삼켜지므로(error.handling: handled) 사용자에게는 실패가 노출되지 않지만, record entity 를 재인덱싱할 때마다 실패하는 SQL 이 전송되어 APM 이 error span 으로 기록한다. 최근 1시간 표본에서 errored mysql2 span 2050+ 건이 전부 이 쿼리였다.

Representative Error 는 STALE 하다. 클러스터에 고정된 대표 메시지는 Unknown column 'capture_ids' (first_seen 2025-04-16) 이지만, Error Tracking 이 동일 fingerprint family(Unknown column 'X' in 'field list') 의 여러 변종을 한 이슈로 묶으면서 오래된 샘플을 pin 한 것이다. last_seen 시점의 실제 발생 메시지는 Unknown column 'name' 이며, capture_ids 변종은 현재 발생하지 않는다.

Quick Facts#

Field Value
exception.class ActiveRecord::StatementInvalid (cause: Mysql2::Error)
exception.message Mysql2::Error: Unknown column 'name' in 'field list' (현재), 대표값 Unknown column 'capture_ids' 는 stale
SQL SELECT name FROM records WHERE records.id = ? LIMIT ?
top_frame app/models/concerns/entity_indexable.rb:155 (_ancestor_namepick(:name))
error.handling handled (rescue StandardError → nil)
runtime ruby 3.3.0, activerecord-7.2.2, mysql2-0.5.4
deploy records ancestor 인덱싱 도입 커밋 75616e02b / 5e8521b9f (TSLA-1895)
env production, us-west-2 (tesla_production)

Affected Teams#

Team / Domain Error Count Impact
tesla / cupixworks-api (entity search indexing) 최근 1h 표본 2050+ span (전량 동일 쿼리) 사용자 영향 없음 (rescue 로 삼켜짐). record entity 재인덱싱마다 실패 SQL round-trip + APM error span 노이즈

Timeline#

  1. 2025-04-16 11:16 KST — 클러스터 first_seen. 대표 메시지 Unknown column 'capture_ids' (현재는 미발생하는 stale 변종).
  2. 2026-05-27 16:07 KST — TSLA-1895 로 unified entity search / ancestry 인덱싱 도입 (75616e02b). _ancestor_nameklass.where(id:).pick(:name) 추가.
  3. 2026-06-01 09:42 KST — TSLA-1895 후속 (5e8521b9f)에서 _add_ancestor(h, :record, :record_id, cache) 로 record ancestor 추가. records 테이블에 name 컬럼이 없어 Unknown column 'name' 발생 시작.
  4. 2026-08-04 18:55 KST — APM span 증거: SELECT name FROM records WHERE records.id = ? LIMIT ?Mysql2::Error: Unknown column 'name' in 'field list', error.handling: handled.
  5. 2026-08-04 16:38 KST — 클러스터 last_seen.

Error Log#

Datadog Logs

text
Mysql2::Error: Unknown column 'capture_ids' in 'field list'

주의: 위는 클러스터에 고정된 stale representative 이다. last_seen 시점의 실제 발생 메시지는 아래 APM span (Log Evidence 참조):

text
Mysql2::Error: Unknown column 'name' in 'field list'
SQL: SELECT name FROM records WHERE records.id = ? LIMIT ?

Impact#

  • Service: cupixworks-mysql2 (APM mysql2 instrumentation span; 실제 앱은 tesla / cupixworks-api)
  • 발생 횟수: 39131929 (Error Tracking 누적, 여러 Unknown column 변종 포함)
  • 최초 발생: 2025-04-16 11:16 KST (stale capture_ids 변종 기준)
  • 최근 발생: 2026-08-04 16:38 KST (name 변종)

Root Cause Summary#

tesla 의 EntityIndexable concern 이 entity 검색 문서를 만들 때 조상(ancestor) 이름을 조회한다. _entity_ancestry (entity_indexable.rb:108-120) 는 _add_ancestor(h, :record, :record_id, cache) 를 호출하고, 이는 _ancestor_name (entity_indexable.rb:149-158) 에서 key.to_s.camelize.safe_constantizeRecord 클래스를 얻은 뒤 Record.where(id: ancestor_id).pick(:name) 를 실행한다. 그러나 records 테이블에는 name 컬럼이 없어(schema 확인: note/description/uuid 는 있으나 name 없음) MySQL 이 Unknown column 'name' in 'field list' 를 반환한다. _ancestor_name 은 이 예외를 rescue StandardError 로 잡아 nil 을 반환하므로(error.handling: handled) 인덱싱은 정상 진행되고 사용자 영향은 없다. 다만 record entity 가 재인덱싱될 때마다 실패하는 SQL 이 DB 로 전송되어 mysql2 APM span 이 error 로 태깅되고, 이것이 Error Tracking 에 대량 누적된다. :capture ancestor 는 captures 테이블에 name 컬럼이 있어 정상이며, 문제는 name 컬럼이 없는 :record(그리고 name 없는 다른 조상) 에 국한된다.

Technical Analysis#

Code Path#

  • Entry point: entity 인덱싱(save/update 후 재인덱싱 콜백) → _entity_ancestry
  • Failure point: app/models/concerns/entity_indexable.rb:155 pick(:name)

_entity_ancestry 가 조상별로 _add_ancestor 를 호출한다. :recordrecords 테이블을 가리킨다.

app/models/concerns/entity_indexable.rb:108-120ruby
  def _entity_ancestry(cache = nil)
    h = {}
    _add_ancestor(h, :team, :team_id, cache)
    _add_ancestor(h, :workspace, :workspace_id, cache)
    _add_facility_ancestor(h, cache)
    _add_ancestor(h, :record, :record_id, cache)   # records 테이블엔 name 컬럼 없음
    _add_ancestor(h, :capture, :capture_id, cache)
    _add_ancestor(h, :level, :level_id, cache)
    _add_ancestor(h, :bim, :bim_id, cache)
    _add_ancestor(h, :review, :review_id, cache)
    _add_ancestor(h, :annotation_layer, :annotation_layer_id, cache)
    h
  end

_ancestor_name 이 조상 클래스를 constantize 한 뒤 pick(:name) 로 이름 컬럼만 조회한다. key=:record 이면 klass=Record 가 되고, pick(:name)SELECT name FROM records WHERE records.id = ? LIMIT 1 을 생성한다.

app/models/concerns/entity_indexable.rb:149-158ruby
  def _ancestor_name(key, ancestor_id, cache = nil)
    return cache.ancestor_name(key, ancestor_id) if cache

    klass = key.to_s.camelize.safe_constantize
    return nil unless klass

    klass.where(id: ancestor_id).pick(:name)   # records 에 name 없음 → Unknown column 'name'
  rescue StandardError
    nil                                         # 예외를 삼킴 → error.handling: handled
  end

기대 동작: 조상 record 의 name 을 읽어 entity 문서에 넣는다. 실제 동작: records 테이블에 name 컬럼이 없어 MySQL 이 즉시 Unknown column 'name' 을 반환하고, rescue StandardError 가 nil 을 반환한다. entity 문서에는 record 이름이 항상 누락되지만 인덱싱 자체는 실패하지 않는다.

records 테이블 스키마 — name 컬럼 부재 (발췌):

db/schema.rb (create_table ruby
    t.text   "description"
    t.string "note"
    t.string "uuid"
    # ... t.string "name" 은 존재하지 않음

대조군 — captures 테이블에는 name 컬럼이 있어 :capture ancestor 는 정상:

db/schema.rb (create_table ruby
    t.string "name"

Log Evidence#

status:error 레벨 로그는 없다 (예외가 rescue 되어 애플리케이션 로그로 남지 않음). 증거는 mysql2 APM instrumentation span 이다. 사용한 Datadog spans 쿼리:

text
service:cupixworks-mysql2 status:error

last_seen 근처(2026-08-04) 표본 span 전체가 동일한 실패 SQL 이었다:

text
2026-08-04T09:55:05.123Z cupixworks-mysql2 | SELECT name FROM records WHERE records . id = ? LIMIT ?
2026-08-04T09:55:05.092Z cupixworks-mysql2 | SELECT name FROM records WHERE records . id = ? LIMIT ?
2026-08-04T09:55:04.923Z cupixworks-mysql2 | SELECT name FROM records WHERE records . id = ? LIMIT ?

span custom attributes (원문):

json
{
  "base_service": "cupixworks-api",
  "component": "active_record",
  "db": {
    "instance": "tesla_production",
    "statement": "SELECT name FROM records WHERE records . id = ? LIMIT ?",
    "system": "mysql2"
  },
  "env": "production",
  "error": {
    "file": "/var/app/current/vendor/bundle/ruby/3.3.0/gems/mysql2-0.5.4/lib/mysql2/client.rb",
    "handling": "handled",
    "message": "Mysql2::Error: Unknown column 'name' in 'field list'",
    "stack": "... Mysql2::Error: Unknown column 'name' in 'field list' (ActiveRecord::StatementInvalid) ..."
  }
}

빈도 확인 — 최근 1시간 errored mysql2 span 표본(약 2050건)이 전부 이 쿼리:

text
service:cupixworks-mysql2 status:error   (from now-1h)
→ { "SELECT name FROM records WHERE records . id = ? LIMIT ?": 2050 }

Unknown column 'capture_ids' 대표 메시지 재검색은 로그/스팬 모두 0건:

text
"Unknown column 'capture_ids'"        → 0 spans
service:cupixworks-mysql2 "Unknown column" (log)  → 0 logs (rescue 되어 로그 없음, span 만 존재)

이는 대표 메시지가 stale 하고 실제 현재 발생은 name 변종임을 확정한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 현재 발생 메시지는 대표값 capture_ids 가 아니라 name 이며, records 테이블에 name 컬럼이 없어 pick(:name) 이 실패한다 last_seen 근처 span 전량이 SELECT name FROM records ... + error.message: Unknown column 'name'; entity_indexable.rb:155 pick(:name); schema 에 records.name 부재 Confirmed
H2 RevisionRequestcapture_ids 를 컬럼처럼 쿼리하여 Unknown column 'capture_ids' 발생 (대표 메시지 그대로) 대표 메시지에 capture_ids 명시 capture_idsProperties::RevisionRequest 의 가상 attribute(sys[:capture_ids], Metable serialize)로 DB 컬럼 아님; schema/migration 에 capture_ids 컬럼 없음; span/log 검색 0건 Rejected
H3 사용자 요청을 500 으로 실패시키는 기능 버그 대량 발생 error.handling: handled (rescue StandardError → nil); status:error 애플리케이션 로그 0건; 인덱싱은 계속 진행됨 Rejected
H4 외부 의존성/인프라 장애 status-board scope svc:cupixworks-mysql2::unknown, active null / recent 없음; MySQL 자체는 정상 (schema 오류일 뿐) Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/entity_indexable.rb:155_ancestor_namename 컬럼이 없는 클래스에 대해 pick(:name) 을 호출하지 않도록 한다. 접근 방식(둘 중 하나):
    • klass.column_names.include?('name') (또는 klass.has_attribute?('name')) 로 가드한 뒤에만 pick(:name) 실행. 없으면 nil 반환.
    • recordsname 이 없는 모델은 표시용 이름으로 다른 컬럼(예: records 의 경우 note/uuid 여부는 제품 요구사항 확인 필요)을 쓰거나 ancestor name 을 아예 생략. 근거: 예외는 삼켜지지만 record entity 재인덱싱마다 실패 SQL round-trip 이 발생해 DB 부하와 APM error span 노이즈를 만든다. 컬럼 존재 여부는 정적으로 알 수 있으므로 DB 왕복 자체를 없애는 것이 정답이다.

단기 개선 (1주 이내)#

  • _entity_ancestry (entity_indexable.rb:108-120) 의 조상 목록 중 대상 테이블에 name 컬럼이 없는 조상(:record 등)을 점검하고, 이름이 필요 없다면 _add_ancestor 에서 name 조회를 건너뛰도록 정리한다.
  • batch preload 경로(5e8521b9f, TSLA-1895 의 cache 버전 cache.ancestor_name)도 동일한 컬럼 부재 문제를 갖는지 확인 — cache 를 채우는 preload 쿼리도 records.name 을 select 하면 같은 오류가 난다.

장기 개선 (재발 방지)#

  • APM mysql2 instrumentation 이 rescue 된 예외까지 error span 으로 태깅하므로, "handled" span 은 alarm/트래킹에서 필터하거나, 애초에 실패가 예상되는 쿼리를 코드에서 제거한다.
  • ancestor name 조회를 컬럼 메타데이터 기반으로 일반화(예: 모델별 display_name_column 선언)하여 신규 조상 추가 시 동일 실수를 방지한다.

Monitoring#

record.name 실패 SQL 재발을 추적하는 timeseries 쿼리:

text
service:cupixworks-mysql2 status:error @db.statement:"SELECT name FROM records WHERE records . id = ? LIMIT ?"

entity 인덱싱 관련 mysql2 error span 전반 추적:

text
service:cupixworks-mysql2 status:error @component:active_record

Risk Assessment#

  • Risk level: low (사용자 영향 없음, rescue 로 삼켜짐 — 다만 DB round-trip / APM 노이즈 지속)
  • 예상 복잡도: trivial (컬럼 존재 가드 추가 또는 name 조회 생략)

Noise Verdict#

bug — records 테이블에 없는 name 컬럼을 pick(:name) 로 조회하는 명백한 schema/SQL 불일치 결함으로, rescue 로 삼켜지지만 record 재인덱싱마다 실패 SQL 을 계속 전송하므로 코드 수정(컬럼 가드 또는 name 조회 생략)이 필요하다.