数据分布决定SQL写法

这是2016年8月份上海MOORACLE大会上陈宏义老师(老K)分享的一个案例,将一个merge SQL,通过改写成plsql的方式,大大提高了执行效率。 老虎刘在看到这个案例的时候,开始没有注意到执行计划里面显示的各表实际记录数,不认为plsql的改写方式比分析函数的写法更高效,还与陈老师有过几次邮件讨论,直到后来仔细查看了执行计划。

原SQL如下:

merge into t_customer c using

(

select a.cstno, a.amount from t_trade a,

(select cstno,max(trade_date) trade_date from t_trade

group by cstno) b

where a.cstno = b.cstno and a.trade_date=b.trade_date

) m

on(c.cstno = m.cstno)

when matched then

update set c.amount = m.amount;

这个SQL是将用户交易明细表(t_trade )的最近的一笔消费额,更新到用户信息表(t_customer)的消费额字段,使用的是merge操作。

执行计划:
这里写图片描述
老虎刘注:

在没有掌握分析函数的写法前,SQL的红色部分是group by后取其他字段信息的一个较为常见的写法,也是这个SQL执行效率差的根本原因。

原SQL还有一个隐患,就是如果t_trade的某个cstno对应的最大trade_date有重复,那么这个SQL会报ORA-30926 错误无法执行。

如果不仔细看执行计划(两表的真实数据量信息),这种SQL的惯用优化方法是使用分析函数改写:

改写方法1:

merge into t_customer c using

(

select a.cstno,a.amount from

(select trade_date,cstno,amount,

row_number()over(partition by cstno order by trade_date desc) RNO from t_trade)a

where RNO=1

)  m

on(c.cstno = m.cstno)

when matched then

update set c.amount = m.amount;

这种改写方法会比原SQL效率提高很多,而且不存在某个cstno对应的max trade_date 重复时报错的问题。

但是陈老师没有使用分析函数的改写方法,而是根据两表数据量相差较大的特点,将SQL改写成一段更为高效的plsql:

改写方法2:

declare

vamount number;

begin

for v in (select * from t_customer )

loop

select amount into vamount from

(select amount from t_trade where cstno=v.cstno order by trade_date desc)

where rownum<2;

update t_customer set amount = vamount where cstno=v.cstno;

end loop

commit;

end;

根据原SQL的执行计划我们知道,t_customer表的记录数比较少,只有1000多条,而t_trade表有1000万条,比例为1:10000(不知道这是真实数据还是测试数据,只有1000多个用户,而且一个用户平均1万个消费明细,看起来不像真实数据)。

在这样一个两表数据相差较大的特殊情况下,plsql写法确实是比分析函数的写法要高效。这个改写非常巧妙。

我们再来分析一下这两种改写的优缺点:

1、plsql的改写方式,适合在t_customer表比较小,而且t_customer 和 t_trade 两表的记录数比例比较大的情况下,执行效率才会比分析函数的改写高一些。在本例中,如果t_customer表的记录数是10万,那么分析函数的写法反而要比plsql的写法快上几十到上百倍。

3、plsql这种改写的前提是必须存在t_trade表cstno + trade_date 两字段的联合索引。而分析函数的改写就不需要任何索引的支持。

4、对于t_trade这种千万记录级别的表,使用分析函数的写法可以通过开启并行来提速;plsql的改写,如果要提高效率,需要先将t_customer表按cstno分组,用多个session并发执行。

我们再来看看,陈老师的这段plsql,是不是可以用单个sql来实现,我做了一个尝试,SQL代码如下:

merge into t_customer c using

(

select tc.cstno,

(select amount

from t_trade td1

where td1.cstno=tc.cstno and td1.trade_date = (select max(trade_date) from t_trade td2 where tc.cstno = td2.cstno) and rownum=1 ) as amount

from t_customer tc

)  m

on(c.cstno = m.cstno)

when matched then

update set c.amount = m.amount;

执行计划大致如下:

这里写图片描述
这种写法也是需要t_trade表存在cstno+trade_date 联合索引(IDX_T_TRADE),而且T_customer 表的数据量远低于T_trade。

根据执行计划,这个sql的执行效率应该比plsql写法的效率不相上下。

总结:

SQL优化,除了要避免低效的SQL写法,主要还是要看表的数据量与数据分布情况,plsql的改写方法,在少数比较特殊的情况下会体现出较高的效率,在某些数据分布的情况下,效率可能还不如原SQL。但是,优化思路非常值得借鉴。

而分析函数的改写方式,则不论数据如何分布,都会比原SQL要高效,通用性更强。

对于本例改写前的SQL,应该还有很多开发人员和DBA在使用,在了解了分析函数的使用方法后,原SQL的低效写法就应该被彻底抛弃了。

最后的plsql改写成单SQL,逻辑看起来比较复杂难懂,一般不会用到这样的改写,大家了解一下就好了。

还是那句话,优化无定式,优化器是死的,人脑是活的,只有掌握了原理,才能让SQL执行效率越来越高。

转载自:

http://www.yunweipai.com/archives/18609.html

