复杂的多表查询——以超市交易数据为例
之前的内容基本上都是基于单表进行的查询操作,但是在实际工作中,数据往往分散在多个表中,这个时候我们就需要用到多表查询的知识了。
通常来说,多表查询主要有两类:一类是纵向的表合并,也就是将结构相同的表上下拼接起来;另一类是横向的表连接,即将多个表中的字段合并到一张大表中。
(1)纵向表合并
纵向表合并非常好理解,就是把多张相同结构的表按照垂直的方向,将它们进行合并,直白的理解就是上下堆叠(也就是记录的追加)。下面我们以某超市的经营数据为例,来学习一下纵向表合并。
假设有一个大型连锁超市,它有很多家加盟店,各个加盟店将不同月份的经营数据存储在各自的数据表中。每家店数据的存储方式都相同,也就是字段都一样,分别为:门店ID、用户ID(如果没有办理会员,则用户ID为空)、订单ID、交易日期、应付金额、折扣金额、实付金额以及支付类型。
现在的需求是需要把A、B、C三个超市的交易记录合并到一张表里面。
纵向合并表需要用到UNION或者UNION ALL关键词,这两个关键词的功能是一样的(都是合并操作),但是还是有很大区别的:
- UNION ALL 在合并表的时候不做任何附加动作,只是将多个表格简单的首尾相连;
- UNION 合并表格的时候,除了拼接之外还会多一个附加动作——去重(以前旧版本还有排序功能,新版本舍弃了排序功能)
在性能方面,UNION ALL的合并速度要比UNION 快的多,尤其是数据量比较大的时候,两者的合并速度差异还是非常明显的。所以在做纵向表合并的时候,一定要考虑清楚是否需要做去重处理。
# UNION 和 UNION ALL 的区别
SELECT pay_type FROM TransC1805
UNION ALL SELECT
pay_type FROM transD1810;
SELECT pay_type FROM TransC1805
UNION
SELECT pay_type FROM transD1810;
下面尝试使用UNION ALL 将三张交易表合并成一个表:
SELECT * from TransA1710
UNION ALL
SELECT * from TransB1801
UNION ALL
SELECT * FROM TransC1805;
当需要合并的表格的字段数量和顺序都一样的时候,这样写是没有问题的。如果其中有一个表的结构不一致(包括数量和排布顺序),这样写就会出问题了。比如,现在有超市D在2018年10月份的交易数据,但是这个门店在记录数据的时候,字段顺序做了一些调整:
# 如果按照上面的方式合并,得到的结果就会有问题:会按照错乱的顺序直接拼接
SELECT * FROM TransC1805
UNION ALL
SELECT * FROM transD1810;
# 正确的写法
SELECT * FROM TransC1805
UNION ALL
SELECT shop_id,uid,order_id,date,amt1,amt2,amt3,pay_type FROM transD1810;
SELECT shop_id,uid,order_id,idate,amt1,amt2,amt3,pay_type FROM TransC1805
UNION ALL
SELECT shop_id,uid,order_id,date,amt1,amt2,amt3,pay_type FROM transD1810;
当要合并的表的字段顺序不一致的时候,手动将所有需要的字段都写出来,并且保证顺序一致。
有时候我们并不需要将表中所有记录和字段都合并,这个时候可以更加灵活使用UNION ALL来完成,比如:将3张表中支付方式为现金(pay_type=1)的交易合并起来,并且保留门店ID、用户ID、交易订单号、交易时间和实际交易额信息。
# 将3张表中支付方式为现金(pay_type=1)的交易合并起来,并且保留门店ID、用户ID、交易订单号、交易时间和实际交易额信息。
SELECT shop_id,uid,order_id,idate,amt3 from TransA1710
where pay_type=1
UNION ALL
SELECT shop_id,uid,order_id,idate,amt3 from TransB1801
where pay_type=1
UNION ALL
SELECT shop_id,uid,order_id,idate,amt3 FROM TransC1805
where pay_type=1;
(2)表连接操作
关于表连接操作就稍微有点复杂了,这里会用到各种 JOIN操作,对于初学者来说,特别容易搞混,下面我们通过两个简单的数据集来学习一下JOIN连接操作:
CREATE TABLE TA(K INT, V VARCHAR(10));
INSERT INTO TA VALUES
(1,"AB"), (2,"A");
SELECT * FROM TA;
CREATE TABLE TB(K INT, V VARCHAR(10));
INSERT INTO TB VALUES
(1,"AB"),
(3,"B");
SELECT * FROM TB;
K=1的记录在TA和TB两张表中都有,K=2的记录为TA表中独有,K=3的记录为TB表中独有。
- 内连接:INNER JOIN
内连接就是把两张表中共有的数据提取出来。
文氏图:
#内连接
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V
FROM TA
INNER JOIN TB
ON TA.K=TB.K;
- 左连接:LEFT JOIN
左连接查询会返回左表(TA)中所有记录,不管右表中有没有关联的数据,如果右表中有关联的数据会被一并返回(右表中没有的字段,则以NULL值填充)。
文氏图:
#左连接
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V
FROM TA #左表
LEFT JOIN TB #右表
ON TA.K=TB.K;
- 右连接:
右连接查询会返回右表(TB)中所有记录,不管左表中有没有关联的数据,如果左表中有关联的数
据会被一并返回(左表中没有的字段,则以NULL值填充)
文氏图:
#右连接
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V
FROM TA
RIGHT JOIN TB
ON TA.K=TB.K;
- 全连接:FULL OUTER JOIN
FULL OUTER JOIN 一般称为全连接或者外连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN 。外连接查询能返回左右表里的所有记录,其中左右表里能关联起来的记录被连接后
返回。
文氏图:
# MySQL中不支持FULL OUTER JOIN,直接使用会报错
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V
FROM TA
FULL OUTER JOIN TB
ON TA.K=TB.K;
# 可以使用UNION 模拟全连接返回的结果
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V FROM TA
LEFT JOIN TB
ON TA.K=TB.K
UNION
SELECT TA.K AS A_K,TB.K AS B_K,TA.V AS A_V,TB.V AS B_V FROM TA
RIGHT JOIN TB
ON TA.K=TB.K;
以上就是SQL中常见的四种表连接方式了。此外,还有三种延伸用法,大家感兴趣的话,可以自行研究一下。
(3)综合案例——校园一卡通数据的表连接操作
下面我们通过一个综合案例来学习一下SQL的表连接操作。这里使用的数据是关于校园一卡通的流水记录,数据来源于DC竞赛网,此数据集共包含两部分,分别是学生的图书借阅记录(共包含239947 条数据)和消费记录(共包含 条数据)。
- 将数据集导入MySQL中
# 创建学生图书借阅记录表
CREATE TABLE stu_borrow
(stu_id VARCHAR(10),
borrow_date DATE,
book_title VARCHAR(500),
book_number VARCHAR(50) );
# 导入图书借阅数据
LOAD DATA local INFILE "/Users/zhucan/Desktop/borrow.csv"
INTO TABLE stu_borrow
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n';
# 创建学生的一卡通消费表
CREATE TABLE stu_card(
stu_id VARCHAR(10),
custom_class VARCHAR(10),
custom_add VARCHAR(20),
custom_type VARCHAR(20),
custom_date DATETIME,
amt FLOAT,
balance FLOAT );
# 导入消费数据
LOAD DATA local INFILE '/Users/zhucan/Desktop/card.txt'
INTO TABLE stu_card111
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n';
- 简单数据探索
数据全部导入MySQL之后,需要对数据进行探索,了解数据现状:
# 查询某位同学的借书记录
SELECT * FROM stu_borrow
WHERE stu_id = '9708'
ORDER BY borrow_date;
从返回结果来看,图书借阅记录表存在重复记录,根据实际业务情况,这样的数据是不应该出现的,所以后续进行数据处理的时候,需要做去重处理。
# 查询某位学生的消费记录
SELECT * FROM stu_card
WHERE stu_id = '1040'
ORDER BY custom_date;
同样,对于消费记录来说,也存在重复值,后续数据处理需要做去重操作,并且,消费金额还有复数,这里我们理解为充值行为(因为负的消费金额会导致余额的增加)。
- 数据处理
由于两个表中都有重复值,我们首先对数据集进行统计汇总,确保汇总后的结果使得每个学生对应的记录只有一条。对于学生来说,一般以学年为单位进行分析,所以我们选择2014年9月——2015年9月作为统计窗口期。
# 统计2014-9~2015-9学年度每个学生的借书次数以及借阅数量,并将统计结果构成新表
CREATE TABLE borrow_times AS
SELECT stu_id ,COUNT(DISTINCT borrow_date) AS borrow_times ,COUNT(DISTINCT book_title) AS books
FROM stu_borrow
WHERE borrow_date BETWEEN '2014-09-01' AND '2015-08-31'
GROUP BY stu_id;
# 查看统计结果的前5行
SELECT * FROM borrow_times
LIMIT 5;
# 删除stu_card表中重复记录以及消费金额为负的记录,并将清洗结果直接存储到stu_card_distinct表 中
CREATE TABLE stu_card_distinct AS
SELECT DISTINCT * FROM stu_card
WHERE amt>0;
# 如果运行报错:Error Code: 1206. The total number of locks exceeds the lock table size # 说明缓存内存不够(默认为8M),需要设置大一些(比如64M = 64*1024*1024 = 67108864 B)
show variables like "%_buffer_pool_size%";
SET GLOBAL innodb_buffer_pool_size=67108864;
# 统计2014-9~2015-9学年度每个学生的消费总额,最小金额、最大金额和客单价,并将统计结果直接存储到custom表中
CREATE TABLE custom AS
SELECT stu_id ,COUNT(*) AS custom_times ,
SUM(amt) AS custom_amt ,MIN(amt) AS min_amt ,
MAX(amt) AS max_amt ,SUM(amt)/COUNT(*) pct
FROM stu_card_distinct
WHERE custom_date BETWEEN '2014-09-01' AND '2015-08-31'
GROUP BY stu_id;
# 查询统计结果的5行信息
SELECT * FROM custom
LIMIT 5;
- 表连接操作
将上面处理好的两张表,整合成一张表。具体选择何种连接方式,需要根据实际业务需求。
# 统计结果的整合
SELECT COUNT(*) FROM custom; # 共计5427条
SELECT COUNT(*) FROM borrow_times; # 共计4158条
# 学生消费表custom作为主表,图书借阅表borrow_times作为辅表(左连接)
SELECT t1.*, t2.borrow_times,t2.books FROM custom AS t1
LEFT JOIN borrow_times AS t2
ON t1.stu_id = t2.stu_id;
# 内连接:提取出两张表中共有的学生记录
SELECT t1.*, t2.borrow_times,t2.books FROM custom AS t1
INNER JOIN borrow_times AS t2
ON t1.stu_id = t2.stu_id;
【补充】LOAD DATA的详细用法
详细解释见MySQL用户手册《refman-8.0-en.a4.pdf》P2449-P2459