查的时候能顺手算:+ - * / 用在 select 里,能选出表里根本没存的值。
Demo 里 qty * price 算出 128.00、31.60,price * 0.8 把 299.00 打成 239.20;
3 + 4 * 2 给 11、加括号给 14。
三个「不报错但结果不对」的地方:7 / 2 这里给 3(MySQL 给 3.5000)、
除零给 NULL 不报错、'abc' + 1 给 1。
最后撞上浮点误差:0.1 + 0.2 给 0.30000000000000004,明细合计给 1969.1000000000001,
订单表里存的却是 1969.1。
前三章都在「取」——取列、取名、取符合条件的行。 这一篇开始「算」。 算术运算是聚合函数的地基:先弄清「一行之内怎么算」, 下一篇才好理解「一堆行怎么压成一个数」。
一、四则运算:列能算,常量和列也能算
语法就是在 select 后面写表达式:qty * price、price * 0.8、stock - 40。
+ - * / 四个都能用,还能加括号。
算出来的东西没有名字,所以照例起别名。 不起别名的话,表头就是一长串表达式,导出去没法看。
用在 where 里也合法:WHERE price * 0.8 > 100 是「打完八折还过 100 的」。
但这种写法用不上索引,数据量大时要改成「把条件反过来算」。
二、优先级:先乘除后加减,括号最优先
3 + 4 * 2 给 11,(3 + 4) * 2 给 14。 跟小学数学一模一样,不用记特例。
但有个反直觉的地方:and 的优先级高于 or,而比较运算又高于 and。
混在一起时括号不是可选项,是必需品。
三、除法:这一处两个引擎给的数不一样
本沙箱(SQLite)7 / 2 给 3,因为两边都是整数,它做整数除法。
MySQL 给 3.5000,它的 / 永远给小数。
MySQL 要整除得写 7 DIV 2,取余写 7 % 2 或 MOD(7, 2)。
本沙箱没有 DIV,这条是教材原码,不在这里执行。
想在这边拿到 3.5,把任一边写成小数(7 / 2.0),或者 CAST(7 AS REAL) / 2。
写 SQL 时凡是除法,都要先想清楚要整数还是要小数。
四、三个「不报错但结果不对」
一、除零:10 / 0 给 NULL,不报错。 MySQL 默认也不报错,只记一条警告;
开了严格模式才报错。 所以「除以零」会悄悄变成一个空值流下去。
二、字符串参与算术:'abc' + 1 给 1,'3abc' + 1 给 4。
规则是「能读多少数字算多少,读不到就算 0」。 MySQL 结果一样,但会记警告——
这就是隐式转换最坑的地方。
三、null 参与算术:NULL + 1 还是 NULL。
任何一边是 null,整个表达式就是 null,凑不出别的结果。
五、浮点数:为什么钱不能用 float 存
0.1 + 0.2 给 0.30000000000000004。
这不是数据库的错,是二进制浮点的固有性质——0.1 在二进制里是无限循环小数。
更值得看的是这一组:明细逐行算 qty * price 再求和,给 1969.1000000000001;
而订单表里存的 amount 是 1969.1。 同一笔钱,两条路算出两个数。
所以钱一律用定点小数:MySQL 的 DECIMAL(10,2) 存的是十进制,
不会长尾巴。 本沙箱没有 DECIMAL(只能退化成浮点),上面那点误差就是这么露出来的。
六、动手 Demo:从明细算总价,一路撞到浮点误差
Demo 七段:算总价、打折、比优先级、试除法、撞除零、喂字符串、看浮点。 每一段都只有一句 SQL,重点是盯着输出里的数字对不对。
import _q46 as Q
c = Q.con()
# 明细算总价:qty * price
c.execute("SELECT id, qty, price, qty * price AS total "
"FROM order_detail LIMIT 4")
# 打折、扣库存
c.execute("SELECT name, price, price * 0.8 AS sale FROM goods "
"ORDER BY price DESC LIMIT 3")
c.execute("SELECT name, stock, stock - 40 AS after_sale FROM goods "
"WHERE stock > 0 LIMIT 3")
# 优先级:先乘除后加减
c.execute("SELECT 3 + 4 * 2 AS no_paren, (3 + 4) * 2 AS with_paren")
# 除法:这里给 3,MySQL 给 3.5000
c.execute("SELECT 7 / 2 AS a, 7 / 2.0 AS b, 7 % 2 AS c")
# 除零与 null:都不报错
c.execute("SELECT 10 / 0 AS div_zero, 10 % 0 AS mod_zero, "
"NULL + 1 AS null_add")
# 字符串参与算术:自动转数字
c.execute("SELECT 'abc' + 1 AS s1, '3abc' + 1 AS s2")
# 浮点误差
c.execute("SELECT CAST(0.1 + 0.2 AS TEXT) AS float_err")
c.execute("SELECT CAST(SUM(qty * price) AS TEXT) AS sum_detail, "
"CAST((SELECT SUM(amount) FROM orders) AS TEXT) AS sum_order "
"FROM order_detail")
实跑输出:
--------------------------------------------------------------------
第十五课:查的时候顺手算——算术运算
--------------------------------------------------------------------
四则运算:列和列能算,列和数字也能算
SQL> SELECT id, qty, price, qty * price AS total FROM order_detail LIMIT 4
id qty price total
---------------------------------
1 1 128.00 128.00
2 2 15.80 31.60
3 1 89.90 89.90
4 1 299.00 299.00
(4 行)
**`qty * price` 是查的时候现算的,表里没有 `total` 这一列。**
**四个运算符 `+ - * /` 都能用,还能加括号。**
打折、扣库存:乘一个数、减一个数
SQL> SELECT name, price, price * 0.8 AS sale FROM goods ORDER BY price DESC LIMIT 3
name price sale
-----------------------------
运动鞋 299.00 239.20
足球 156.00 124.80
篮球 128.00 102.40
(3 行)
SQL> SELECT name, stock, stock - 40 AS after_sale FROM goods WHERE stock > 0 LIMIT 3
name stock after_sale
----------------------------------
篮球 50 10
足球 30 -10
羽毛球拍 80 40
(3 行)
**第二段第二个数出现负数(足球 30 减 40 得 -10)**——
**算术运算不会替你检查业务合理性,该拦的地方要用 `where` 或程序判。**
优先级:先乘除后加减,括号最优先
SQL> SELECT 3 + 4 * 2 AS no_paren, (3 + 4) * 2 AS with_paren
no_paren with_paren
------------------------
11 14
(1 行)
**11 和 14,差在括号上。** **这跟小学数学一模一样,不用记特例。**
除法:这里给整数,MySQL 给小数
SQL> SELECT 7 / 2 AS a, 7 / 2.0 AS b, 7 % 2 AS c
a b c
-----------------
3 3.50 1
(1 行)
**`7 / 2` 这里给 3,MySQL 给 3.5000。**
**两个引擎对整数除法的默认行为不同,这一条最容易在迁移时炸。**
**MySQL 要整除得写 `7 DIV 2`,取余写 `7 % 2` 或 `MOD(7, 2)`。**
**本沙箱没有 `DIV`,上面那句 `7 DIV 2` 是教材原码,不在这里执行。**
**想在这边拿到 3.5,把任一边写成小数**(`7 / 2.0`),**或者 `CAST(7 AS REAL) / 2`。**
除零和 null:都不报错,都给 null
SQL> SELECT 10 / 0 AS div_zero, 10 % 0 AS mod_zero, NULL + 1 AS null_add
div_zero mod_zero null_add
-----------------------------------
NULL NULL NULL
(1 行)
**MySQL 默认也不报错,给 NULL 加一条警告**(`sql_mode` 开着严格模式才报错)。
**所以「除以零」在 SQL 里不会中断流程,只会悄悄变成空值。**
字符串参与算术:自动转成数字
SQL> SELECT 'abc' + 1 AS s1, '3abc' + 1 AS s2
s1 s2
----------
1 4
(1 行)
**`'abc'` 转成 0,`'3abc'` 转成 3(读到第一个非数字就停),所以给 1 和 4。**
**MySQL 结果一样,但会记一条警告。**
**这是隐式转换最坑人的地方**:不报错、结果还挺「合理」,**线上就变成一个查不出来的错数。**
浮点数:算出来的钱不是整数分
SQL> SELECT CAST(0.1 + 0.2 AS TEXT) AS float_err
float_err
--------------------
0.30000000000000004
(1 行)
SQL> SELECT CAST(SUM(qty * price) AS TEXT) AS sum_detail, CAST((SELECT SUM(amount)
FROM orders) AS TEXT) AS sum_order FROM order_detail
sum_detail sum_order
----------------------------------
1969.1000000000001 1969.1
(1 行)
**`0.1 + 0.2` 给 `0.30000000000000004`,不是 0.3。**
**明细合计给 `1969.1000000000001`,订单表里存的却是 `1969.1`。**
**两个数差在小数点后第 13 位,但用 `=` 比就永远不相等。**
**所以钱别用浮点存——MySQL 用 `DECIMAL(10,2)`。**
**本沙箱没有 DECIMAL,只能用 REAL 代替,上面那点误差就是这么露出来的。**
第一段算总价。 1 × 128.00、2 × 15.80,total 这一列是现算的,表里没有。
第二段打折与扣库存。 299.00 打八折给 239.20;
足球库存 30 减 40 给 -10,负数照样算出来,没人拦你。
第三段优先级。 11 和 14,差的就是括号。
第四段除法。 7 / 2 给 3、7 / 2.0 给 3.50、7 % 2 给 1。
同一句搬到 MySQL,第一个数会变成 3.5000。
第五段除零。 三列全是 NULL,一句报错都没有。
第六段字符串。 'abc' + 1 给 1,'3abc' + 1 给 4。
第七段浮点。
0.30000000000000004 和 1969.1000000000001 两个尾巴,就是「钱别用浮点存」最直白的证据。
30 秒实验
- 把
qty * price改成price * qty,结果会变吗?为什么? SELECT 10 / 4与SELECT 10 / 4.0分别给什么?跟你想的一样吗?SELECT CAST(SUM(qty * price) AS TEXT)那串尾巴,换成先算整数分再除 100 还在吗?
常见误区
- 以为
/一定给小数。 整数除整数在不同引擎给不同结果,迁移时这一处最容易被漏掉。 - 用 float 存金额。 单行看不出问题,一旦求和或比较相等就露馅,而且这种错不会报错, 只会算错钱。
- 以为除零会中断。 它给 NULL 继续往下跑,于是「除以零」变成一个藏在结果里的空值。
自测三问(点开对答案)
1. `7 / 2` 在 MySQL 和本沙箱分别给什么? —— MySQL 给 3.5000,本沙箱给 3(整数除法)。MySQL 要整除写 `7 DIV 2`。 2. `'abc' + 1` 为什么不报错? —— SQL 会把字符串隐式转成数字,读不到数字就当 0,所以给 1;MySQL 一样,但会记警告。 3. 金额字段为什么不用 float? —— 二进制浮点存不下 0.1 这类十进制小数,求和后会长出尾巴,比较相等还会失败。用 DECIMAL。一行之内能算了。
下一篇把这些运算套到「一堆行」上——sum、avg、max、min、count 五个聚合函数。
- ☐ (本篇完,下一篇 4.2 聚合函数)