diff --git a/backend/src/main/java/com/shadowfit/service/exercise/PoseDataService.java b/backend/src/main/java/com/shadowfit/service/exercise/PoseDataService.java
index ca027905..65b86e9b 100644
--- a/backend/src/main/java/com/shadowfit/service/exercise/PoseDataService.java
+++ b/backend/src/main/java/com/shadowfit/service/exercise/PoseDataService.java
@@ -21,6 +21,7 @@
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -87,8 +88,19 @@ public class PoseDataService {
* 저장 전 다운샘플(위치 B: Spring, pose-ingest-downsampling.md §3-B) — 라이브 분석
* (DTW·sync·rep 감지)은 이 저장 이전 FastAPI에서 이미 끝난 값이라 저장본을 줄여도 영향 없고,
* 영향받는 건 리포트 시계열 해상도뿐(같은 문서 §1 안전판).
+ *
+ *
격리수준 READ COMMITTED (#276, 2026-09-24 사용자 confirm). 기본 RR 에서는 중복 키
+ * 한 건(재전송이 원본과 겹칠 때)이 {@code PRIMARY} 의 파티션 끝(supremum)에 X 락을 잡아, 커밋까지
+ * 같은 파티션의 모든 신규 삽입을 세우고 두 재전송이 겹치면 데드락이 된다. RC 에서는 그 락이
+ * 안 생긴다 — 결정적 재현과 동시 부하(워커 8, 중복)에서 RR 45.9% → RC 0/960
+ * ({@code loadtest/results/r276-lock-trace-2026-09-24/}). 중복 검사가 uk 원본 레코드에 잡는
+ * next-key 락은 RC 에서도 남아 한 방향 대기는 생길 수 있다 — 세션 키가 {@code session_id}
+ * 로 묶여 있어 순환이 안 닫힌다는 것이 근거이고, 한 트랜잭션이 여러 세션 키를 섞게 되면 다시 볼 것.
+ * 아래 세션 조회는 잠금 없는 읽기 한 번이라 RC 로 바뀌어도 보장이 달라지지 않는다.
+ * 데드락 재시도({@code ExerciseGrpcService})는 다른 원인에 대한 그물로 그대로 둔다.
+ * 분기와 기각된 대안: docs/decisions/r276-lock-root-cause-fix.md
*/
- @Transactional
+ @Transactional(isolation = Isolation.READ_COMMITTED)
public void savePoseDataBatch(Long sessionId, List grpcList) {
if (grpcList == null || grpcList.isEmpty()) return;
diff --git a/backend/src/test/java/com/shadowfit/service/exercise/PoseDataResendDeadlockRaceTest.java b/backend/src/test/java/com/shadowfit/service/exercise/PoseDataResendDeadlockRaceTest.java
new file mode 100644
index 00000000..97a33459
--- /dev/null
+++ b/backend/src/test/java/com/shadowfit/service/exercise/PoseDataResendDeadlockRaceTest.java
@@ -0,0 +1,166 @@
+package com.shadowfit.service.exercise;
+
+import com.shadowfit.grpc.PoseDataRequest;
+import com.shadowfit.model.exercise.Category;
+import com.shadowfit.model.exercise.Exercise;
+import com.shadowfit.model.exercise.Session;
+import com.shadowfit.model.exercise.Status;
+import com.shadowfit.model.member.Member;
+import com.shadowfit.model.member.UserRole;
+import com.shadowfit.repository.exercise.CategoryRepository;
+import com.shadowfit.repository.exercise.ExercisesRepository;
+import com.shadowfit.repository.exercise.SessionRepository;
+import com.shadowfit.repository.member.MemberRepository;
+import com.shadowfit.support.MySqlContainerSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.dao.PessimisticLockingFailureException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 재전송이 원본과 겹칠 때의 데드락 (#276) — {@code savePoseDataBatch} 를 RC 로 돌린 처방의 회귀 가드.
+ *
+ * 무엇을 고정하나. 기본 RR 에서는 중복 키 한 건이 {@code PRIMARY} 의 파티션 끝(supremum)에
+ * X 락을 잡고, 서로 다른 세션의 재전송이 동시에 겹치면 그 X 를 둘이 쥔 채 서로의 insert intention 을
+ * 기다려 데드락이 된다(결정적 재현: {@code loadtest/results/r276-lock-trace-2026-09-24/}). 같은 모양을
+ * 서비스 경로로 건다 — 세션 {@value #SESSIONS} 개가 각자 같은 배치를 {@value #RESENDS} 번(첫 번만 신규,
+ * 나머지는 전부 중복) 동시에 보낸다.
+ *
+ *
왜 «0» 을 단언할 수 있나. 같은 모양의 SQL 판에서 RR 은 문장당 약 46%, RC 는 0/960 이었다.
+ * 이 테스트는 {@value #SESSIONS} × {@value #RESENDS} 번을 부르므로 RR 로 되돌리면 사실상 반드시 걸린다
+ * (2026-09-24, 격리수준을 빼고 돌려 실패하는 것을 확인했다). 데드락 재시도는 {@code ExerciseGrpcService}
+ * 에 있고 이 테스트는 서비스를 직접 부르므로, 여기서 잡히는 것은 재시도로 가려지기 전의 데드락이다.
+ *
+ *
H2 는 이 잠금을 재현하지 못한다 — 실 MySQL({@link MySqlContainerSupport})에서만 돈다.
+ */
+@SpringBootTest
+@ActiveProfiles("race")
+@DisplayName("pose 재전송 데드락 (#276)")
+class PoseDataResendDeadlockRaceTest extends MySqlContainerSupport {
+
+ private static final int SESSIONS = 8;
+ private static final int RESENDS = 20;
+ private static final int FRAME_COUNT = 50; // DOWNSAMPLE_WINDOW=5 → 10행
+
+ @Autowired private PoseDataService poseDataService;
+ @Autowired private ExercisesRepository exercisesRepository;
+ @Autowired private CategoryRepository categoryRepository;
+ @Autowired private MemberRepository memberRepository;
+ @Autowired private SessionRepository sessionRepository;
+ @Autowired private JdbcTemplate jdbcTemplate;
+
+ private Long memberId;
+ private final List sessionIds = new ArrayList<>();
+
+ @AfterEach
+ void tearDown() {
+ for (Long id : sessionIds) {
+ jdbcTemplate.update("DELETE FROM pose_data WHERE session_id = ?", id);
+ jdbcTemplate.update("DELETE FROM exercise_sessions WHERE id = ?", id);
+ }
+ if (memberId != null) {
+ jdbcTemplate.update("DELETE FROM users WHERE id = ?", memberId);
+ }
+ }
+
+ @Test
+ @DisplayName("서로 다른 세션의 중복 재전송이 동시에 겹쳐도 데드락이 나지 않고, 멱등은 유지된다")
+ void concurrentDuplicateResendsDoNotDeadlock() throws Exception {
+ seedSessions();
+ List batch = frames();
+
+ ExecutorService pool = Executors.newFixedThreadPool(SESSIONS);
+ CountDownLatch start = new CountDownLatch(1);
+ AtomicInteger deadlocks = new AtomicInteger();
+ ConcurrentLinkedQueue others = new ConcurrentLinkedQueue<>();
+
+ for (Long sessionId : sessionIds) {
+ pool.submit(() -> {
+ start.await();
+ for (int i = 0; i < RESENDS; i++) {
+ try {
+ poseDataService.savePoseDataBatch(sessionId, batch);
+ } catch (PessimisticLockingFailureException e) { // 데드락·락 대기 초과의 공통 조상
+ deadlocks.incrementAndGet();
+ } catch (Throwable t) {
+ others.add(t);
+ }
+ }
+ return null;
+ });
+ }
+ start.countDown();
+ pool.shutdown();
+ assertThat(pool.awaitTermination(120, TimeUnit.SECONDS)).as("시간 안에 끝나야 한다").isTrue();
+
+ assertThat(others).as("데드락 외 예외는 없어야 한다").isEmpty();
+ assertThat(deadlocks.get())
+ .as("RR 로 되돌리면 중복 한 건이 파티션 끝을 잠가 여기서 걸린다 — %d/%d",
+ deadlocks.get(), SESSIONS * RESENDS)
+ .isZero();
+
+ for (Long sessionId : sessionIds) {
+ Integer rows = jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM pose_data WHERE session_id = ?", Integer.class, sessionId);
+ assertThat(rows).as("세션 %d: 재전송 %d번이 한 벌로 접혀야 한다", sessionId, RESENDS)
+ .isEqualTo(FRAME_COUNT / 5);
+ }
+ }
+
+ private void seedSessions() {
+ Member member = memberRepository.saveAndFlush(Member.builder()
+ .email("resend-deadlock@test.com").username("재전송데드락").password("dummy")
+ .role(UserRole.USER).build());
+ memberId = member.getId();
+
+ Category category = categoryRepository.findByName("LOWER")
+ .orElseGet(() -> categoryRepository.save(Category.builder().name("LOWER").build()));
+ Exercise exercise = exercisesRepository.saveAndFlush(Exercise.builder()
+ .name("스쿼트").category(category).expectedDurationMinutes(15)
+ .syncThresholdBeginner(new BigDecimal("60.00"))
+ .syncThresholdAdvanced(new BigDecimal("85.00"))
+ .build());
+
+ // 시작 시각을 같게 둔다 — created_at 이 세션 시작 시각이라 전부 같은 파티션(같은 달)에 들어간다.
+ // 동시에 운동 중인 사용자는 정의상 같은 달이므로 이것이 실사용 조건이다.
+ LocalDateTime startTime = LocalDateTime.now().withNano(0);
+ for (int i = 0; i < SESSIONS; i++) {
+ Session session = sessionRepository.saveAndFlush(Session.builder()
+ .member(member).exercise(exercise).startTime(startTime)
+ .status(Status.IN_PROGRESS).totalReps(0).difficultyLevel(1).build());
+ sessionIds.add(session.getId());
+ }
+ }
+
+ private List frames() {
+ List frames = new ArrayList<>();
+ for (int i = 0; i < FRAME_COUNT; i++) {
+ frames.add(PoseDataRequest.newBuilder()
+ .setTimestampSec(i * 0.1)
+ .setJointCoordinates("{}")
+ .setSyncRate(72.5)
+ .setRepNumber(1)
+ .setSmoothedKneeAngle(120.0)
+ .setFeedbackMessage("ok")
+ .build());
+ }
+ return frames;
+ }
+}
diff --git a/docs/18-testing-guide.md b/docs/18-testing-guide.md
index cb9ba164..5a1e804e 100644
--- a/docs/18-testing-guide.md
+++ b/docs/18-testing-guide.md
@@ -60,7 +60,8 @@ Windows PowerShell:
| Docker 없을 때 | — | `@Testcontainers(disabledWithoutDocker = true)` → **건너뜀**. 강제로 끄려면 `-Dmysql.container=false` |
쓰는 클래스: `FlywayMigrationValidationTest`(마이그레이션↔엔티티 정합 — 이 프로파일이 존재하는 첫 번째 이유),
-`PoseDataOrphanRaceTest`(FK 없는 파티션 표), `SignupUsernameRaceTest`(벤더 제약명), `WeeklySummaryBLayerRaceTest`(`JSON_TABLE`).
+`PoseDataOrphanRaceTest`(FK 없는 파티션 표), `SignupUsernameRaceTest`(벤더 제약명), `WeeklySummaryBLayerRaceTest`(`JSON_TABLE`),
+`PoseDataResendDeadlockRaceTest`(InnoDB 잠금 — 재전송 데드락 #276, 격리수준 RC 의 회귀 가드).
`PoseDataOrphanWindowTest` 는 같은 프로파일이지만 **측정 장치**라 `-Dmeasure.orphan.window=true` 로만 돈다(4분+, 2026-09-11 에는
30스레드 구간이 InnoDB 데드락으로 끝났다 — 미검증, 박스가 다른 컨테이너와 동거 중이었다). 새 테스트가 «H2 로는 원리상 안 된다» 면 `MySqlContainerSupport` 를
상속하고 `@ActiveProfiles("race")` 를 붙인다 — 그 외에는 기본 프로파일에 둔다(컨테이너 기동 비용).
diff --git a/docs/architecture/ai-backend-changelog.md b/docs/architecture/ai-backend-changelog.md
index b62c5de1..c7899ddb 100644
--- a/docs/architecture/ai-backend-changelog.md
+++ b/docs/architecture/ai-backend-changelog.md
@@ -334,6 +334,12 @@
- **바꾼 것.** `AbstractOutboxPublisher.dispatchOne` 이 예외의 출처를 가른다 — `dispatch()` 예외 = RETRY(재시도 기록 + 백오프, 상한 초과 시 `onGivenUp` → `FAILED`), 결과 기록 단계 예외 = 예전대로 회수 대기. 예외 뒤 재시도 기록마저 실패하면 후자로 떨어져 행을 잃지 않는다.
- **결합면 영향.** `StopAnalysis`·`ReattachAnalysis` 경로는 gRPC 실패를 이미 RETRY 로 분류해 돌려주므로(예외를 안 던지는 계약) 보통 경로는 그대로다. 달라지는 건 그 분류 밖에서 새는 예외(DB 조회 실패·gRPC 밖 런타임 예외)뿐이다 — 이제 재시도 상한을 소진한다. 갱신 트리거 2번(전달 보장)·4번(실패 처리).
+### #276 — fix(pose): `savePoseDataBatch` 를 READ COMMITTED 로 (2026-09-24) ⭐ 실패 처리 변경, Spring 단독
+
+- **무엇이 문제였나.** AI 재전송(`spring_client.py`, 실패 시 3회)이 원본과 겹치면 RR 에서 중복 키 한 건이 `pose_data` 의 파티션 끝(`PRIMARY` supremum)에 X 락을 잡아, 서로 다른 세션의 재전송이 동시에 겹칠 때 데드락이 됐다(동시 부하 45.9%). 지금까지는 Spring 쪽 데드락 재시도(상한 5)가 가렸고, 그 소진분은 `INTERNAL` → AI 재전송으로 되돌아가는 **두 겹** 구조였다.
+- **바꾼 것.** 이 트랜잭션만 RC — supremum 락이 안 생긴다(0/960, [`loadtest/results/r276-lock-trace-2026-09-24/`](../../loadtest/results/r276-lock-trace-2026-09-24/README.md)). 데드락 재시도는 그물로 남긴다. 분기: [`../decisions/r276-lock-root-cause-fix.md`](../decisions/r276-lock-root-cause-fix.md)
+- **결합면 영향.** RPC·proto·응답 계약은 그대로다. 달라지는 것은 «재전송이 `INTERNAL` 을 받을 확률» — 측정한 모양(다세션 중복, 워커 8)에서는 데드락이 0 이었다 — 다른 동시성·다른 삽입 모양에서의 0 은 안 쟀다. 갱신 트리거 4번(실패 처리). AI 코드 변경 없음.
+
---
## 5. 결합 요소별 변경 시점
diff --git a/docs/decisions/r276-lock-root-cause-fix.md b/docs/decisions/r276-lock-root-cause-fix.md
new file mode 100644
index 00000000..6a15abd6
--- /dev/null
+++ b/docs/decisions/r276-lock-root-cause-fix.md
@@ -0,0 +1,83 @@
+# #276 근본 처방 — 재시도가 아니라 «중복 하나가 파티션 끝을 잠그는» 자리를 없앤다
+
+작성: 2026-09-24
+상태: **ㄴ(RC) 채택 · 적용 (2026-09-24 사용자 confirm)** — ㄷ(자연키 PK) 비용 측정 착수는 미결정
+근거: [`loadtest/results/r276-lock-trace-2026-09-24/`](../../loadtest/results/r276-lock-trace-2026-09-24/README.md)
+관련: [#276](https://github.com/Shadowfit/init/issues/276) · [`r276-retry-followup.md`](./r276-retry-followup.md) · [`pose-batch-idempotency-vs-partition.md`](./pose-batch-idempotency-vs-partition.md) · [`online-ddl-vs-blocking-alter.md`](./online-ddl-vs-blocking-alter.md)
+
+---
+
+## 0. 한 줄
+
+지금 처방(데드락 재시도 상한 5)은 **그물**이다. 09-24 결정적 재현이 **그물 아래의 자리**를 찾았다 —
+RR 에서 **중복 키 한 건**이 `PRIMARY` 의 파티션 끝(supremum)에 `X` 를 잡고, 커밋까지 **그 파티션의 모든 신규 삽입을 세운다.**
+데드락은 그 `X` 를 두 트랜잭션이 동시에 쥘 때 닫히는 결과일 뿐이다. 그 `X` 를 안 생기게 하는 후보가 둘 있고, 둘 다 동시 부하에서 0/960 이었다.
+
+## 1. 확인된 것 (측정)
+
+| 사실 | 근거 |
+|---|---|
+| supremum `X` 는 중복 **한 건**에서 생긴다(엇갈림 불필요) | trace base 단계 1 |
+| ODKU 고유가 아니다 — `INSERT IGNORE`·평범한 `INSERT`(1062) 도 같다 | trace insert_ignore · plain_insert |
+| 그 X 는 **무관한 세션의 신규 키**도 커밋까지 세운다 | trace dup_blocks_new |
+| RC 에서는 supremum 이 없다. 단 uk 원본 레코드의 next-key `X` 가 남아 **한 방향 대기**가 생긴다 | trace read_committed (3.26초 대기, 데드락 0) |
+| 멱등 키를 PK 로 하면 원본 레코드 `REC_NOT_GAP` 하나만 잡는다. id 를 보조 인덱스로 남겨도 같다 | trace natural_pk · natural_pk_keep_id |
+| 동시 부하(워커 8·중복): base **45.9%**, RC·자연키 PK 두 변형 **0/960**, 네 팔 모두 멱등 유지(200행) | concurrent.txt, 라틴 방격 3블록 |
+
+**미검증**: supremum `X` 가 «왜» 생기는지의 소스 수준 설명(가설 하나를 세웠다가 철회했다 — 결과 README §4).
+
+## 2. 후보
+
+### ㄱ. 현상 유지 — 재시도 상한 5
+
+- 비용 0. 08-26 에 확정된 값
+- 남는 것: 데드락은 계속 나고(앱 경로 상한 5 에서도 잔여 4.2%, 08-23), 재시도 루프 p99 약 4초.
+ 그리고 이번에 보인 **«중복 하나가 파티션 끝을 세운다»** 는 재시도와 무관하게 그대로다
+
+### ㄴ. 이 쓰기 경로만 READ COMMITTED
+
+`PoseDataService.savePoseDataBatch` 의 `@Transactional` 에 `isolation = READ_COMMITTED` 한 줄.
+
+| 얻는 것 | 치르는 것 / 모르는 것 |
+|---|---|
+| supremum `X` 가 사라진다 · 동시 부하 0/960 | **한 방향 대기는 남는다** — 재전송 트랜잭션이 커밋할 때까지 «다음 세션 원본 앞 gap» 에 들어갈 신규 삽입이 기다린다 |
+| 코드 한 줄 · 되돌리기 쉽다 · 스키마 불변 | 「순환이 안 생긴다」는 **세션 키가 `session_id` 로 묶여 있다는 논증**이다. 한 트랜잭션이 여러 세션 키를 섞는 경로가 생기면 안 선다 |
+| | 같은 트랜잭션 안의 세션 조회(`findById`)도 RC 가 된다 — 이 경로에선 무해해 보이나 **확인 안 함** |
+| | binlog 는 8.0 기본값 ROW 에 기대고 있다(설정 파일에 명시 없음). STATEMENT 로 바뀌면 RC 와 못 산다 |
+
+### ㄷ. 멱등 키를 PK 로 — `(session_id, rep_number, timestamp_sec, created_at)`, id 는 AUTO_INCREMENT 보조 인덱스로 유지
+
+| 얻는 것 | 치르는 것 / 모르는 것 |
+|---|---|
+| supremum 도, 한 방향 대기도 없다 — 원본 레코드 하나만 잠근다 · 동시 부하 0/960 | **1억 행급 파티션 표의 PK 재구성** — 테이블 재작성이다. 온라인으로 가능한지·걸리는 시간은 **안 쟀다**([`online-ddl-vs-blocking-alter.md`](./online-ddl-vs-blocking-alter.md) 의 축) |
+| `uk_pose_event` 가 PK 로 흡수돼 **인덱스 하나가 줄어든다** | PK 폭 12B → 21B — 보조 인덱스(`idx_session_timestamp`, `idx_pose_id`)가 그만큼 커진다. `idx_session_timestamp` 는 PK 앞부분과 겹쳐 **중복 인덱스가 될 수 있다**(검토 안 함) |
+| 행이 **세션 순으로 물리 정렬**된다 — 세션 단위 읽기(`findFramesBySessionId`)가 PK 범위 스캔이 될 수 있다 | 그 이득도 **안 쟀다.** 삽입이 파티션 끝 한 점이 아니라 **동시 세션 수만큼의 지점**으로 흩어진다 — 페이지 분할·버퍼풀 영향 미측정(uk-bufferpool 08-23 판이 «흩어진 삽입 −46%» 를 본 적 있다) |
+| id 를 남기므로 `reports.detailed_analysis` 의 `poseDataId` 참조가 산다 | id 로 찾는 조회(`PoseDataRepository.findJointCoordinatesById`)가 PK 다이브에서 **보조 인덱스 + PK 룩업**으로 바뀐다(행 1개라 작을 것 — 미측정). JPA 엔티티 `@Id` 변경 필요 |
+
+(id 를 아예 지우는 변형도 잠금은 같았지만, 저장된 `poseDataId` 가 깨지므로 후보에서 뺀다.)
+
+### ㄹ. 기존 키를 먼저 조회해 신규 행만 INSERT
+
+- 조회와 삽입 사이에 재전송이 끼면 같은 경로로 떨어진다 — 창을 좁힐 뿐 자리를 없애지 않는다. **측정 안 함.** 비교용으로만 적는다
+
+## 3. 추천 (결정 아님)
+
+**ㄴ 을 먼저, ㄷ 은 비용을 잰 뒤에.**
+
+- ㄴ 은 한 줄이고 되돌리기 쉽다. 측정으로 데드락 0/960 이 나왔고, 남는 한 방향 대기는 재전송 트랜잭션 길이(배치 하나, ms 단위)만큼이다
+- ㄷ 은 자리를 완전히 없애는 **구조적 답**이고 DB 이야기로는 가장 굵다(인덱스 설계·클러스터링·온라인 DDL). 하지만 치르는 것 네 줄이 전부 미측정이라,
+ 지금 채택하면 «근거 없는 결정» 이 된다. 착수한다면 순서는 ① 삽입 처리량(append vs 세션별 지점) ② PK 재구성 시간·온라인 여부 ③ 세션 단위 읽기 이득
+- 재시도(ㄱ)는 어느 쪽을 골라도 **남겨 둔다** — 다른 원인의 데드락에 대한 그물로서. 상한을 줄일지는 ㄴ/ㄷ 적용 뒤 `pose_batch_deadlock_retries` 로 본다
+
+## 4. 미결정 (사용자 confirm 필요)
+
+1. ~~ㄴ 을 적용할지~~ → ✅ 적용(아래 결정 로그)
+2. ㄷ 의 비용 측정(§3 순서)을 착수할지
+3. #276 에 이 판 결과를 코멘트로 남기고, 이슈 상태 블록을 갱신할지
+
+## 결정 로그
+
+- 2026-09-24 — 문서 작성. 결정 없음
+- 2026-09-24 — **ㄴ 채택 (사용자 confirm).** `PoseDataService.savePoseDataBatch` 에 `@Transactional(isolation = READ_COMMITTED)`.
+ 회귀 가드 `PoseDataResendDeadlockRaceTest`(실 MySQL, 세션 8 × 재전송 20): **RR 로 되돌리면 데드락 63/160 으로 실패, RC 에서 0 으로 통과**를 둘 다 확인했다.
+ 데드락 재시도(상한 5)는 그대로 둔다(§3)
diff --git a/loadtest/measure_r276_lock_trace.sh b/loadtest/measure_r276_lock_trace.sh
new file mode 100644
index 00000000..db061f2c
--- /dev/null
+++ b/loadtest/measure_r276_lock_trace.sh
@@ -0,0 +1,190 @@
+#!/usr/bin/env bash
+# #276 결정적 재현 — 두 세션을 한 문장씩 진행시키며 단계마다 performance_schema.data_locks 를 찍는다
+#
+# 이슈: https://github.com/Shadowfit/init/issues/276
+# 지금까지의 판(r276-*)은 전부 «확률» 이었다 — 워커 N 개를 풀어놓고 데드락 비율을 셌다.
+# 잠금 자리(PRIMARY supremum, 파티션 p2026_05)는 데드락 덤프 한 쌍으로만 봤고,
+# **어느 문장·어느 시점에 그 X 가 생기는지**는 미검증으로 남아 있었다(08-20 코멘트).
+# 이 rig 은 부하를 걸지 않는다. 세션 둘(t1·t2)에 문장을 하나씩 넣고, 넣을 때마다 제3의
+# 세션(root)이 잠금 표를 읽는다. 같은 입력이면 같은 출력이 나오므로 판 수가 필요 없다.
+#
+# 무대: 격리 컨테이너(mysql:8.0) 에 Flyway V1~V26 을 그대로 적용한 스키마.
+# 대상 표는 매 시나리오마다 `pose_lab` 을 `CREATE TABLE ... LIKE pose_data` 로 새로 만든다
+# (파티션·PK·uk_pose_event 전부 복제). 팔에 따라 키·격리수준·INSERT 형태만 바꾼다.
+#
+# 쓰는 법:
+# docker run -d --name r276-locktrace -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=shadowfit mysql:8.0
+# (V*.sql 을 버전 순으로 적용 — README 참고)
+# CONTAINER=r276-locktrace ./loadtest/measure_r276_lock_trace.sh > out.txt
+#
+# ⚠️ 단계 사이 간격은 SLEEP(기본 1초)이다. 막힌 문장은 innodb_lock_wait_timeout(기본 50초)
+# 안에 다음 단계가 풀어주므로 간격이 결과를 바꾸지 않는다 — 단 박스가 극단적으로 느리면
+# «막혔다» 와 «아직 안 끝났다» 를 구분 못 한다. 각 dump 의 LOCK_STATUS=WAITING 으로 판정한다.
+set -uo pipefail
+export MSYS_NO_PATHCONV=1 # Git Bash 가 docker exec 인자의 /tmp 를 Windows 경로로 바꾸지 않게
+
+CONTAINER=${CONTAINER:-r276-locktrace}
+SLEEP=${SLEEP:-1}
+ARMS=${ARMS:-"base dup_blocks_new plain_insert dup_only new_only no_uk insert_ignore read_committed natural_pk natural_pk_keep_id"}
+
+# 컨테이너 안에서 도는 드라이버. 인자: 시나리오 파일 경로(컨테이너 안).
+# 시나리오 줄 형식: T1|sql · T2|sql · ROOT|sql · DUMP|라벨
+read -r -d '' DRIVER <<'EOS'
+set -u
+SCEN=$1; SLEEP=$2
+cd /tmp && rm -f p1 p2 t1.log t2.log && mkfifo p1 p2
+# -n: 버퍼링 끔 · --force: 데드락 뒤에도 세션을 살려 둔다 · -vvv: 문장과 결과를 같이 남긴다
+mysql -ut1 -pp1 -n --force -vvv shadowfit t1.log 2>&1 &
+mysql -ut2 -pp2 -n --force -vvv shadowfit t2.log 2>&1 &
+exec 3>p1 4>p2
+Q="SELECT t.PROCESSLIST_USER AS who, l.INDEX_NAME AS idx, l.PARTITION_NAME AS part,
+ l.LOCK_TYPE AS type, l.LOCK_MODE AS mode, l.LOCK_STATUS AS status, l.LOCK_DATA AS data
+ FROM performance_schema.data_locks l
+ JOIN performance_schema.threads t ON t.THREAD_ID = l.THREAD_ID
+ WHERE l.OBJECT_NAME = 'pose_lab'
+ ORDER BY who, l.LOCK_TYPE, idx, l.ENGINE_LOCK_ID;"
+while IFS='|' read -r who sql; do
+ [ -z "$who" ] && continue
+ case "$who" in
+ \#*) ;;
+ T1) echo "$sql" >&3; echo ">>> T1: $sql"; sleep "$SLEEP" ;;
+ T2) echo "$sql" >&4; echo ">>> T2: $sql"; sleep "$SLEEP" ;;
+ ROOT) mysql -uroot -proot shadowfit -e "$sql" 2>&1 | grep -v 'Using a password' ;;
+ DUMP) echo "=== [$sql]"; mysql -uroot -proot -t -e "$Q" 2>&1 | grep -v 'Using a password' || true
+ echo " (잠금 없음이면 표가 비어 있다)" ;;
+ esac
+done < "$SCEN"
+exec 3>&- 4>&-
+wait
+echo "--- t1 세션 로그 (오류만)"; grep -E 'ERROR|Query OK|rows affected' t1.log | sed 's/^/ /'
+echo "--- t2 세션 로그 (오류만)"; grep -E 'ERROR|Query OK|rows affected' t2.log | sed 's/^/ /'
+EOS
+
+v() { echo "($1, $2, $3.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00')"; }
+COLS="(session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at)"
+ODKU="ON DUPLICATE KEY UPDATE session_id = session_id" # PoseDataService.INSERT_POSE_SQL 과 같은 꼴
+ins() { echo "INSERT INTO pose_lab $COLS VALUES $(v "$1" "$2" "$3") $ODKU;"; }
+insi() { echo "INSERT IGNORE INTO pose_lab $COLS VALUES $(v "$1" "$2" "$3");"; }
+
+# 공통 준비: 원본(재전송이 겹칠 대상) 두 행을 커밋해 둔다 — 세션 901·902 의 rep1.
+setup() { # $1 = 표 변형 DDL(없으면 빈 문자열)
+ cat </dev/null
+echo "# MySQL $(docker exec "$CONTAINER" mysql -uroot -proot -N -e 'select version()' 2>/dev/null) · 기본 격리 $(docker exec "$CONTAINER" mysql -uroot -proot -N -e 'select @@transaction_isolation' 2>/dev/null) · SLEEP=$SLEEP"
+docker exec -i "$CONTAINER" bash -c 'cat > /tmp/driver.sh' <<<"$DRIVER"
+dl() { docker exec "$CONTAINER" mysql -uroot -proot -N -e "SELECT COUNT FROM information_schema.INNODB_METRICS WHERE NAME='lock_deadlocks'" 2>/dev/null; }
+for arm in $ARMS; do
+ echo; echo "################ 팔: $arm ################"
+ scenario "$arm" | docker exec -i "$CONTAINER" bash -c 'cat > /tmp/scen.txt'
+ before=$(dl)
+ docker exec "$CONTAINER" bash /tmp/driver.sh /tmp/scen.txt "$SLEEP"
+ after=$(dl)
+ echo "--- 이 팔의 데드락 수: $((after - before))"
+ # LATEST DETECTED DEADLOCK 은 서버 수명 동안 남으므로, 이 팔에서 새로 났을 때만 찍는다
+ [ "$after" -gt "$before" ] && docker exec "$CONTAINER" mysql -uroot -proot -e "SHOW ENGINE INNODB STATUS\G" 2>/dev/null \
+ | sed -n '/LATEST DETECTED DEADLOCK/,/^TRANSACTIONS$/p' | grep -E 'TRANSACTION|index|lock_mode|lock mode|WE ROLL BACK|supremum|INSERT' | head -30 \
+ | sed 's/^/ [deadlock] /'
+done
diff --git a/loadtest/measure_r276_lock_trace_concurrent.sh b/loadtest/measure_r276_lock_trace_concurrent.sh
new file mode 100644
index 00000000..de4b2376
--- /dev/null
+++ b/loadtest/measure_r276_lock_trace_concurrent.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# #276 — 결정적 trace(measure_r276_lock_trace.sh)의 판정을 동시 부하에서 확인한다
+#
+# trace 는 «두 세션 · 한 순서» 라 「데드락 0」을 일반화할 수 없다. 여기서는 기존 확률 rig
+# (measure_r276_deadlock.sh 의 same_partition 팔)과 같은 모양 — 워커 W 개가 각자 다른 세션의
+# 같은 R 행을 I 번 반복(첫 문장만 신규, 나머지 전부 중복) — 을 팔 넷에 건다:
+# base 운영 그대로 (RR · PK=(id, created_at) · uk_pose_event)
+# read_committed 격리수준만 RC
+# natural_pk auto-increment id 를 없애고 멱등 키를 PK 로
+# natural_pk_keep_id 멱등 키를 PK 로 + id 는 AUTO_INCREMENT 보조 인덱스로 남김(리포트의 poseDataId 참조 보존)
+# 각 문장은 autocommit(= 문장 하나가 트랜잭션 하나). 판 순서는 블록마다 회전(라틴 방격), 첫 블록 버림.
+#
+# 무대는 trace 와 같은 격리 컨테이너. 운영 compose DB 는 안 건드린다.
+set -uo pipefail
+export MSYS_NO_PATHCONV=1
+
+CONTAINER=${CONTAINER:-r276-locktrace}
+WORKERS=${WORKERS:-8}
+ITER=${ITER:-40}
+ROWS=${ROWS:-25}
+BLOCKS=${BLOCKS:-4} # 첫 블록은 버림
+ARMS=(base read_committed natural_pk natural_pk_keep_id)
+
+read -r -d '' RUN <<'EOS'
+set -u
+ARM=$1 W=$2 I=$3 R=$4
+M="mysql -uroot -proot shadowfit"
+$M -e "DROP TABLE IF EXISTS pose_lab; CREATE TABLE pose_lab LIKE pose_data;" 2>/dev/null
+if [ "$ARM" = natural_pk ]; then
+ $M -e "ALTER TABLE pose_lab MODIFY id BIGINT NOT NULL; ALTER TABLE pose_lab DROP INDEX uk_pose_event, DROP PRIMARY KEY, DROP COLUMN id, ADD PRIMARY KEY (session_id, rep_number, timestamp_sec, created_at);" 2>/dev/null
+fi
+if [ "$ARM" = natural_pk_keep_id ]; then
+ $M -e "ALTER TABLE pose_lab DROP INDEX uk_pose_event, DROP PRIMARY KEY, ADD PRIMARY KEY (session_id, rep_number, timestamp_sec, created_at), ADD KEY idx_pose_id (id);" 2>/dev/null
+fi
+PRE=""; [ "$ARM" = read_committed ] && PRE="SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;"
+cd /tmp && rm -f w*.sql w*.err
+for w in $(seq 1 "$W"); do
+ sid=$((900 + w)); vals=""
+ for r in $(seq 1 "$R"); do
+ vals="$vals${vals:+,}($sid, $r, $r.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00')"
+ done
+ { echo "$PRE"; for i in $(seq 1 "$I"); do
+ echo "INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES $vals ON DUPLICATE KEY UPDATE session_id = session_id;"
+ done; } > "w$w.sql"
+done
+for w in $(seq 1 "$W"); do mysql -ut1 -pp1 --force shadowfit < "w$w.sql" 2> "w$w.err" & done
+wait
+dead=$(cat w*.err | grep -c 'ERROR 1213' || true)
+other=$(cat w*.err | grep 'ERROR' | grep -vc 'ERROR 1213' || true)
+rows=$($M -N -e "SELECT COUNT(*) FROM pose_lab" 2>/dev/null)
+echo "$ARM $dead $((W * I)) $other $rows"
+EOS
+docker exec -i "$CONTAINER" bash -c 'cat > /tmp/run.sh' <<<"$RUN"
+
+echo "# MySQL $(docker exec "$CONTAINER" mysql -uroot -proot -N -e 'select version()' 2>/dev/null) · W=$WORKERS I=$ITER R=$ROWS · 블록 $BLOCKS(첫 블록 버림)"
+echo "# block order arm deadlocks attempts other_errors final_rows"
+n=${#ARMS[@]}
+for b in $(seq 0 $((BLOCKS - 1))); do
+ for k in $(seq 0 $((n - 1))); do
+ arm=${ARMS[$(( (k + b) % n ))]}
+ printf '%s %s ' "$b" "$k"
+ docker exec "$CONTAINER" bash /tmp/run.sh "$arm" "$WORKERS" "$ITER" "$ROWS"
+ done
+done
diff --git a/loadtest/results/r276-lock-trace-2026-09-24/README.md b/loadtest/results/r276-lock-trace-2026-09-24/README.md
new file mode 100644
index 00000000..ac2a2216
--- /dev/null
+++ b/loadtest/results/r276-lock-trace-2026-09-24/README.md
@@ -0,0 +1,83 @@
+# #276 결정적 재현 — 중복 하나가 파티션 끝을 잠근다 (2026-09-24)
+
+이슈: [#276](https://github.com/Shadowfit/init/issues/276) · rig: [`measure_r276_lock_trace.sh`](../../measure_r276_lock_trace.sh)(단계별 잠금 표) · [`measure_r276_lock_trace_concurrent.sh`](../../measure_r276_lock_trace_concurrent.sh)(동시 부하 확인)
+원문: [`trace.txt`](./trace.txt) · [`concurrent.txt`](./concurrent.txt)
+
+## 0. 왜 이 판인가
+
+08-17~08-30 의 판은 전부 **확률**(워커 N 개를 풀어 데드락 비율을 셈)이었고, 처방도 **재시도 상한**(현재 5)이었다.
+잠금 자리(`PRIMARY` supremum)는 데드락 덤프 한 쌍으로만 봤고 **어느 문장이 그 X 를 만드는지**는 미검증이었다.
+이 판은 부하 없이 두 세션을 한 문장씩 진행시키며 단계마다 `performance_schema.data_locks` 를 찍는다 —
+같은 입력이면 같은 출력이라 판 수가 필요 없다. 그리고 **재시도가 아니라 그 X 를 안 생기게 하는 구조**를 팔로 넣었다.
+
+## 1. 무대
+
+- 격리 컨테이너 `mysql:8.0` → **8.0.46**, 기본 격리 REPEATABLE-READ, `innodb_autoinc_lock_mode=2`
+- 스키마: Flyway `V1`~`V26` 을 버전 순으로 그대로 적용. 대상 표 `pose_lab` = `CREATE TABLE ... LIKE pose_data`
+ (PK `(id, created_at)` · `uk_pose_event` · 월 파티션 전부 복제). 운영 compose DB 는 안 건드렸다
+- 삽입문은 `PoseDataService.INSERT_POSE_SQL` 과 같은 꼴(`ON DUPLICATE KEY UPDATE session_id = session_id`)
+- 원본 행: 세션 901·902 의 `(rep 1, ts 1.000)` 을 먼저 커밋 — 재전송이 겹칠 대상
+- 로컬 박스(i3-6100, 2물리코어). **비율의 절대값은 이 박스의 것**이다
+
+## 2. 결정적 trace — 판정
+
+| 팔 | 중복 1건 직후 T1 이 쥐는 것 | 남(T2)의 삽입 | 데드락 |
+|---|---|---|---|
+| **base** (운영 그대로) | `PRIMARY` **supremum `X`** + 원본 PK 행 `X,REC_NOT_GAP` + uk 원본 `X` | **막힘** — `X,INSERT_INTENTION WAITING` on supremum, T1 COMMIT 까지(3.09초) | 0 (T2 가 먼저 막혀 순환이 안 닫힘) |
+| **dup_blocks_new** | 같음 | **무관한 세션(903)의 신규 키도 막힘** | 0 |
+| plain_insert (ODKU 아님, 1062 실패) | **supremum `X` 가 똑같이 남는다** | — | — |
+| insert_ignore | 같음 (supremum `X`) | 막힘 | 0 |
+| dup_only / new_only | dup_only 는 base 와 같음 · new_only 는 gap 락 없음 | new_only 는 안 막힘 | 0 |
+| no_uk | 잠금 없음 | 안 막힘 | 0 — **대신 중복이 그대로 들어간다(6행)** |
+| **read_committed** | supremum 없음. 원본 PK 행 `X,REC_NOT_GAP` + uk 원본 **`X`(next-key)** | **한 방향 대기** — T1 의 신규(901,2,2)가 T2 가 잡은 uk (902,1,1) 앞 gap 에서 WAITING(3.26초) | 0 |
+| **natural_pk** (id 제거, 멱등 키 = PK) | **원본 PK 행 `X,REC_NOT_GAP` 하나뿐** — gap·supremum 없음 | 안 막힘 | 0 |
+| **natural_pk_keep_id** (멱등 키 = PK, id 는 AUTO_INCREMENT 보조 인덱스 `KEY(id)`) | natural_pk 와 같음 — `idx_pose_id` 는 잠금 목록에 안 나온다 | 안 막힘 | 0 — 기존 id(1·2) 그대로 |
+
+### 읽으면
+
+1. **supremum `X` 의 출처는 «중복 한 건» 이다.** 두 세션이 엇갈릴 필요도 없다 — T1 혼자 중복 하나를 넣은 직후에 이미 있다.
+ ODKU 고유도 아니다: `INSERT IGNORE` 와 평범한 `INSERT`(1062 실패)도 같은 락을 남긴다. 즉 **RR 에서의 중복 키 처리 경로**다.
+2. **그 X 는 데드락 이전에 «파티션 끝 직렬화» 다.** 중복을 넣은 트랜잭션이 커밋할 때까지, 같은 파티션(= 같은 달)에
+ **새 행을 넣으려는 모든 트랜잭션**이 supremum 의 insert intention 에서 기다린다 — 원본과 무관한 세션도(dup_blocks_new).
+ auto-increment PK 라 모든 신규 삽입이 파티션 끝으로 가기 때문이다.
+3. **데드락은 그 X 를 두 트랜잭션이 «동시에» 쥘 때 닫힌다.** 이 trace 는 문장 단위로 진행하므로 T2 가 X 를 얻기 전에 막혀
+ 순환이 안 생긴다 — 둘이 같은 순간에 중복을 처리하는 창은 SQL 단계로는 못 만든다. 그래서 동시 부하 판(§3)으로 확인했다.
+ 동시성이 오를수록 그 창이 겹칠 확률이 오른다 — 08-20 워커 스윕(w=2 1.2% → w=16 59.5%)과 방향이 맞는다.
+4. **RC 는 supremum 을 없애지만 대기를 다 없애지는 않는다.** 중복 검사는 RC 에서도 uk 원본 레코드에 next-key `X` 를 잡는다.
+ 관측된 대기는 **한 방향**(낮은 세션의 신규 → 다음 세션이 잡은 레코드 앞 gap)이라 이 모양에서는 순환이 안 생겼다.
+5. **자연키 PK 는 둘 다 없앤다.** 중복 검사가 곧 PK 조회라 원본 레코드 하나만 `REC_NOT_GAP` 으로 잠근다.
+ id 를 AUTO_INCREMENT 보조 인덱스로 남겨도 같다 — 리포트가 영구 저장한 `poseDataId`(`WorstSectionDto`)를 살릴 수 있다.
+
+## 3. 동시 부하 확인 — trace 의 «0» 이 부하에서도 0 인가
+
+기존 확률 rig 의 same_partition 팔과 같은 모양: 워커 8 · 워커마다 다른 세션의 같은 25행을 40문(첫 문장만 신규) · 문장 = autocommit.
+팔 4 × 4블록, **블록마다 순서 회전(라틴 방격), 블록 0 버림.**
+
+| 팔 | 블록 1 | 블록 2 | 블록 3 | 중앙값 | 최종 행 |
+|---|---:|---:|---:|---:|---:|
+| base | 155/320 | 147/320 | 129/320 | **45.9%** | 200 |
+| read_committed | 0/320 | 0/320 | 0/320 | **0 / 960** | 200 |
+| natural_pk | 0/320 | 0/320 | 0/320 | **0 / 960** | 200 |
+| natural_pk_keep_id | 0/320 | 0/320 | 0/320 | **0 / 960** | 200 |
+
+- 같은 날 팔 3 개로 먼저 돌린 판(이 파일에 없음)은 base 141·103·148(중앙값 44.1%), 나머지 둘 0/960 이었다 — 판정이 같다
+- base 의 45.9% 는 08-20 로컬(37.5%·42.2%)·08-23 AWS(42.8%)와 같은 자리다 — 무대가 기존 판을 재현한다
+- 두 후보 모두 **멱등은 유지**(최종 200행 = 8세션 × 25행)
+- 그 외 에러 0
+
+## 4. 정정·철회
+
+- 🔴 **trace 도중 세운 가설 하나를 철회한다** — base 팔 최종 id 가 1·2·4·5 라 «T1 의 중복이 클러스터에 먼저 들어갔다 되감기며
+ 락이 supremum 으로 상속됐다» 고 봤는데, **단일 세션에서 중복 하나를 넣으면 RR·RC 모두 id 가 안 빈다**(1·2 연속).
+ 빈 id 3 은 T1 이 아니라 **대기하던 T2** 쪽에서 났다. supremum X 가 «왜» 생기는지의 소스 수준 설명은 **미검증**이다
+- 08-17 코멘트의 «PK 가 AUTO_INCREMENT 라 삽입이 끝에 몰린다» 는 **자리의 설명으로는 맞다**(natural_pk 에서 사라지므로).
+ 08-20 코멘트가 no_uk 대조군을 근거로 이 서술을 «반증됐다» 고 적었고, 같은 날 덤프 코멘트는 **자리**(PRIMARY supremum)만
+ 되돌렸다 — auto-increment 쪽 서술은 반증 표시가 붙은 채 남아 있었다. 이 판이 그것을 되살린다
+
+## 5. 말하면 안 되는 것
+
+- **RC 가 «모든 패턴에서» 데드락 0 이라는 것.** 관측한 대기가 한 방향이었던 것은 세션의 키가 `session_id` 로 묶여 있어서다.
+ 한 트랜잭션이 여러 세션의 키를 섞어 넣는 경로가 생기면 이 논증은 안 선다. 부하 확인도 이 모양 하나다
+- **자연키 PK 의 비용.** 이 판은 잠금만 봤다 — 쓰기 처리량·페이지 분할·보조 인덱스 크기·1억 행 PK 재구성 시간은 **안 쟀다**
+- **supremum 직렬화의 운영 영향 크기.** 재전송이 드문 동안은 트랜잭션이 짧아 체감이 작을 것이다 — 이것도 **안 쟀다**
+- 45.9% 등 절대 비율 — 박스·동시성·페이로드(작은 JSON) 조건의 값이다
diff --git a/loadtest/results/r276-lock-trace-2026-09-24/concurrent.txt b/loadtest/results/r276-lock-trace-2026-09-24/concurrent.txt
new file mode 100644
index 00000000..e0886c96
--- /dev/null
+++ b/loadtest/results/r276-lock-trace-2026-09-24/concurrent.txt
@@ -0,0 +1,18 @@
+# MySQL 8.0.46 · W=8 I=40 R=25 · 블록 4(첫 블록 버림)
+# block order arm deadlocks attempts other_errors final_rows
+0 0 base 165 320 0 200
+0 1 read_committed 0 320 0 200
+0 2 natural_pk 0 320 0 200
+0 3 natural_pk_keep_id 0 320 0 200
+1 0 read_committed 0 320 0 200
+1 1 natural_pk 0 320 0 200
+1 2 natural_pk_keep_id 0 320 0 200
+1 3 base 155 320 0 200
+2 0 natural_pk 0 320 0 200
+2 1 natural_pk_keep_id 0 320 0 200
+2 2 base 147 320 0 200
+2 3 read_committed 0 320 0 200
+3 0 natural_pk_keep_id 0 320 0 200
+3 1 base 129 320 0 200
+3 2 read_committed 0 320 0 200
+3 3 natural_pk 0 320 0 200
diff --git a/loadtest/results/r276-lock-trace-2026-09-24/trace.txt b/loadtest/results/r276-lock-trace-2026-09-24/trace.txt
new file mode 100644
index 00000000..a2703a6e
--- /dev/null
+++ b/loadtest/results/r276-lock-trace-2026-09-24/trace.txt
@@ -0,0 +1,621 @@
+# MySQL 8.0.46 · 기본 격리 REPEATABLE-READ · SLEEP=1
+
+################ 팔: base ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | PRIMARY | p2026_05 | RECORD | X,GAP | GRANTED | 4, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | PRIMARY | p2026_05 | RECORD | X,GAP | GRANTED | 4, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec id
+901 1 1.000 1
+901 2 2.000 4
+902 1 1.000 2
+902 2 2.000 5
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.14 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (3.24 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.08 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: dup_blocks_new ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 중복]
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (903, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 무관한 신규(903,1,1) 를 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+=== [3 T1 COMMIT 뒤]
++------+---------+----------+--------+--------------------+---------+------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+--------------------+---------+------------------------+
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | GRANTED | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+--------------------+---------+------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: COMMIT;
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.01 sec)
+ Query OK, 0 rows affected (0.04 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (1.02 sec)
+ Query OK, 0 rows affected (0.03 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: plain_insert ################
+>>> T1: BEGIN;
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00');
+=== [1 T1 의 평범한 INSERT 가 1062 로 실패한 직후]
++------+---------------+----------+--------+------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | uk_pose_event | p2026_05 | RECORD | S | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: ROLLBACK;
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ ERROR 1062 (23000) at line 2: Duplicate entry '901-1-1.000-2026-05-15 10:00:00' for key 'pose_lab.uk_pose_event'
+ Query OK, 0 rows affected (0.01 sec)
+--- t2 세션 로그 (오류만)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: dup_only ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 중복]
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 중복]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 중복(두 번째)]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 3, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 2, 0x800000020000, 0x6A067010, 3 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 중복(두 번째)]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 3, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 2, 0x800000020000, 0x6A067010, 3 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.01 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.03 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.01 sec)
+ Query OK, 0 rows affected (3.09 sec)
+ Query OK, 0 rows affected (0.01 sec)
+ Query OK, 0 rows affected (0.04 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: new_only ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 3, 3.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 신규]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 3, 3.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 신규]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 4, 4.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 신규(두 번째)]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 4, 4.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 신규(두 번째)]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.09 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.01 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.07 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: no_uk ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+------+----------+-------+------+---------+------+
+| who | idx | part | type | mode | status | data |
++------+------+----------+-------+------+---------+------+
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+------+----------+-------+------+---------+------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec id
+901 1 1.000 1
+901 1 1.000 3
+901 2 2.000 5
+902 1 1.000 2
+902 1 1.000 4
+902 2 2.000 6
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.07 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.01 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.02 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: insert_ignore ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT IGNORE INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00');
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+---------------+----------+--------+------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | uk_pose_event | p2026_05 | RECORD | S | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT IGNORE INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00');
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | uk_pose_event | p2026_05 | RECORD | S | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT IGNORE INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00');
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,GAP | GRANTED | 4, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | S | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT IGNORE INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00');
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X | GRANTED | supremum pseudo-record |
+| t1 | PRIMARY | p2026_05 | RECORD | X,GAP | GRANTED | 4, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | S | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+--------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec id
+901 1 1.000 1
+901 2 2.000 4
+902 1 1.000 2
+902 2 2.000 5
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected, 1 warning (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.09 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected, 1 warning (3.15 sec)
+ Query OK, 1 row affected (0.01 sec)
+ Query OK, 0 rows affected (0.05 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: read_committed ################
+>>> T1: SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN;
+>>> T2: SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 2, 0x6A067010 |
+| t2 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 902, 1, 0x800000010000, 0x6A067010, 2 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+---------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X,GAP,INSERT_INTENTION | WAITING | 902, 1, 0x800000010000, 0x6A067010, 2 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 2, 0x6A067010 |
+| t2 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 902, 1, 0x800000010000, 0x6A067010, 2 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 1, 0x6A067010 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 901, 1, 0x800000010000, 0x6A067010, 1 |
+| t1 | uk_pose_event | p2026_05 | RECORD | X,GAP,INSERT_INTENTION | WAITING | 902, 1, 0x800000010000, 0x6A067010, 2 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 2, 0x6A067010 |
+| t2 | uk_pose_event | p2026_05 | RECORD | X | GRANTED | 902, 1, 0x800000010000, 0x6A067010, 2 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------------+----------+--------+------------------------+---------+---------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec id
+901 1 1.000 1
+901 2 2.000 3
+902 1 1.000 2
+902 2 2.000 4
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (3.04 sec)
+ Query OK, 0 rows affected (0.05 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.03 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: natural_pk ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec
+901 1 1.000
+901 2 2.000
+902 1 1.000
+902 2 2.000
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.01 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.06 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.07 sec)
+--- 이 팔의 데드락 수: 0
+
+################ 팔: natural_pk_keep_id ################
+>>> T1: BEGIN;
+>>> T2: BEGIN;
+=== [0 시작 — 둘 다 BEGIN]
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [1 T1 이 중복(901,1,1) 을 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 1, 1.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [2 T2 가 중복(902,1,1) 을 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (901, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [3 T1 이 신규(901,2,2) 를 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T2: INSERT INTO pose_lab (session_id, rep_number, timestamp_sec, joint_coordinates, sync_rate, smoothed_knee_angle, feedback_message, created_at) VALUES (902, 2, 2.000, JSON_OBJECT('k',1), 50.00, 0.00, NULL, '2026-05-15 10:00:00') ON DUPLICATE KEY UPDATE session_id = session_id;
+=== [4 T2 가 신규(902,2,2) 를 넣은 직후]
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| who | idx | part | type | mode | status | data |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+| t1 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 901, 1, 0x800000010000, 0x6A067010 |
+| t1 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
+| t2 | PRIMARY | p2026_05 | RECORD | X,REC_NOT_GAP | GRANTED | 902, 1, 0x800000010000, 0x6A067010 |
+| t2 | NULL | p2026_05 | TABLE | IX | GRANTED | NULL |
++------+---------+----------+--------+---------------+---------+------------------------------------+
+ (잠금 없음이면 표가 비어 있다)
+>>> T1: COMMIT;
+>>> T2: COMMIT;
+=== [5 둘 다 COMMIT 뒤]
+ (잠금 없음이면 표가 비어 있다)
+session_id rep_number timestamp_sec id
+901 1 1.000 1
+901 2 2.000 3
+902 1 1.000 2
+902 2 2.000 4
+--- t1 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.02 sec)
+--- t2 세션 로그 (오류만)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 0 rows affected (0.00 sec)
+ Query OK, 1 row affected (0.00 sec)
+ Query OK, 0 rows affected (0.07 sec)
+--- 이 팔의 데드락 수: 0