我已经不是第一次遇到关于 TINYINT
的问题了。在 MySQL 中,当我们将某个字段设置为 TINYINT
,随着业务的扩展,我们可能会发现 TINYINT
的范围无法满足需求。这时需要修改字段属性。但如果表的数据量很大,或者由于分表导致涉及的表数量众多,这个过程可能会变得比较复杂。更糟糕的是,如果代码中的数据类型为 int
,而 MySQL 中的数据类型为 TINYINT
,可能会导致存储异常数据。
这里把 MySQL 整数类型做个总结:
类型名称 | 翻译 | 空间占用 | 范围(有符号) | 范围(无符号) |
---|---|---|---|---|
TINYINT | 很小的整数 | 1个字节 | -128〜127 | 0 〜255 |
SMALLINT | 小的整数 | 2个宇节 | -32768〜32767 | 0〜65535 |
MEDIUMINT | 中等大小的整数 | 3个字节 | -8388608〜8388607 | 0〜16777215 |
INT (INTEGHR) | 普通大小的整数 | 4个字节 | -2147483648〜2147483647 | 0〜4294967295 |
BIGINT | 大整数 | 8个字节 | -9223372036854775808〜9223372036854775807 | 0〜18446744073709551615 |
TINYINT 与 INT:空间节省的实际影响
如上表所示,使用 TINYINT
替代 INT
可以节省 3 字节的存储空间。
换句话说,如果你有一个包含 1,000,000 行的表,并且其中有一个 INT
类型的列。将此列更改为 TINYINT
,将大约节省 3MB 的存储空间(1,000,000 行 * 3 字节/行 = 3,000,000 字节 ≈ 3MB)。但这种节省在大多数情况下可能并不会带来显著的性能提升或成本降低。
何时应考虑使用 TINYINT
对于这个问题,我认为没有必要过于纠结,也没有绝对的答案。
有时候,我们选择使用 TINYINT
主要是出于教条式的成本考虑,但如前所述,这种成本差异几乎可以忽略。因此,决定何时使用 TINYINT
,我们只需要考虑其范围是否满足当前业务需求以及可能的未来发展。
例如,如果你需要添加一个表示性别或者 true
/false
的字段,这种情况下,数据范围较小,且不可能超过 TINYINT
的范围,那么使用 TINYINT
是合适的。然而,对于如订单来源或状态等可能会发生变化的字段,我们需要更加谨慎,因为这些场景是典型的随着业务的发展可能会修改 TINYINT
字段的情况,这也是我在实际工作中遇到的最常见的场景。
解读公司内部《MySQL开发规范》
公司内部的《MySQL开发规范》 中有这么一段说明:
禁止使用tinyint ,在JAVA环境有转换问题。区分使用TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT数据类型和取值范围(TINYINT>SMALLINT>MEDIUMINT>INT>BIGINT>DECIMAL—存储空间逐渐变大,而性能却逐渐变小)。
禁止使用 TINYINT
这个问题的关键在于 MySQL 中的 TINYINT
类型和 Java 中的数据类型之间的映射关系。
先看一个例子:
package blog.dongguabai.others.mysql_tinyint;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @author dongguabai
* @date 2023-11-30 00:27
*/
public class Test {
public static String url = "jdbc:mysql://10.224.221.151:3306/test_1";
public static String user = "root";
public static String password = "root";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS tinyint_test");
stmt.execute("CREATE TABLE tinyint_test (id TINYINT UNSIGNED, id_signed TINYINT, id_boolean TINYINT(1))");
stmt.execute("INSERT INTO tinyint_test VALUES (255, -128, 1)");
try (ResultSet rs = stmt.executeQuery("SELECT * FROM tinyint_test")) {
while (rs.next()) {
System.out.println("id: " + rs.getObject("id").getClass().getName());
System.out.println("id_signed: " + rs.getObject("id_signed").getClass().getName());
System.out.println("id_boolean: " + rs.getObject("id_boolean").getClass().getName());
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出结果:
id: java.lang.Integer
id_signed: java.lang.Integer
id_boolean: java.lang.Boolean
可以得出如下结论:
-
TINYINT
转换为Integer
在 MySQL 中,
TINYINT
是一个 8 位的整数,可以是有符号的(取值范围 -128 到 127)或无符号的(取值范围 0 到 255)。在 Java 中最接近的类型是byte
,也是 8 位的,但它是有符号的,取值范围是 -128 到 127。这意味着TINYINT
的取值范围可能超过了 JavaByte
类型的取值范围。因此,JDBC 默认将TINYINT
类型转换为 Java 的Integer
类型,而不是Byte
类型 -
TINYINT(1)
转换为Boolean
在 MySQL 中,
TINYINT(1)
通常用于表示布尔值,JDBC 将其转换为 Java 的Boolean
类型。(可以在 JDBC 连接字符串中添加
tinyInt1isBit=false
参数,这样TINYINT(1)
就不会被转换为Boolean
,而是和其他TINYINT
类型一样被转换为Integer
)
原理可以从 com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)
中找到:
@Override
public Object getObject(int columnIndex) throws SQLException {
...
Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
switch (field.getMysqlType()) {
//TINYINT(1)转化为 BIT
case BIT:
// TODO Field sets binary and blob flags if the length of BIT field is > 1; is it needed at all?
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;
if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}
return obj;
}
return data;
}
return field.isSingleBit() ? Boolean.valueOf(getBoolean(columnIndex)) : getBytes(columnIndex);
case BOOLEAN:
return Boolean.valueOf(getBoolean(columnIndex));
//TINYINT 转化为 Integer
case TINYINT:
return Integer.valueOf(getByte(columnIndex));
case TINYINT_UNSIGNED:
case SMALLINT:
case SMALLINT_UNSIGNED:
case MEDIUMINT:
case MEDIUMINT_UNSIGNED:
case INT:
return Integer.valueOf(getInt(columnIndex)); //TINYINT_UNSIGNED 转化为 Integer
...
}
这里要注意数据库字段设置为 TINYINT(1)
在 columnDefinition
会被映射为 MysqlType.BIT
。
存储空间逐渐变大,而性能却逐渐变小
存储空间和性能之间的关系可以从两个方面来理解:
-
存储空间:
TINYINT
、SMALLINT
、MEDIUMINT
、INT
和BIGINT
这些类型的存储空间从小到大排列。这是因为它们能表示的数值范围也是从小到大的。例如,TINYINT
只需要 1 字节就可以表示 -128 到 127 的整数,而BIGINT
需要 8 字节来表示更大范围的整数。 -
性能:当我们说性能逐渐变小,我们是指处理更大的数据类型通常需要更多的 CPU 时间和内存。例如,读取和写入一个
BIGINT
值通常会比读取和写入一个TINYINT
值需要更多的时间。这是因为处理更大的数据需要更多的计算资源。但这些不同的整数类型在性能上的差异是微不足道的。这是因为现代的 CPU 和内存都足够快了。
References
- MySQL开发规范(内网)
欢迎关注公众号: