SQL学习精粹之group by分组

本文介绍了SQL中分组查询的技巧,包括如何利用group by取最大值、结合case when进行复杂分组统计,以及使用having子句进一步过滤分组结果。通过多个实例展示了如何应对各种分组场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

group by 有一个原则,

不分组必聚合原则:就是 select 后面的所有列中,没有使用聚合函数的列,必须出现在 group by 后面(重要)

一、group by取最大值

面试遇到一个分组取最大值的问题

例子一 取最新充值记录

充值记录表

185156_c8Rj_2507499.png

CREATE TABLE chongZhi (
   id int not null primary key AUTO_INCREMENT COMMENT '主键编号',
   userId int COMMENT '充值用户id',
   jine int  COMMENT '充值金额',
   date date COMMENT '充值日期'
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '充值记录表';

insert into chongZhi(userId,jine,date) value(1,20,'2016-10-28');
insert into chongZhi(userId,jine,date) value(1,60,'2016-10-29');
insert into chongZhi(userId,jine,date) value(1,55,'2016-10-30');
insert into chongZhi(userId,jine,date) value(2,100,'2016-10-22');
insert into chongZhi(userId,jine,date) value(2,200,'2016-10-28');
insert into chongZhi(userId,jine,date) value(2,300,'2016-10-30');

select userId as 用户ID,max(date) as 最近充值时间 from chongZhi group by 1;

运行结果

140755_BaOa_2507499.png

可是这样不能显示充值数据ID和充值金额,不然会违反不分组必聚合原则sql会报错

下面的例子可以解决这个问题。

例子二 物料供货商最新报价

物料报价表:

185956_rP4g_2507499.png

根据上面的表 查询每种物料 每种供货商 的最新报价:

与上面问题的不同之处:

①这个是两个字段确定唯一性(类似联合主键,这里是联合分组)。

②需要显示本表中其他的字段。

其实这个差不多。

SQL:

CREATE TABLE `WuLiaoBiao` (
   id int not null primary key AUTO_INCREMENT COMMENT '主键编号',
   wuliaoName varchar(50) COMMENT '物料名',
   gongId int COMMENT '供货商ID',
   price int  COMMENT '价格',
   date date COMMENT '日期'
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '物料表';


insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料1',1,500,'2016-10-01');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料1','1','200','2016-10-02');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料1','2','300','2016-10-03');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料1','2','800','2016-10-04');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料1','2','100','2016-10-05');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料2','2','190','2016-10-06');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料2','2','150','2016-10-09');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料2','1','100','2016-10-19');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料2','1','200','2016-10-20');
insert into WuLiaoBiao(wuliaoName,gongId,price,date) value('物料2','1','400','2016-10-29');
                                                                                        

select t1.id,t1.wuliaoName,t1.gongId,t1.price,t1.date from WuLiaoBiao t1, 
(select wuliaoName,gongId,max(date) as date from WuLiaoBiao group by wuliaoName,gongId) t2
where t1.wuliaoName=t2.wuliaoName and t1.gongId=t2.gongId and t1.date=t2.date;

运行结果

140853_9ZNz_2507499.png

实现方式,依据【不分组必聚合原则】,因为需要查询的列有的不在聚合函数内,所以外层主查询不能分组。使用本身表以及该表的分组查询结果做自连接,where条件为分组查询的所有列(包括聚合函数列)和本身表做等值连接。

二、group by+case when

例子一  日期胜负表:

score 表:

144201_EexN_2507499.png

某球队需要做每天的比赛胜负数统计:

select date as 时间, count(1) as 比赛总数,
  sum(case when sco='胜' then 1 else 0 end) 胜利数,
  sum(case when sco="负" then 1 else 0 end) 失败数
  from score group by date;

 select date as 时间, count(1) as 比赛总数,
	sum(case sco when '胜' then 1 else 0 end) 胜利数,
	sum(case sco when "负" then 1 else 0 end) 失败数
	from score group by date;

查询结果:

144431_hFEa_2507499.png

例子二  科目成绩(行转列):

数据表:

姓名 科目 分数
张三 语文 80
张三 数学 98
张三 英语 65
李四 语文 70
李四 数学 80
李四 英语 90

期望查询结果:

姓名 语文 数学 英语
张三 80     98      65
李四 70     80      90

代码

SQL 代码  

create table testScore    
(    
   tid int primary key AUTO_INCREMENT,    
   tname varchar(30) null,    
   ttype varchar(10) null,    
   tscor int null   
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '成绩表';
     
-- 插入数据    
insert into testScore(tname,ttype,tscor) value('张三','语文',80); 
insert into testScore(tname,ttype,tscor) value('张三','数学',98);   
insert into testScore(tname,ttype,tscor) value('张三','英语',65);    
insert into testScore(tname,ttype,tscor) value ('李四','语文',70);    
insert into testScore(tname,ttype,tscor) value ('李四','数学',80);    
insert into testScore(tname,ttype,tscor) value ('李四','英语',90); 

select tname 姓名 ,
max(case ttype when '语文' then tscor else 0 end) 语文,
max(case ttype when '数学' then tscor else 0 end) 数学,
max(case ttype when '英语' then tscor else 0 end) 英语 
from testScore 
group by tname

实例三 大州人口统计:

有如下数据:(为了看得更清楚,我并没有使用国家代码,而是直接用国家名作为Primary Key)

国家(country)人口(population)
中国600
美国100
加拿大100
英国200
法国300
日本250
德国200
墨西哥50
印度250

根据这个国家人口数据,统计亚洲和北美洲的人口数量。应该得到下面这个结果。

人口
亚洲1100
北美洲250
其他700

代码

SQL 代码  

SELECT  CASE country
                WHEN '中国'     THEN '亚洲'
                WHEN '印度'     THEN '亚洲'
                WHEN '日本'     THEN '亚洲'
                WHEN '美国'     THEN '北美洲'
                WHEN '加拿大'  THEN '北美洲'
                WHEN '墨西哥'  THEN '北美洲'
        ELSE '其他' END  AS 大州,
        SUM(population) AS 人口总数
FROM    country_pop
GROUP BY CASE country
                WHEN '中国'     THEN '亚洲'
                WHEN '印度'     THEN '亚洲'
                WHEN '日本'     THEN '亚洲'
                WHEN '美国'     THEN '北美洲'
                WHEN '加拿大'  THEN '北美洲'
                WHEN '墨西哥'  THEN '北美洲'
        ELSE '其他' END;

同样的,我们也可以用这个方法来判断工资的等级,并统计每一等级的人数。SQL代码如下;

SQL 代码  

SELECT
        CASE WHEN salary <= 500 THEN '1'
             WHEN salary > 500 AND salary <= 600  THEN '2'
             WHEN salary > 600 AND salary <= 800  THEN '3'
             WHEN salary > 800 AND salary <= 1000 THEN '4'
        ELSE NULL END salary_class,
        COUNT(*)
FROM    Table_A
GROUP BY
        CASE WHEN salary <= 500 THEN '1'
             WHEN salary > 500 AND salary <= 600  THEN '2'
             WHEN salary > 600 AND salary <= 800  THEN '3'
             WHEN salary > 800 AND salary <= 1000 THEN '4'
        ELSE NULL END;

对于groupby后面一般都是跟一个列名,但在该例子中通过case语句使分组变得跟强大了。

实例四 人口性别分组(行转列):

有如下数据

国家(country)性别(sex)人口(population)
中国1340
中国2260
美国145
美国255
加拿大151
加拿大249
英国140
英国260

按照国家和性别进行分组,得出结果如下

国家
中国340260
美国4555
加拿大5149
英国4060

代码

SQL 代码   

SELECT country,
       SUM( CASE WHEN sex = '1' THEN 
                      population ELSE 0 END),  --男性人口
       SUM( CASE WHEN sex = '2' THEN 
                      population ELSE 0 END)   --女性人口
FROM  Table_A
GROUP BY country;

GROUP BY子句中的NULL值处理

当GROUP BY子句中用于分组的列中出现NULL值时,将如何分组呢?SQL中,NULL不等于NULL(在WHERE子句中有过介绍)。然而,在GROUP BY子句中,却将所有的NULL值分在同一组,即认为它们是“相等”的。

三、HAVING子句

GROUP BY子句分组,只是简单地依据所选列的数据进行分组,将该列具有相同值的行划为一组。而实际应用中,往往还需要删除那些不能满足条件的行组,为了实现这个功能,SQL提供了HAVING子句。语法如下。

SELECT column, SUM(column)

FROM table

GROUP BY column

HAVING SUM(column) condition value

说明:HAVING通常与GROUP BY子句同时使用。当然,语法中的SUM()函数也可以是其他任何聚合函数。DBMS将HAVING子句中的搜索条件应用于GROUP BY子句产生的行组,如果行组不满足搜索条件,就将其从结果表中删除。

HAVING子句的应用

从TEACHER表中查询至少有两位教师的系及教师人数。

实现代码:

SQL 代码  

SELECT DNAME, COUNT(*) AS num_teacher

FROM TEACHER

GROUP BY DNAME

HAVING COUNT(*)>=2

HAVING子句与WHERE子句的区别

HAVING子句和WHERE子句的相似之处在于,它也定义搜索条件。但与WHERE子句不同,HAVING子句与组有关,而不是与单个的行有关。

1、如果指定了GROUP BY子句,那么HAVING子句定义的搜索条件将作用于这个GROUP BY子句创建的那些组。

2、如果指定WHERE子句,而没有指定GROUP BY子句,那么HAVING子句定义的搜索条件将作用于WHERE子句的输出,并把这个输出看作是一个组。

3、如果既没有指定GROUP BY子句也没有指定WHERE子句,那么HAVING子句定义的搜索条件将作用于FROM子句的输出,并把这个输出看作是一个组。

4、在SELECT语句中,WHERE和HAVING子句的执行顺序不同。在本书的5.1.2节介绍的SELECT语句的执行步骤可知,WHERE子句只能接收来自FROM子句的输入,而HAVING子句则可以接收来自GROUP BY子句、WHERE子句和FROM子句的输入。

转载于:https://my.oschina.net/iyinghui/blog/760074

<think>嗯,我现在要设计一个联合主键,但不太确定该怎么开始。首先,联合主键是什么?好像是用多个列组合起来作为主键,而不是单一列。那为什么要用联合主键呢?可能是当单独一列无法唯一标识记录时,需要多列组合才能保证唯一性。比如订单明细表,可能需要订单ID和产品ID一起才能唯一确定一条记录。 不过,设计联合主键的时候应该考虑哪些因素呢?可能需要确保这几个列的组合确实是唯一的,不会有重复。然后,这些列的值是不是稳定的?比如,如果其中一个列的值可能会改变,那作为主键的一部分可能不太好,因为主键通常不建议频繁更新。还有,联合主键的列数量是不是越少越好?太多的话可能会影响性能,比如在索引和连接查询的时候。 另外,联合主键的顺序有没有讲究?比如在数据库索引中,列的顺序可能会影响查询效率。比如,把最常用的查询条件放在前面,这样索引能更有效。还有,主键的选择是否会影响外键关系?如果其他表要引用这个联合主键,外键也需要包含所有这些列,这样会不会让外键变得复杂? 还有一个问题,使用联合主键和单独使用一个代理主键(比如自增ID)相比有什么优缺点?联合主键更符合业务逻辑,可能不需要额外的列,但可能会让关联表更复杂。而代理主键简单,但可能需要在业务逻辑中处理唯一性约束。 还有,数据库设计规范里有没有关于联合主键的推荐做法?比如,某些情况下必须使用联合主键,或者避免使用?可能需要查阅一些数据库设计的最佳实践。比如,当业务实体本身就需要多个属性来唯一标识时,联合主键是合适的,但如果只是为了避免使用代理键,可能并不好。 另外,联合主键的数据类型选择也很重要,比如尽量使用定长、较小的数据类型,这样索引效率更高。比如用整数类型而不是字符串类型,或者较短的字符串。如果其中一个列是字符串且很长,可能会影响性能。 还需要考虑的是,联合主键在应用层的使用是否方便。比如,在ORM框架中,处理复合主键可能需要额外的配置,可能会有一些限制或者更复杂的代码。 还有,当表之间有继承关系或者关联时,联合主键的设计会不会带来问题?比如子表需要包含父表的主键作为外键,如果父表是联合主键,子表的外键也要包含所有列,这可能增加子表的复杂性。 可能还需要考虑索引的大小,联合主键的索引会占用更多的存储空间,特别是当列很多或者列的数据类型较大时。这会影响数据库的性能,尤其是在插入、更新和查询的时候。 总结一下,设计联合主键的步骤可能包括:确定业务需求,确认需要多个列保证唯一性;选择稳定且最小的列组合;考虑索引效率和查询模式;评估对关联表和外键的影响;以及与应用层的兼容性。同时,需要权衡使用联合主键和代理主键的利弊,根据具体情况做出选择。</think>设计联合主键需遵循以下步骤及原则,确保数据库结构的合理性与高效性: --- ### **一、理解联合主键的作用** 联合主键(Composite Primary Key)通过**多个字段的组合**唯一标识一条记录,适用于以下场景: - **业务实体需多属性唯一标识**:例如订单明细表(`订单ID + 产品ID`)。 - **避免冗余字段**:不引入无意义的自增ID,直接使用业务字段组合。 - **强制数据完整性**:天然约束多字段的唯一性。 --- ### **二、设计原则** #### **1. 唯一性** - **字段组合必须全局唯一**:确保任意两条记录的字段组合不重复。 - **示例**:学生选课表中,`学号 + 课程号` 可唯一标识一条选课记录。 #### **2. 最小化字段数量** - **用最少的字段保证唯一性**:例如用 `国家 + 身份证号` 替代 `国家 + 省份 + 姓名`。 - **避免冗余字段**:多余的字段会增加索引大小,降低性能。 #### **3. 稳定性** - **选择值不频繁变更的字段**:若字段值常修改(如用户名),不适合作为主键。 - **示例**:用户表使用 `注册手机号` 而非 `昵称`。 #### **4. 数据类型优化** - **优先使用紧凑类型**:如整型(`INT`)优于字符串(`VARCHAR`)。 - **控制字段长度**:例如用 `SMALLINT` 代替 `INT` 存储小范围数值。 --- ### **三、实现步骤** #### **步骤1:分析业务需求** - 确认哪些字段的组合能唯一标识记录。 - **示例**:图书馆借阅记录的 `读者ID + 图书ISBN + 借阅日期`。 #### **步骤2:验证唯一性约束** - 通过SQL查询检查现有数据是否满足唯一性: ```sql SELECT field1, field2, COUNT(*) FROM table GROUP BY field1, field2 HAVING COUNT(*) > 1; ``` #### **步骤3:定义联合主键** - 在DDL中明确指定: ```sql CREATE TABLE 订单明细 ( 订单ID INT, 产品ID INT, 数量 INT, PRIMARY KEY (订单ID, 产品ID) ); ``` #### **步骤4:评估索引效率** - **索引列顺序**:高频查询条件字段应放在联合主键左侧。 ```sql -- 若常按订单ID查询,则主键顺序为 (订单ID, 产品ID) ``` --- ### **四、联合主键 vs 代理主键** | **对比维度** | **联合主键** | **代理主键(如自增ID)** | |--------------------|--------------------------------------|----------------------------------| | **唯一性依据** | 业务逻辑本身 | 无业务意义,依赖生成规则 | | **外键引用复杂度** | 需引用多字段 | 仅引用单一字段 | | **索引性能** | 可能较大,影响查询速度 | 通常更紧凑,查询更快 | | **业务可读性** | 高(直接反映业务属性) | 低(仅为技术标识) | --- ### **五、注意事项** 1. **外键关联**:引用联合主键的表需包含所有主键字段。 ```sql CREATE TABLE 订单日志 ( 订单ID INT, 产品ID INT, 操作时间 DATETIME, FOREIGN KEY (订单ID, 产品ID) REFERENCES 订单明细(订单ID, 产品ID) ); ``` 2. **ORM适配**:需配置实体类复合主键(如JPA中 `@IdClass`)。 3. **索引维护**:联合主键本身是聚簇索引(如InnoDB),避免额外创建重复索引。 --- ### **六、适用场景总结** - **强业务约束**:如行政区划编码(`省代码 + 市代码`)。 - **多对多关系表**:如学生选课(`学生ID + 课程ID`)。 - **高频按主键查询**:联合主键顺序需匹配查询条件。 --- 通过以上步骤,可设计出既符合业务需求,又具备高性能的联合主键结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值