<think>嗯,我现在要在SQL里统计各个系的男生和女生人数。这个问题看起来不算太难,但作为刚开始学SQL的人,我需要仔细想想怎么做才对。首先,我得确定数据库里有哪几个相关的表。通常来说,学生信息可能会存在一个学生表里,比如叫students,里面应该包含学生所在的系、性别等信息。 假设学生表的结构是这样的:students有id(学号)、name(姓名)、gender(性别,可能是‘男’或‘女’),还有department(系别)。那我需要按系别分组,然后在每个系里统计男生和女生的数量。 那SQL的GROUP BY子句应该会用到。不过,通常GROUP BY是按一列分组,这里可能需要同时按系和性别分组。这样的话,可能需要先按系和性别分组,然后统计每个组的人数。比如: SELECT department, gender, COUNT(*) as count FROM students GROUP BY department, gender; 不过这样的话,结果会是每个系里每个性别一行记录,比如计算机系男有10人,女有5人,数学系男8人,女6人等等。但用户可能想要的是每个系一行,然后分别显示男生和女生的人数,变成两列。这时候可能需要用条件聚合,比如使用CASE WHEN或者IF语句来分别统计男女的数量。 这时候可以考虑用SUM配合CASE WHEN。例如,统计每个系的男生人数和女生人数,可以这样写: SELECT department, SUM(CASE WHEN gender = '男' THEN 1 ELSE 0 END) AS male_count, SUM(CASE WHEN gender = '女' THEN 1 ELSE 0 END) AS female_count FROM students GROUP BY department; 这样每个系都会有一行,显示该系的男生和女生人数。或者,如果数据库支持的话,比如MySQL,可以使用IF函数: SELECT department, COUNT(IF(gender = '男', 1, NULL)) AS male_count, COUNT(IF(gender = '女', 1, NULL)) AS female_count FROM students GROUP BY department; 这里COUNT(IF(...))是因为当条件不满足时,返回NULL,而COUNT会忽略NULL,所以只计算符合条件的行数。这可能更简洁一些。 不过,不同的数据库系统可能有不同的函数,比如在SQL Server中,可能需要使用CASE WHEN。所以通用的写法应该是第一种,使用CASE WHEN的方式。 另外,需要注意性别字段的取值是否只有‘男’和‘女’,或者可能有其他情况。如果有其他情况,可能需要过滤或者单独处理,但问题中只提到统计男女生,可能假设只有这两种情况。 总结一下,步骤应该是: 1. 确定学生表中有department和gender字段。 2. 按department分组。 3. 对每个分组,分别计算gender为男和女的数量。 4. 使用聚合函数结合条件表达式来实现分性别统计。 可能的错误点包括忘记GROUP BY导致所有行被聚合成一行,或者在CASE WHEN中没有正确处理条件导致统计错误。另外,字段名是否正确,比如department是否是正确的列名,gender的取值是否正确等,都需要注意。 测试的时候,可以先单独查看gender和department的数据,确认是否存在空值或者不一致的情况。例如,如果有的学生的gender是NULL,或者系别为空,那统计的时候这些数据可能会被忽略,需要根据需求决定是否处理这些情况。 最终,正确的SQL语句应该能输出每个系的男生和女生人数,如: 系别 | 男生人数 | 女生人数 计算机系 | 10 | 5 数学系 | 8 | 6 ... 这样用户就能清晰地看到每个系的男女分布情况了。</think>在SQL中统计各系的男生和女生人数,可以通过以下步骤实现: --- ### **1. 数据表结构假设** 假设存在学生信息表`students`,包含以下字段: - `id`:学号 - `name`:姓名 - `gender`:性别(值:`男`/`女`) - `department`:所属系别 --- ### **2. 统计方法** #### **方法一:使用`GROUP BY`分组统计** 按`department`和`gender`分组,直接统计每组的记录数: ```sql SELECT department, gender, COUNT(*) AS count FROM students GROUP BY department, gender ORDER BY department, gender; ``` **输出结果示例**: | department | gender | count | |------------|--------|-------| | 计算机系 | 男 | 10 | | 计算机系 | 女 | 5 | | 数学系 | 男 | 8 | | 数学系 | 女 | 6 | --- #### **方法二:按系别横向展开** 使用`CASE WHEN`或`SUM`函数,将男生和女生人数显示为两列: ```sql SELECT department, SUM(CASE WHEN gender = '男' THEN 1 ELSE 0 END) AS male_count, SUM(CASE WHEN gender = '女' THEN 1 ELSE 0 END) AS female_count FROM students GROUP BY department ORDER BY department; ``` 或(MySQL简化写法): ```sql SELECT department, COUNT(IF(gender = '男', 1, NULL)) AS male_count, COUNT(IF(gender = '女', 1, NULL)) AS female_count FROM students GROUP BY department; ``` **输出结果示例**: | department | male_count | female_count | |------------|------------|--------------| | 计算机系 | 10 | 5 | | 数学系 | 8 | 6 | --- ### **3. 关键点解释** 1. **分组聚合**: - `GROUP BY department`:按系别分组。 - `COUNT(*)`:统计每组的记录数。 2. **条件聚合**: - `CASE WHEN gender = '男' THEN 1 ELSE 0 END`:若性别为男,返回`1`,否则`0`。 - `SUM`函数累加所有`1`,得到男生人数。 3. **空值处理**: - `COUNT(IF(gender = '男', 1, NULL))`:仅统计满足条件的非空值。 --- ### **4. 注意事项** - 确保`gender`字段的值为`男`或`女`(区分大小写)。 - 若存在`NULL`值,需用`COALESCE(gender, '未知')`处理。 - 根据实际表名和字段名调整代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值