PARTITION BY <分区表达式>(可选):将结果集分成多个分区,窗口函数会在每个分区内独立执行。分区表达式可以是一个或多个列名,用于确定如何将结果集分成不同的分区。
ORDER BY <排序表达式> ASC | DESC(可选):指定窗口内行的排序顺序。排序表达式可以是一个或多个列名,用于确定窗口内行的排序方式。
ROWS/Range <行范围>(可选):定义窗口的行范围。行范围可以是固定的行数(如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW),也可以是相对于当前行的动态范围(如ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,表示从窗口开始到当前行的所有行)。
常用的窗口函数:
分类
函数名
作用
例子
序号/排名
ROW_NUMBER()
唯一序号 (1, 2, 3, 4)
给每个订单编个号,即使金额一样也不能重复
RANK()
跳跃排名 (1, 2, 2, 4)
并列第一,下一个就是第三名,相同的值会留下空位
DENSE_RANK()
紧凑排名 (1, 2, 2, 3)
并列第一,下一个是第二名。不会为相同的值留下空位 (适合发奖状)
错位/偏移
LAG(col, n)
往前看 n 行
这一单比上一单多花多少钱?
LEAD(col, n)
往后看 n 行
这一单比下一单少花多少钱?
分布/切片
NTILE(n)
切成 n 份
把用户分成“高/中/低”消费三档
聚合窗口
SUM(), AVG()
累计/组内统计
算出用户的累计消费流水
上手使用窗口函数
还记得那个富豪榜吗
1 2 3 4 5 6 7
SELECT u.username, SUM(o.total_amount) as money FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.username ORDERBY money DESC;
select u.username, SUM(o.total_amount) as money, RANK() OVER(ORDERBYSUM(o.total_amount) DESC) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.username ORDERBY money DESC;
select u.username, SUM(o.total_amount) as money, RANK() OVER(ORDERBYSUM(o.total_amount) DESC) as rank FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.username ORDERBY mone
报错内容为:
1
ERROR: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'rank
这不是Mysql版本的问题,而是这个rank和窗口函数RANK()冲突了
这就像在 Python 里写 print = 1 或者在 Java 里写 int class = 5 一样,不能用系统自带的命令名来给变量起名。
如果不想换名字非要用rank?必须用 反引号把它包起来
1 2 3 4 5 6 7 8 9
SELECT u.username, SUM(o.total_amount) as money, -- 加了反引号 `rank`,告诉 MySQL 这是别名,不是关键字 RANK() OVER(ORDERBYSUM(o.total_amount) DESC) as `rank` FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.username ORDERBY money DESC;
SELECT u.city, u.username, IFNULL(SUM(o.total_amount),0) as money, RANK() OVER(ORDERBYSUM(o.total_amount) DESC) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id WHERE u.city = "东莞市" GROUPBY u.city, u.username LIMIT 3;
SELECT u.city, IFNULL(SUM(o.total_amount),0) as money, RANK() OVER(ORDERBYSUM(o.total_amount) DESC) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.city, u.username LIMIT 3;
SELECT*FROM( SELECT u.city, u.username, IFNULL(SUM(o.total_amount),0) as money, RANK() OVER( PARTITIONBY u.city ORDERBYSUM(o.total_amount) DESC ) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.city, u.username )AS t WHERE t.ranklist <=3AND t.money >0;
select name,details->>'$.color'as color from products;
执行后的输出是这样的
1 2 3 4 5 6 7
+-----------+-------+ | name | color | +-----------+-------+ | T-Shirt | white | | iPhone 15 | black | +-----------+-------+ 2 rows in set (0.0020 sec)
或者我要查询详情中包含 “storage” 属性的商品
1
select*from products where details->>'$.storage'isnot null;
执行后的输出是这样的
1 2 3 4 5 6
+----+-----------+---------------------------------------------------------+ | id | name | details | +----+-----------+---------------------------------------------------------+ | 2 | iPhone 15 | {"color": "black", "screen": "6.1", "storage": "256GB"} | +----+-----------+---------------------------------------------------------+ 1 row in set (0.0005 sec)
或者如果要查找json的某个数组中包含某个值的行,使用JSON_CONTAINS()
1
select*from products where json_contains(details,'"summer"',"$.tags");
执行后的输出结果是这样的
1 2 3 4 5 6
+----+---------+---------------------------------------------------------------+ | id | name | details | +----+---------+---------------------------------------------------------------+ | 1 | T-Shirt | {"size": "L", "tags": ["summer", "casual"], "color": "white"} | +----+---------+---------------------------------------------------------------+ 1 row in set (0.0010 sec)
修改json数据
不要把整个 JSON 拿出来改完再覆盖回去,使用原生函数效率更高。
比如我要给所有商品增加一个NB属性,并把color设置成blue
1
update products set details = json_set(details,"$.color","blue","$.NB","true");
WITH cte_name (列名1, 列名2, ...) AS ( -- 这里写你的子查询 SELECT ... ) -- 紧接着写主查询,可以使用上面的 cte_name SELECT*FROM cte_name;
使用CTE
还记得上面那个每个城市消费前三的例子吗
1 2 3 4 5 6 7 8 9 10 11 12 13 14
SELECT*FROM( SELECT u.city, u.username, IFNULL(SUM(o.total_amount),0) as money, RANK() OVER( PARTITIONBY u.city ORDERBYSUM(o.total_amount) DESC ) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.city, u.username )AS t WHERE t.ranklist <=3AND t.money >0;
如果把它换成CTE写法就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-- 1. 先定义“带排名的用户列表” WITH UserRanks AS ( SELECT u.city, u.username, IFNULL(SUM(o.total_amount),0) as money, RANK() OVER(PARTITIONBY u.city ORDERBYSUM(o.total_amount) DESC) as ranklist FROM users AS u LEFTJOIN orders AS o ON u.id = o.user_id GROUPBY u.city, u.username ) -- 2. 再对这个列表进行筛选 SELECT* FROM UserRanks WHERE ranklist <=3AND money >0;
看着好像没什么差别?确实,在这个简单例子里,确实只是“排版”的区别。
分步聚合
我要找出总消费金额 > 5000的用户,并列出他们的姓名、电话、城市以及具体的消费总额。
先写一个查询列出所有用户消费金额
1 2 3 4
select user_id, SUM(total_amount) AS total_spent FROM orders GROUPBY user_id;
然后把这表作为一个UserSpent临时表,和用户表内连接起来就是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
WITH UserSpent AS ( select user_id, SUM(total_amount) AS total_spent FROM orders GROUPBY user_id )
SELECT u.username, u.city, u.phone, us.total_spent FROM users AS u INNERJOIN UserSpent AS us ON u.id = us.user_id WHERE us.total_spent >5000 ORDERBY us.total_spent DESC;
再用到窗口函数,给他加个排名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
WITH UserSpent AS ( select user_id, SUM(total_amount) AS total_spent FROM orders GROUPBY user_id )
SELECT u.username, u.city, u.phone, us.total_spent, RANK() OVER(ORDERBY us.total_spent DESC) as `rank` FROM users AS u INNERJOIN UserSpent AS us ON u.id = us.user_id WHERE us.total_spent >5000 ORDERBY us.total_spent DESC;
SELECT DATE_FORMAT(created_at, '%Y-%m') AS sale_month, SUM(total_amount) AS total_money FROM orders GROUPBY DATE_FORMAT(created_at, '%Y-%m');
然后把这个查询作为一个CTE,在另一个CTE里使用窗口函数创建一个上一个月的列
1 2 3 4 5 6 7 8 9 10 11 12 13 14
WITH MonthlyStats AS ( SELECT DATE_FORMAT(created_at, '%Y-%m') AS sale_month, SUM(total_amount) AS total_money FROM orders GROUPBY DATE_FORMAT(created_at, '%Y-%m') ), GrowthStats AS ( SELECT sale_month, total_money, LAG(total_money, 1) OVER (ORDERBY sale_month) AS last_month_money FROM MonthlyStats )
WITH MonthlyStats AS ( SELECT DATE_FORMAT(created_at, '%Y-%m') AS sale_month, SUM(total_amount) AS total_money FROM orders GROUPBY DATE_FORMAT(created_at, '%Y-%m') ), GrowthStats AS ( SELECT sale_month, total_money, LAG(total_money, 1) OVER (ORDERBY sale_month) AS last_month_money FROM MonthlyStats ) SELECT sale_month, total_money, last_month_money, CONCAT(ROUND((total_money - last_month_money) / last_month_money *100, 2), '%') AS growth_rate FROM GrowthStats ORDERBY sale_month;
SELECT DATE_FORMAT(created_at, '%Y-%m') AS sale_month, SUM(total_amount) AS total_money, LAG(SUM(total_amount), 1) OVER (ORDERBY DATE_FORMAT(created_at, '%Y-%m')) AS last_month_money, CONCAT(ROUND((SUM(total_amount) -LAG(SUM(total_amount), 1) OVER (ORDERBY DATE_FORMAT(created_at, '%Y-%m')) ) /LAG(SUM(total_amount), 1) OVER (ORDERBY DATE_FORMAT(created_at, '%Y-%m')) *100, 2), '%') AS growth_rate FROM orders GROUPBY DATE_FORMAT(created_at, '%Y-%m');
CREATETRIGGER koukucun BEFORE INSERT ON order_items FOREACHROW BEGIN UPDATE products SET products.stock = products.stock - NEW.quantity WHERE NEW.product_id = products.id; END $$
CREATETRIGGER koukucun BEFORE INSERT ON order_items FOREACHROW BEGIN -- 1. 定义一个变量存当前库存 DECLARE current_stock INT; -- 2. 查出当前商品的库存 SELECT stock INTO current_stock FROM products WHERE id = NEW.product_id; -- 明确指定 id 是产品的 id -- 3. 判断库存够不够 IF current_stock < NEW.quantity THEN -- 4. 如果不够,抛出异常!这会中断 INSERT 操作,订单创建失败 SIGNAL SQLSTATE'45000' SET MESSAGE_TEXT ='库存不足,无法下单'; ELSE -- 5. 如果够,执行扣减 UPDATE products SET stock = stock - NEW.quantity WHERE id = NEW.product_id; END IF;
END $$
DELIMITER ;
这样如果库存不足的话,INSERT语句就会直接创建失败了
使用触发器
现在products表里随便找个商品看看库存有多少个?
1 2 3 4 5 6 7
MySQL localhost:3306 ssl mall_db_simple SQL > select * from products limit 1; +----+-----------+----------+--------+-------+--------+ | id | name | category | price | stock | status | +----+-----------+----------+--------+-------+--------+ | 1 | 零食-世界 | 零食 | 535.00 | 999 | 1 | +----+-----------+----------+--------+-------+--------+ 1 row in set (0.0043 sec)
-- 2. 查出当前商品的库存 SELECT stock INTO current_stock FROM products WHERE products.id = NEW.product_id;
-- 3. 判断库存够不够 IF current_stock < NEW.quantity THEN -- 4. 如果不够,抛出异常!这会中断 INSERT 操作,订单创建失败 SIGNAL SQLSTATE'45000' SET MESSAGE_TEXT ='库存不足,无法下单'; ELSE -- 5. 如果够,执行扣减 UPDATE products SET stock = stock - NEW.quantity WHERE id = NEW.product_id; END IF;
CREATEPROCEDURE recharge_and_bonus(IN p_user_id INT, IN p_amount DECIMAL(10,2)) BEGIN -- 1. 充钱 UPDATE users SET balance = balance + p_amount WHERE id = p_user_id; -- 2. 送钱 IF p_amount >=100THEN UPDATE users SET balance = balance +5WHERE id = p_user_id; -- 再送5元! END IF; -- 3. 返回最新余额 SELECT balance, points FROM users WHERE id = p_user_id; END $$