今天这篇文章,我和你聊一聊在 MySQL 中如何存储货币的方法。你可能已经知道,在 SQL Server 数据库中是有专用的 money 数据类型的,还支持货币符号,非常好用。但是在 MySQL 中没有这样的实现,所以我们就要另辟蹊径去解决它。
1. 高精度的数据类型都有哪些?
如果不考虑数据的精确度和使用的便利程度、性能、存储等条件,在 MySQL 中可以存储数值的数据类型其实有很多,例如整形数据类型 INT、BIGINT,定点数 DECIMAL,浮点数 FLOAT、DOUBLE,甚至 VARCHAR 都能拿来存储数值。
那哪种数据类型适合存储货币呢?
首先 FLOAT 和 DOUBLE 这两个数据类型是一定要排除的。
由于这两个类型都是浮点数据类型,不够精确,在运算时无法保证结果的正确性。
例如 M*G/G 的结果不一定等于 M。
DECIMAL 是一种高精度的定点数据类型,在 MySQL 中它还有 1 个同义词:NUMERIC,这两个数据类型在 MySQL 中被视为相同的类型。
目前业内大多都是采用这种数据类型来保存财务数据等对精度要求非常高的数据。
不过凡事无绝对,你也可以把财务数据使用 BIGINT 的数据类型做存储。假设存储的数据要精确到小数点后两位,则把金额乘以 100 之后存储即可,取出时再除以 100。
再说下 INT 数据类型,由于这个数据类型的存储空间只有 4 个字节,数值范围不一定能满足财务数据的需求,因此 INT 数据类型比较适合存储游戏金币、网站代币等要求高精度但数值范围不大的数据。
那 VARCHAR 这个数据类型是否能存储货币呢?
我个人是不太建议使用 VARCHAR 这个数据类型存储货币值。
一是缺少约束,除了数字以外其他乱七八糟的字符都可以存进去;二是使用了 VARCHAR 就不能使用 MySQL 内置的一些数字函数(MIN, MAX, SUM 等),会有一些意料之外的情况出现。
下面我们就做个实验来看一下,在 VARCHAR 数据类型中使用数字函数的问题。
先创建一个包含有 VARCHAR 数据类型的表,在表中插入几行字符型的数值数据:
mysql> create table varchar_test (col_1 varchar(10));
Query OK, 0 rows affected (0.01 sec)
mysql> insert into varchar_test values('0'),('1'),('2'),('9'),('10');
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0
mysql> select max(col_1) from varchar_test;
+------------+
| max(col_1) |
+------------+
| 9 |
+------------+
1 row in set (0.00 sec)
可以看到 MAX 函数给出的结果是不正确的,其他的数字函数你可以自行测试一下。
因此不建议使用 VARCHAR 数据类型存储货币值。
2. DECIMAL 数据类型的使用
DECIMAL 和 NUMRIC 在 MySQL 内部被实现为同样的类型,用来保存高精度的数值。
在对列进行定义的时候,可以指定它的精度和标度:
DECIMAL(M,D)
-
M 是精度,也就是数据的总长度(这不包括小数点和负号)
-
D 是标度,也就是小数点后的保留长度
DECIMAL 数据类型支持的 M 的最大值为 65,D 是 30,并且 D 的长度只能小于或等于 M。
如果你在创建数据列的时候没有指定精度和标度,默认按照 DECIMAL (10,0) 处理。
3. DECIMAL 数据类型的使用限制与注意事项
第一,MySQL 的 DECIMAL 数据类型不能存储货币的符号(也就是 $、¥等货币符号)。
因此如果你的财务数据包含多币种的话,需要另加一个字段存储币种。
第二,如果插入的值的精度高于实际定义的精度,系统会自动四舍五入处理,使插入的值符合我们的定义。
在操作 DECIMAL 数据类型的字段时,MySQL 会四舍五入把超出范围的数据截断。
因此设置标度的时候要足够的长,留足小数点后的空间,免得四舍五入造成财务数据不准确。
因为财务妹子对账的要求可是一分钱都不能错的,你自掏腰包把钱补给她都不行,就是非常的严格。
下面我们就做个实验看一下 DECIMAL 在标度不够时的表现。
先建一张包含 decimal (5,1) 的表,插入 3 行不同长度的数据:
mysql> create table decimal_test(col_1 int, col2 decimal(5,1));
Query OK, 0 rows affected (0.01 sec)
mysql> insert into decimal_test values(1,10.1),(2,10.11),(3,10.16);
Query OK, 3 rows affected, 2 warnings (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 2
mysql> select * from decimal_test;
+-------+------+
| col_1 | col2 |
+-------+------+
| 1 | 10.1 |
| 2 | 10.1 |
| 3 | 10.2 |
+-------+------+
3 rows in set (0.00 sec)
在这个实验中,你可以观察到由于操作的数值超出了标度的范围,后面的两行数据都四舍五入了。
因此在设计货币相关的字段时请务必留出足够的空间。
4. DECIMAL 数据类型性能相关
在早期的 MySQL 版本中,就支持了 DECIMAL 的数据类型。
但是由于 CPU 并不支持对 DECIMAL 的直接计算,当时的 MySQL 数据库使用了浮点运算来实现了这个数据类型,因此并不精确。
直到 5.0 版本开始,MySQL 才真正实现了 DECIMAL 数据类型的精确计算,真正能用在财务系统中。
也正是因为和 DECIMAL 相关的计算都是由 MySQL 来处理的,因此在对 DECIMAL 相关的列进行计算时,性能不如 CPU 直接支持的浮点数和整数计算的快。
因此在合适的情况下,也可以考虑选用 BIGINT 的数据类型,它能同时避免浮点数计算不精确和 DECIMAL 计算代价高的问题,不过你也需要同时处理和小数点相关的问题。
5. 小结
今天,我给你介绍了如何在 MySQL 中存储财务数据等要求高精度数据类型的场景,并介绍了两种可行的方案:BIGINT 和 DECIMAL。
在存储财务数据时,DECIMAL 是这几种数据类型中使用起来最方便的,它直接支持高精度小数的存储。
但是由于 CPU 不能直接对 DECIMAL 进行计算,因此理论上计算效率要稍低一些。
BIGINT 数据类型是一个不错的平衡,它能直接利用 CPU 进行计算,也能存储足够大的数值。
但是由于 BIGINT 是整数类型,在存取的时候如果有小数点,要处理小数点相关的问题。所以在使用时要在公司内部做好上下游的沟通,建立代码规范,避免上游或下游消费数据时出错。
综合来说,建议首选 DECIMAL 这个数据类型,其次是 BIGINT。
参考文献:
- MySQL 官方文档:decimal 数据类型,https://dev.mysql.com/doc/refman/5.7/en/fixed-point-types.html
- 《MySQL 技术内幕:SQL 编程》,姜承尧著