문자열 컬럼을 숫자로 조회했더니 전체 행이 나오고 인덱스도 안 쓰인 이유
문제 발생
주문 코드로 한 건을 찾는 쿼리가 모든 행을 돌려줬습니다.
SELECT * FROM orders WHERE order_code = 0;
-- 5 rows returnedorder_code는 VARCHAR(25)였고, 애플리케이션이 파싱 실패로 0을 넘긴 상태였습니다. 다른 화면에서는 코드가 숫자 리터럴로 들어가 인덱스를 태우지 못해 느려졌습니다.
원인 분석
MySQL 문서는 비교 규칙을 이렇게 정합니다 — 그 밖의 모든 경우 인자는 부동소수점(배정밀도) 숫자로 비교되며, 문자열과 숫자 피연산자의 비교도 부동소수점 비교로 이루어집니다.
그래서 'grape' = 0은 타입 오류가 아니라 숫자로 변환된 뒤의 비교가 됩니다. 앞부분이 숫자가 아닌 문자열은 0으로 변환되므로 모두 참이 됩니다. 문서에도 VARCHAR 컬럼과 0을 비교해 전체 행이 반환되고, '0'으로 따옴표를 붙이면 빈 결과가 되는 예제가 그대로 실려 있습니다.
인덱스가 안 쓰인 것도 같은 이유입니다. 문서가 직접 설명합니다 — 문자열 컬럼과 숫자의 비교에서는 MySQL이 그 컬럼의 인덱스로 값을 빠르게 찾을 수 없습니다. 이유까지 적혀 있습니다 — 1로 변환되는 문자열이 '1', ' 1', '1a'처럼 여러 개이기 때문입니다. 인덱스는 문자열 순서로 정렬돼 있어 이 조건을 범위로 좁힐 수 없습니다.
해결 방안
- 비교하는 값의 타입을 컬럼과 맞춥니다. 애플리케이션에서 문자열로 바인딩하면 이 문제 자체가 생기지 않습니다.
SELECT * FROM orders WHERE order_code = '0';Drizzle처럼 파라미터화된 쿼리를 쓰면 값의 JS 타입이 그대로 바인딩되므로, Number(...)를 거친 값을 문자열 컬럼에 넘기고 있지 않은지 확인합니다.
-
입력을 경계에서 검증합니다. 파싱 실패가
0이나NaN으로 조용히 흘러 들어가는 경로를 막습니다. Zod 스키마에서 문자열은 문자열로 유지합니다. -
컬럼 타입 자체를 다시 봅니다. 값이 항상 숫자라면
VARCHAR대신 정수 타입이 맞습니다. 앞자리 0이나 하이픈이 의미를 갖는다면 문자열이 맞고, 그때는 비교도 항상 문자열로 합니다. -
컬럼에 함수를 씌우지 않습니다.
CAST(order_code AS UNSIGNED) = 0처럼 컬럼 쪽을 변환하면 같은 이유로 인덱스를 쓸 수 없습니다. 변환은 값 쪽에서 합니다. -
EXPLAIN으로 확인합니다. 결과가 맞아 보여도type: ALL이면 인덱스를 못 쓰고 있는 것입니다. 이 종류의 문제는 데이터가 적은 로컬에서는 느려지지 않아 드러나지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.