본문으로 건너뛰기
개발 머꼬
개발 노트SQL
hohyeon.dev20

문자열 컬럼을 숫자로 조회했더니 전체 행이 나오고 인덱스도 안 쓰인 이유

  • #Debugging
  • #Engineering Note
  • #SQL

문제 발생

주문 코드로 한 건을 찾는 쿼리가 모든 행을 돌려줬습니다.

SELECT * FROM orders WHERE order_code = 0;
-- 5 rows returned

order_codeVARCHAR(25)였고, 애플리케이션이 파싱 실패로 0을 넘긴 상태였습니다. 다른 화면에서는 코드가 숫자 리터럴로 들어가 인덱스를 태우지 못해 느려졌습니다.

원인 분석

MySQL 문서는 비교 규칙을 이렇게 정합니다 — 그 밖의 모든 경우 인자는 부동소수점(배정밀도) 숫자로 비교되며, 문자열과 숫자 피연산자의 비교도 부동소수점 비교로 이루어집니다.

그래서 'grape' = 0은 타입 오류가 아니라 숫자로 변환된 뒤의 비교가 됩니다. 앞부분이 숫자가 아닌 문자열은 0으로 변환되므로 모두 참이 됩니다. 문서에도 VARCHAR 컬럼과 0을 비교해 전체 행이 반환되고, '0'으로 따옴표를 붙이면 빈 결과가 되는 예제가 그대로 실려 있습니다.

인덱스가 안 쓰인 것도 같은 이유입니다. 문서가 직접 설명합니다 — 문자열 컬럼과 숫자의 비교에서는 MySQL이 그 컬럼의 인덱스로 값을 빠르게 찾을 수 없습니다. 이유까지 적혀 있습니다 — 1로 변환되는 문자열이 '1', ' 1', '1a'처럼 여러 개이기 때문입니다. 인덱스는 문자열 순서로 정렬돼 있어 이 조건을 범위로 좁힐 수 없습니다.

해결 방안

  1. 비교하는 값의 타입을 컬럼과 맞춥니다. 애플리케이션에서 문자열로 바인딩하면 이 문제 자체가 생기지 않습니다.
SELECT * FROM orders WHERE order_code = '0';

Drizzle처럼 파라미터화된 쿼리를 쓰면 값의 JS 타입이 그대로 바인딩되므로, Number(...)를 거친 값을 문자열 컬럼에 넘기고 있지 않은지 확인합니다.

  1. 입력을 경계에서 검증합니다. 파싱 실패가 0이나 NaN으로 조용히 흘러 들어가는 경로를 막습니다. Zod 스키마에서 문자열은 문자열로 유지합니다.

  2. 컬럼 타입 자체를 다시 봅니다. 값이 항상 숫자라면 VARCHAR 대신 정수 타입이 맞습니다. 앞자리 0이나 하이픈이 의미를 갖는다면 문자열이 맞고, 그때는 비교도 항상 문자열로 합니다.

  3. 컬럼에 함수를 씌우지 않습니다. CAST(order_code AS UNSIGNED) = 0처럼 컬럼 쪽을 변환하면 같은 이유로 인덱스를 쓸 수 없습니다. 변환은 값 쪽에서 합니다.

  4. EXPLAIN으로 확인합니다. 결과가 맞아 보여도 type: ALL이면 인덱스를 못 쓰고 있는 것입니다. 이 종류의 문제는 데이터가 적은 로컬에서는 느려지지 않아 드러나지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.