从实际出发,坚持理论与实践相结合的原则。
导语篇
最近由于工作的需要,遇到了需要大量使用sql语句的地方,同时也有一定的难度,至少以学校所学是没法解决的,所以想在这里写下这篇博客,记录一下所学到的所用到的知识,既是总结也是一次重新学习,顺便能利用公司庞大的数据表和查询结构来提高自己,因为单靠自己估计不会有遇到这种复杂的问题的机会。
语法篇
case
根据oracle的官方文档来看,分为普通case表达式和搜索case表达式两种,用个例子说明,
//simple_case_expression
CASE sex
WHEN '1' THEN '男'
WHEN '2' THEN '女'
ELSE '未知' END
//searched_case_expression
CASE
WHEN sex='1' THEN '男'
WHEN sex='2' THEN '女'
ELSE '未知' END
两种方式的区别在于,simple case更加简洁,而searched case功能更多,甚至可以写判断式
case的作用:
1. 将已知数据按照另一种方式进行分组,比如给一定范围内的数据划分一个等级
2. 用一个sql语句完成不同条件的分组
3. 在check中使用
group by
根据一定的规则进行分组。通过一定的规则将一个数据集划分成为若干个小的趋于,然后对若干小区域进行数据处理
使用规则:
1. group by X 的意思是所有具有相同X字段值的记录放在同一个分组里
2. group by X,Y 意思是将所有具有相同字段值X和Y的记录放在同一个分组,也就是在第一层分组的基础上再进行一次分组
使用时要注意的点:
1. 组函数以外的select列表里的字段,必须出现在group by中。
2. 使用group by进行分组,select列表中的字段必须要有组函数。
3. 如果要对分组后的结果再进行筛选,要用having而不是where
这里既然提到了分组函数,那么就做一个简要的介绍。分组函数作用于一组数据,并对一组数据返回一个值,例如avg,max,sum,count这些。也叫聚合函数吧,因为英文名是aggregate function。分组函数一般会和group by一起使用。
with … as
这个语句用来定义一个sql语句片段,这个片段能够被整个sql语句使用,也就是一个子查询。用来取出一个较长的sql片段提高可读性,也可以用在union的数据源部分,避免了语句的冗余以及多次查询。
注意问题:
1. 后面必须直接跟使用它的sql语句(如select insert update等),否则将失效。
2. 可以定义多个代码片段,但是只能由一个with,多个片段之间用逗号”,”隔开:
with
cte1 as
(
select ···
),
cte2 as
(
select ···
)
- 当命名和数据库表或是视图重名时,后面紧跟的那个语句还是使用这个子查询。
union
这个操作符用于合并多个select语句的结果集。union内部的每个结果集应该有数量相同的列,列也应该是拥有相似的数据类型,顺序也要一致。
union结果集中的列名总是等于union中第一个select语句中的列名。
union all与union语句都是合并结果集的操作,不过union all不会剔除重复的记录,union在合并结果集后会删除重复的记录,还会进行排序。所以使用union all效率是高于union的。
子查询
通常来讲,子查询返回的数据的类型有三种,返回一张数据表,返回一列值,返回单个值。返回类型不同在使用时也会有不同。
- 作为数据源使用
当子查询在外部查询FROM字句之后使用时,即使子查询只返回一个单一值(Scalar)或是一列值,依然可以看作是一个特殊的数据源。作为数据源使用的子查询很像视图,但是只是临时存在,不在数据库中。
使用子查询的返回类型要随外层方法的不同而变化,例如在from关键字后的子查询可以返回一张二维表,而in这种只能返回单列数据,所以其实还是按照外围的方法来的。
- 作为选择条件使用
作为选择条件的子查询是那些值返回一列的子查询,单个值当作只有一行的列使用。
(有种说法是,尽量不要使用IN,除非IN里面是固定的值,可以使用EXISTS等关键字代替,在Subquery typo with using in详细的说明和讲解,我在这里就不再赘述)
- 子查询作为计算列使用
当子查询作为计算列使用时,会针对外部查询的每一行,返回单个唯一值。
应用篇
sql作为一种声明式的语言,和其他许多程序语言不同,你不需要告诉计算机应该怎么做,而是要告诉计算机你要什么。
所以,sql语句在执行顺序上与声明顺序有着很大的不同,而且根据使用的数据库的不同解析方式也会有所变化。
因为sql语句在执行过程中每个步骤都会产生一个虚拟表,这个虚拟表会作为下一个步骤的输入。所以了解执行顺序对于写出正确的sql语句是十分重要的,例如,在where中不能够使用select中设定的别名,就是因为select是在where之后执行。
select语句的执行顺序(从使用者角度)
- from子句组装来自不同数据源的数据;
- where子句基于指定的条件对记录行进行筛选;
- group by子句将数据划分为多个分组;
- 使用聚集函数进行计算;
- 使用having子句筛选分组;
- 计算所有的表达式;
- select 的字段;
- 使用order by对结果集进行排序。
select语句的执行步骤(从内部实现角度)
- 语法分析,分析语句的语法是否符合规范
- 语义分析,检查语句中涉及的数据库对象是否存在,以及用户的权限
- 视图转换,将涉及视图的查询语句转换为相应的对基表查询语句
- 表达式转换,将复杂的SQL表达式转换为较简单的等效连接表达式
- 选择优化器,不同的优化器一般产生不同的执行计划
- 选择连接方式,oracle有三种连接方式
- 选择连接顺序,确定多表连接时哪个表为源数据表
- 选择数据的搜索路径,确定是使用全表搜索还是索引或其他方式
- 运行执行计划
逻辑查询处理阶段
1、 FROM:对FROM子句中的前两个表执行笛卡尔积(交叉联接),生成虚拟表VT1。
2、 ON:对VT1应用ON筛选器,只有那些使为真才被插入到TV2。
3、 OUTER (JOIN):如果指定了OUTER JOIN(相对于CROSS JOIN或INNER JOIN),保留表中未找到匹配的行将作为外部行添加到VT2,生成TV3。如果FROM子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤1到步骤3,直到处理完所有的表位置。
4、 WHERE:对TV3应用WHERE筛选器,只有使为true的行才插入TV4。
5、 GROUP BY:按GROUP BY子句中的列列表对TV4中的行进行分组,生成TV5。
6、 CUTE|ROLLUP:把超组插入VT5,生成VT6。
7、 HAVING:对VT6应用HAVING筛选器,只有使为true的组插入到VT7。
8、 SELECT:处理SELECT列表,产生VT8。
9、 DISTINCT:将重复的行从VT8中删除,产品VT9。
10、ORDER BY:将VT9中的行按ORDER BY子句中的列列表顺序,生成一个游标(VC10)。
11、TOP:从VC10的开始处选择指定数量或比例的行,生成表TV11,并返回给调用者。
工作中的两个实际应用
在公司的系统中,统计使用的实现方式是查询出所有的内容,将所有的数据都放进一个vo数组中,在服务器端拼接html语句,再传输到前端去。(因为并没有使用视图层的框架,是采取直接传html、js再用ajax获取数据的方式)
由于上述的实现和显示方式,那么在查询出的结果集中就需要有表头和表项两个部分。
- 按照某一字段的种类进行统计
这个相对简单一些,只需要对选出的数据进行一次分组,再用union加上一个表头行和一个总计行就行了,抛去中间的对一些数据权限字段的验证等,以最高学历的统计为例,这种实现方式的sql语句结构为:
WITH tempStatsData AS(
SELECT (case when p.highestEducationName is null then '未知' else p.highestEductationName end) 项目, count(p.id) 合计
FROM police_base_info p where 1=1
GROUP BY p.highestEducationName
)
SELECT '总计' 项目,SUM(t.合计) 合计,null 百分比 FROM tempStatsData t
UNION ALL
SELECT t.项目,t.合计,ROUND(t.合计*100/SUM(t.合计) over(),2) 百分比 FROM tempStatsData t
ORDER BY 合计 DESC
- 对某一字段分类统计
而如果需要对数据进行分段就会比较麻烦了,比如把出生日期转换成年龄段来进行统计,这种需求最容易想到的方式就是把出生日期统一成不同年龄区间的数据:
case when trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)>=0 and trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)<=18 then '0~18岁'
case when ··· then ···
else ··· end
但是这种方式得到的只是一个修改了表项的原始数据表,因为sql语句的执行顺序原因,没有办法进行分组操作。但是我们可以以这张表作为一个中间表,对此表内的数据进行二次操作,达到按照年龄段分组的统计结果。当然在具体执行过程中,年龄段的分配可以动态变化,所以sql语句是可以拼接的,下面给出的语句也只是一个demo:
with temp as(select
(case when trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)>=0 and trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)<=18 then '0~18岁'
when trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)>=25 and trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)<=34 then '25~34岁'
when trunc(months_between(to_date('2018-04-26','yyyy-MM-dd'),b.csrq)/12)>=35 then '35岁以上' else null end) 项目,
(case when b.sys_permCode = '#' then 1 else 0 end) keshi0,
(case when b.sys_permCode = '#' then 1 else 0 end) keshi1,
(case when b.sys_permCode = '#' then 1 else 0 end) keshi2,
from pp_worker_base_info b where 1=1
and b.sys_permCode in ('#','#','#')
)
select tc.项目 ,nvl(keshi0,0) 科室0, nvl(keshi1,0) 科室1, nvl(keshi2,0) 科室2, 合计,nvl(百分比,0) 百分比
from (select tt.项目 项目,sum(t.keshi0) keshi0,sum(t.keshi1) keshi1,sum(t.keshi2) keshi2,count(t.项目) 合计,
decode(count(t.项目),0,0,round(count(t.项目) * 100 / sum(count(t.项目)) over(),2)) 百分比
from temp t
right join(
select '0~18岁' 项目, 1 sort from dual
union
select '25~34岁' 项目, 2 sort from dual
union
select '35岁以上' 项目, 3 sort from dual
) tt on t.项目=tt.项目
group by tt.项目 ,tt.sort
order by tt.sort
) tc
上面sql语句中的sys_permCode是当前所拥有的科室的权限数据,用于分科室统计其中的数据信息。
由于统计的需求,需要将没有数据的行也展示出来,所以使用union
得到对所有列头的枚举,以及排序字段,再通过right join
语句合并到主查询之中。
这种方式比起第一种有一个比较严重的问题,就是没法生成总计行。(就在整理内容准备写这篇文章的当口,我好像找到了解决这个问题的办法)当时由于进度的原因就只能想了个折中方法,在service层中,对取得的数据做一次手动的统计,也是无奈之举。
不过这两天得空,重新思考了一下sql语句的结构,发现可以在子查询里拼装,而不是在right join字句或是结果集中添加,变动后的语句如下(原语句内容太多了,做了一点删减):
with tempStatsData as(select
(case when trunc(months_between(to_date('2018-05-04','yyyy-MM-dd'),b.csrq)/12)>=0 and trunc(months_between(to_date('2018-05-04','yyyy-MM-dd'),b.csrq)/12)<=18 then '19岁以下'
when trunc(months_between(to_date('2018-05-04','yyyy-MM-dd'),b.csrq)/12)>=26 then '26岁以上'
when trunc(months_between(to_date('2018-05-04','yyyy-MM-dd'),b.csrq)/12)>=19 and trunc(months_between(to_date('2018-05-04','yyyy-MM-dd'),b.csrq)/12)<=25 then '19~25岁' else null end) 项目,
(case when b.sys_permCode = '#' then 1 else 0 end) keshi0, /*这个code的具体内容在这个例子中不重要,所以直接省略了,用#代替*/
(case when b.sys_permCode = '#' then 1 else 0 end) keshi1,
(case when b.sys_permCode = '#' then 1 else 0 end) keshi2,
from pp_worker_base_info b where 1=1
and b.sys_permCode in ('#','#','#'))
select tc.项目,nvl(keshi0,0) keshi0,nvl(keshi1,0) keshi1,nvl(keshi2,0) keshi2,nvl(合计,0) 人数,百分比
from (
select '总计' 项目,0 sort,sum(keshi0) keshi0,sum(keshi1) keshi1,sum(keshi2) keshi2,count(t.项目) 合计, null 百分比 from tempStatsData t
union
select tt.项目 项目,sort,sum(t.keshi0) keshi0,sum(t.keshi1) keshi1,sum(t.keshi2) keshi2,count(t.项目) 合计,decode(count(t.项目),0,0,round(count(t.项目) * 100 / sum(count(t.项目)) over(),2)) 百分比 from tempStatsData t
right join( select '19岁以下' 项目, 1 sort from dual
union
select '26岁以上' 项目, 3 sort from dual
union
select '19~25岁' 项目, 2 sort from dual
) tt on t.项目=tt.项目
group by tt.项目, tt.sort
) tc
order by tc.sort;
其实关键就是要分清楚查询与子查询级数的问题,子查询的结果集能够作为外层查询的数据源,当不能够一次性得到自己需要的数据时,使用子查询可以很好的处理,但是在写的时候要十分注意嵌套的层数关系,避免在语句多起来以后分不清层级关系,从而出现混乱和错误。
提高篇
- 留待以后
结语篇
这次的这篇博客因为一些原因,断断续续写了大半个月才(半)完成,思路实在是续不上来,但又不想让努力付诸东流,于是草草写完,中间的好多东西完整性都不够,也没有深度,但是也只能就这样了。希望能看到这里的人见谅。