MySQL TINYINT
들어가며
MySQL에서 BOOLEAN 타입을 사용해본 적이 있는가? 아마 대부분의 개발자가 "있다"라고 답할 것이다. 하지만 MySQL에는 진짜 BOOLEAN 타입이 존재하지 않는다.
1
2
3
4
5
6
7
8
CREATE TABLE test (
is_active BOOLEAN
);
SHOW CREATE TABLE test;
-- 결과:
CREATE TABLE `bool_test` (\n `flag` tinyint(1) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
BOOLEAN으로 선언했지만, 실제로는 TINYINT(1)로 저장된다. 이 글에서는 MySQL 공식 문서를 근거로 TINYINT의 정체, BOOLEAN과의 관계, 그리고 JDBC 매핑 문제까지 깊이 파헤쳐본다.
MySQL 정수 타입 체계
MySQL 8.0 Reference Manual의 13.1.2 Integer Types 문서에서는 다음과 같이 정수 타입을 정의한다.
MySQL supports the SQL standard integer types INTEGER (or INT) and SMALLINT. As an extension to the standard, MySQL also supports the integer types TINYINT, MEDIUMINT, and BIGINT.
즉, INTEGER와 SMALLINT만 SQL 표준이고, TINYINT, MEDIUMINT, BIGINT는 MySQL의 확장 이다.
공식 문서의 저장 크기 및 범위 표
13.7 Data Type Storage Requirements 문서에서 명시한 저장 크기는 다음과 같다.
그리고 13.1.2 Integer Types 문서에서 각 타입의 범위를 명시한다.
BOOLEAN의 실체
13.1.1 Numeric Data Type Syntax 문서에서 BOOLEAN에 대해 다음과 같이 명시한다.
BOOL, BOOLEAN
These types are synonyms for TINYINT(1). A value of zero is considered false. Nonzero values are considered true:
공식 문서가 명확하게 말하고 있다. BOOL과 BOOLEAN은 TINYINT(1)의 동의어(synonym) 다.
TRUE와 FALSE의 정체
같은 문서에서 TRUE와 FALSE에 대해서도 설명한다.
However, the values TRUE and FALSE are merely aliases for 1 and 0, respectively, as shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27mysql> SELECT IF(0 = FALSE, 'true', 'false'); +--------------------------------+ | IF(0 = FALSE, 'true', 'false') | +--------------------------------+ | true | +--------------------------------+ mysql> SELECT IF(1 = TRUE, 'true', 'false'); +-------------------------------+ | IF(1 = TRUE, 'true', 'false') | +-------------------------------+ | true | +-------------------------------+ mysql> SELECT IF(2 = TRUE, 'true', 'false'); +-------------------------------+ | IF(2 = TRUE, 'true', 'false') | +-------------------------------+ | false | +-------------------------------+ mysql> SELECT IF(2 = FALSE, 'true', 'false'); +--------------------------------+ | IF(2 = FALSE, 'true', 'false') | +--------------------------------+ | false | +--------------------------------+The last two statements display the results shown because 2 is equal to neither 1 nor 0.
이것이 MySQL BOOLEAN의 실체다. TRUE는 1의 별칭이고, FALSE는 0의 별칭 이다. 따라서 2는 TRUE(1)도 아니고 FALSE(0)도 아닌 것으로 판정된다.
BOOLEAN 컬럼을 조건으로 사용할 때
WHERE flag = TRUE는 flag가 정확히 1인 경우만 참이 된다. 0이 아닌 모든 값을 참으로 처리하려면WHERE flag또는WHERE flag != 0을 사용해야 한다.
Display Width (1)의 의미
많은 개발자가 TINYINT(1)의 (1)을 "1바이트" 또는 "1비트"로 오해한다.
13.1.6 Numeric Type Attributes 문서에서 Display Width에 대해 다음과 같이 설명한다.
MySQL supports an extension for optionally specifying the display width of integer data types in parentheses following the base keyword for the type. For example, INT(4) specifies an INT with a display width of four digits.
The display width does not constrain the range of values that can be stored in the column. Nor does it prevent values wider than the column display width from being displayed correctly.
즉, Display Width는 저장 공간과 전혀 관계가 없다. 단지 표시 너비를 의미할 뿐이다.
Display Width Deprecated
같은 문서에서 Display Width가 deprecated 되었음을 명시한다.
As of MySQL 8.0.17, the ZEROFILL attribute is deprecated for numeric data types, as is the display width attribute for integer data types. You should expect support for ZEROFILL and display widths for integer data types to be removed in a future version of MySQL.
MySQL 8.0.19 Release Notes에서는 더 구체적인 내용을 확인할 수 있다.
Display width specification for integer data types was deprecated in MySQL 8.0.17, and now statements that include data type definitions in their output no longer show the display width for integer types, with these exceptions:
- The type is TINYINT(1). MySQL Connectors make the assumption that TINYINT(1) columns originated as BOOLEAN columns; this exception enables them to continue to make that assumption.
- The type includes the ZEROFILL attribute.
TINYINT(1)만 예외적으로 Display Width가 유지 되는 이유는, MySQL Connector들이 TINYINT(1)을 BOOLEAN 컬럼으로 인식하기 때문이다.
MySQL 8.0.17 이후 버전에서는 TINYINT(1)을 제외한 정수 타입의 Display Width 지정이 deprecated 되었다. 새로운 스키마 설계 시
INT(11)같은 표기는 피하는 것이 좋다.
BIT 타입은 대안이 될 수 있는가?
13.1.5 Bit-Value Type - BIT 문서에서 BIT 타입을 설명한다.
The BIT data type is used to store bit values. A type of BIT(M) enables storage of M-bit values. M can range from 1 to 64.
그렇다면 BIT(1)은 정말 1비트만 사용할까? 13.7 Data Type Storage Requirements 문서를 보자.
BIT(M)의 저장 크기는 (M+7)/8 바이트 다. 즉:
| 선언 | 계산 | 실제 저장 크기 |
|---|---|---|
| BIT(1) | (1+7)/8 = 1 | 1바이트 |
| BIT(8) | (8+7)/8 = 1.875 → 1 | 1바이트 |
| BIT(9) | (9+7)/8 = 2 | 2바이트 |
| BIT(64) | (64+7)/8 = 8.875 → 8 | 8바이트 |
결론적으로 BIT(1)과 TINYINT는 동일하게 1바이트를 사용 한다. 저장 공간 이점이 없다.
JDBC에서의 타입 매핑 문제
Java 애플리케이션에서 MySQL을 사용할 때, TINYINT(1)과 BOOLEAN의 매핑은 중요한 문제다.
MySQL Connector/J 공식 문서
MySQL Connector/J Developer Guide - 6.3.8 Result Sets 문서에서 두 가지 중요한 설정을 확인할 수 있다.
- tinyInt1isBit
이 설정은 MySQL의 TINYINT(1) 타입을 자바의 BIT (결국 Boolean) 타입으로 취급할지 결정한다.
Since the MySQL server silently converts BIT to TINYINT(1) when creating tables, should the driver treat the datatype TINYINT(1) as the BIT type?
Default Value true Since Version 3.0.16
- transformedBitIsBoolean
이 설정은 위의 과정에서 타입을 변환할 때, 그 이름을 무엇으로 부를지에 대한 세부 설정이다.
If the driver converts TINYINT(1) to a different type, should it use BOOLEAN instead of BIT?
Default Value false Since Version 3.1.9
기본 설정에서 TINYINT(1)은 BIT로 변환된다. transformedBitIsBoolean=true로 설정하면 java.lang.Boolean으로 매핑된다.
MySQL 8.0.19 이후의 변경사항
MySQL Bugs #100722에서 중요한 변경사항이 보고되었다.
The change is that starting from MySQL 8.0.19 only signed TINYINT(1) can be treated as Boolean or Bit, other variants are not supplied with a display width thus always treated as TINYINT.
MySQL 8.0.19부터 signed TINYINT(1)만 Boolean/Bit로 처리 되고, UNSIGNED TINYINT(1)은 그냥 TINYINT로 인식된다. 이로 인해 Hibernate 스키마 검증에서 다음과 같은 에러가 발생할 수 있다.
1
2
3
org.hibernate.tool.schema.spi.SchemaManagementException:
Schema-validation: wrong column type encountered in column [is_active];
found [tinyint (Types#TINYINT)], but expecting [bit (Types#BIT)]
MySQL 8.0.19 이후 버전에서 TINYINT(1) UNSIGNED를 사용하면 JDBC Boolean 매핑이 제대로 동작하지 않을 수 있다. BOOLEAN 용도로 사용할 컬럼은 signed TINYINT(1)로 선언하는 것이 안전하다.
JDBC 연결 문자열 설정
1
2
# TINYINT(1)을 Boolean으로 매핑
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?tinyInt1isBit=true&transformedBitIsBoolean=true
데이터 무결성 보장하기
BOOLEAN 용도로 TINYINT(1)을 사용할 때, 0과 1 외의 값이 저장되는 것을 방지하려면 CHECK 제약조건을 사용할 수 있다.
CHECK 제약조건 (MySQL 8.0.16+)
1
2
3
4
5
6
7
8
9
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
is_active TINYINT(1) NOT NULL DEFAULT 1,
CONSTRAINT chk_is_active CHECK (is_active IN (0, 1))
);
INSERT INTO users (is_active) VALUES (1);
INSERT INTO users (is_active) VALUES (0);
INSERT INTO users (is_active) VALUES (2); -- 실패
에러 메시지:
1
ERROR 3819 (HY000) at line 9: Check constraint 'chk_is_active' is violated.
MySQL 8.0.16 이전 버전에서는 CHECK 제약조건이 파싱되지만 무시된다. 8.0.16 이상에서만 실제로 동작한다.
마치며
MySQL 공식 문서를 통해 다음 사실들을 확인했다.
- BOOLEAN은 TINYINT(1)의 동의어(synonym)다.
- TRUE/FALSE는 1/0의 별칭(alias)이다. -
2 = TRUE가 false임을 예시로 보여준다 - Display Width는 저장 공간과 무관하다.
- BIT(M)의 저장 크기는 (M+7)/8 바이트다.
- MySQL 8.0.17에서 Display Width가 deprecated 되었으나, TINYINT(1)은 JDBC 호환성을 위해 예외다.
- Connector/J의 tinyInt1isBit 옵션이 TINYINT(1)의 Java 타입 매핑을 결정한다.
핵심 원칙을 정리하면
- MySQL의 BOOLEAN은 진짜 Boolean이 아니라 TINYINT(1)이다
- 0과 1 외의 값도 저장 가능하므로 CHECK 제약조건을 고려하자
= TRUE비교와 truthy 체크는 다르게 동작한다- JDBC 매핑 문제를 피하려면 signed TINYINT(1)을 사용하라
데이터베이스의 타입 시스템을 정확히 이해하는 것은 견고한 애플리케이션을 만드는 첫걸음이다.
References
- MySQL 8.0 Reference Manual - 13.1.1 Numeric Data Type Syntax
- MySQL 8.0 Reference Manual - 13.1.2 Integer Types
- MySQL 8.0 Reference Manual - 13.1.6 Numeric Type Attributes
- MySQL 8.0 Reference Manual - 13.7 Data Type Storage Requirements
- MySQL Connector/J Developer Guide - 6.3.8 Result Sets
- MySQL 8.0.19 Release Notes