--- id: b099ca65-bf9f-4d61-814c-74722453fa3c kind: QUESTION slug: nullable-first-highlighted-at title: Highlight 없는 FeedItem을 허용할 것인가 topic: jpa-feed-query-performance topicName: JPA 피드 조회 성능 project: Liner N + 1문제 status: 게시 전 studio: "https://hyeonworks.com/studio/documents/b099ca65-bf9f-4d61-814c-74722453fa3c/edit" questionStatus: OPEN --- # Highlight 없는 FeedItem을 허용할 것인가 정렬키 first_highlighted_at이 nullable이라 하이라이트 없는 FeedItem이 존재할 수 있는 스키마다. 시더는 하이라이트가 만든 FeedItem만 넣어 항상 값이 차지만, 허용 여부를 정하지 않으면 정렬과 커서 비교식의 경계 동작이 정의되지 않는다. ## 관계 - **Keyset Pagination 설계 기준** 정렬키의 null 처리가 커서 설계에 걸리는 지점이다. - **Visibility OR이 Keyset Index를 깨뜨린 문제** 같은 정렬키 인덱스를 다루는 기록이다. ## 사실 - 현재 스키마의 first_highlighted_at은 timestamptz nullable이다. NOT NULL이 아니다. - 시더는 하이라이트가 만든 FeedItem만 생성하므로 이 값을 항상 채운다. 그래서 지금까지의 측정에서는 null이 나타나지 않았다. - FeedItem은 (user, page) 조합당 하나이고 UNIQUE(user_id, page_id) 제약이 있다. - keyset 커서는 (first_highlighted_at, id)를 비교식으로 사용한다. - 정렬키 전용 인덱스는 (first_highlighted_at DESC, id DESC)로 만들었다. ## 가정 - 하이라이트가 하나도 없는 FeedItem이 생기는 경로가 실제로 있을 수 있다. - null이 섞이면 커서 비교식이 경계에서 행을 빠뜨리거나 중복시킬 수 있다. - 정렬 위치를 정의하지 않으면 페이지를 넘길 때 순서가 흔들릴 수 있다. ## 미지수 - 하이라이트 없는 FeedItem을 만드는 경로가 도메인에 존재하는가. 존재한다면 어떤 상황인가. - NOT NULL로 좁힐 것인가, null을 허용하고 정렬 위치를 정의할 것인가. - null을 허용한다면 정렬에서 어디에 두는가. 그 위치를 인덱스가 지원하는가. - 커서가 null을 만났을 때 비교식을 어떻게 표현하는가. - 부분 인덱스 조건을 쓴다면 null 행이 인덱스에서 빠지는데 그 행은 어떻게 조회되는가. - FeedItem 생성 시점과 첫 하이라이트 시점이 다를 수 있는가. ## 제약 - 결정 시점은 keyset 페이징을 프로덕션에 반영하기 전이다. 커서 비교식과 인덱스 정의가 이 결정에 달려 있다. - 스키마를 NOT NULL로 좁히려면 기존 데이터에 null이 없어야 한다. 마이그레이션 전에 확인이 필요하다. - 현재 시더로는 이 경계가 재현되지 않는다. null이 섞인 데이터셋을 따로 만들어야 검증할 수 있다. ## 선택지 ### 1. NOT NULL로 좁힌다 FeedItem이 항상 하이라이트와 함께 만들어진다면 정렬키를 NOT NULL로 정의한다. 커서 비교식이 단순해지고 인덱스도 그대로 쓸 수 있다. 대신 하이라이트 없는 FeedItem을 만드는 경로가 나중에 필요해지면 스키마와 생성 흐름을 다시 바꿔야 한다. ### 2. null을 허용하고 정렬 위치를 정의한다 정렬에서 null을 어디에 둘지 명시하고 인덱스도 같은 위치로 만든다. 커서 비교식은 null 구간을 따로 다룬다. 생성 흐름은 자유로워지지만 커서 표현과 인덱스 정의가 복잡해진다. ### 3. 부분 인덱스로 null 행을 제외한다 정렬키가 있는 행만 인덱스에 담는다. 피드 목록에는 하이라이트가 있는 항목만 노출한다는 정책이 된다. 인덱스는 작아지지만 null 행을 조회하는 별도 경로가 필요하다. ## 다음 검증 1. 도메인에서 하이라이트 없는 FeedItem이 생기는 경로가 있는지 확인한다. 2. 기존 데이터에 first_highlighted_at이 null인 행이 있는지 센다. 3. null이 섞인 데이터셋을 만들어 현재 커서 비교식이 경계에서 어떻게 동작하는지 재현한다. 4. 세 선택지 각각에서 커서로 넘긴 페이지가 OFFSET 페이지와 같은 행·같은 순서인지 대조한다. 5. 정렬 위치를 정의한 경우 인덱스가 그 순서를 그대로 주는지 실행계획으로 확인한다.