1、子查询 Subqueries

子查询是一个嵌套在 SELECT、INSERT、UPDATE 或 DELETE 语句或其他子查询中的查询。通俗来讲,子查询就是嵌套在大“查询”中的小查询。子查询也称为内部查询或内部选择,而包含子查询的语句也称为外部查询或外部选择。

从概念上说,子查询结果会代入外部查询(尽管这不一定是 SQL Server 实际处理带有子查询的 T-SQL 语句的方式)。所以子查询会在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。

比较常见的子查询有:单行子查询、多行子查询、相关子查询、嵌套子查询等。然而并没有一种泾渭分明的子查询分类方法,换句话说,有可能某个子查询既是多行子查询,也是相关子查询,同时还是嵌套子查询。

1.1、单行子查询

顾名思义,单行子查询就是只查询一行数据的内部查询。如果单行子查询仅返回单一值,就可以称之为**标量子查询**。标量子查询也是最常见的单行子查询。示例如下:

  1. -- 查询年龄最小的学生
  2. SELECT * FROM T_Students WHERE Birthday = (SELECT MAX(Birthday) FROM T_Students);
  3. -- 1 次课程 1 考试的成绩高于学生 12 的成绩
  4. SELECT StudentId,Scores FROM T_ExamResults
  5. WHERE Counts = 1 AND CourseId = 1 AND Scores > (
  6. SELECT Scores FROM T_ExamResults WHERE Counts = 1 AND CourseId = 1 AND StudentId = 12);
  7. -- 历次课程 1 考试的平均分高于学生 12 的成绩
  8. SELECT StudentId,AVG(Scores) AvgScore,COUNT(1) ExamCount FROM T_ExamResults
  9. WHERE CourseId = 1
  10. GROUP BY StudentId
  11. HAVING AVG(Scores) > (SELECT AVG(Scores) FROM T_ExamResults WHERE CourseId = 1 AND StudentId = 12);

1.2、多行子查询

相较于单行子查询,多行子查询就是会返回多行的内部查询。示例如下:

  1. -- 查询有女生的班级里的学生
  2. SELECT * FROM T_Students WHERE ClassId IN(SELECT ClassId FROM T_Students WHERE Gender = 0);
  3. -- 查询有女生的班级之外的所有班级的学生
  4. SELECT * FROM T_Students WHERE ClassId NOT IN(SELECT ClassId FROM T_Students WHERE Gender = 0);
  5. -- 查询有 2003 年及以后出生的学生的班级
  6. SELECT * FROM T_Classes WHERE Id IN(SELECT ClassId FROM T_Students WHERE Birthday >= '2003-01-01');

1.3、相关子查询

相关子查询是指查询条件引用了外部查询中字段的内部查询。相反的,如果外部查询的字段没有出现在内部查询的条件中即为非相关子查询。相关子查询的内部查询得依靠外部查询获得值,这意味着内部查询是重复执行的,为外部查询选择的每一行都要执行一次,因此相关子查询也被称之为重复子查询。示例如下:

  1. -- 查询在三(1)班和三(2)班的学生
  2. SELECT * FROM T_Students t1 WHERE EXISTS(
  3. SELECT Id FROM T_Classes t2 WHERE t2.Id = t1.ClassId AND t2.Name IN('三(1)班','三(2)班'));
  4. -- 查询不在三(1)班和三(2)班的学生
  5. SELECT * FROM T_Students t1 WHERE NOT EXISTS(
  6. SELECT Id FROM T_Classes t2 WHERE t2.Id = t1.ClassId AND t2.Name IN('三(1)班','三(2)班'));
  7. -- 查询第 1 次考试的课程及参加了的学生
  8. SELECT (SELECT t2.Name FROM T_Courses t2 WHERE t2.Id=t1.CourseId) CourseName,
  9. (SELECT t3.Name FROM T_Students t3 WHERE t3.Id=t1.StudentId) StudentName
  10. FROM T_ExamResults t1 WHERE t1.Counts = 1;

1.4、嵌套子查询

嵌套子查询是指查询内部嵌套一个或多个子查询的内部查询。一个 T-SQL 语句中可以嵌套任意数量的子查询,尽管通常来说没有这种必要。示例如下:

  1. -- 查询参加了第 1 次课程 1 考试的学生
  2. SELECT * FROM T_Students t3 WHERE t3.Id IN(
  3. SELECT t2.StudentId FROM T_ExamResults t2 WHERE t2.Counts = 1 AND t2.CourseId = (
  4. SELECT t1.Id FROM T_Courses t1 WHERE t1.Name = '英语'));
  5. -- 查询西湖区所在的城市的所有学生
  6. SELECT t3.* FROM T_Students t3 WHERE SUBSTRING(t3.Code,2,6) IN(
  7. SELECT t2.Code FROM T_Districts t2 WHERE t2.ParentId = (
  8. SELECT t1.ParentId FROM T_Districts t1 WHERE t1.Name = '西湖区'));

1.5、子查询小结及性能问题

上文主要讲述了查询语句中的子查询,其实在增删改语句中也一样能够使用子查询。任何能使用表达式的地方都可以使用子查询,只要它返回的是单个值即可。很多包含子查询的语句都可以改写成连接查询。示例如下:

  1. -- 更新语句(子查询写法)
  2. UPDATE T_Students SET Remark='考过满分'
  3. WHERE Id IN(SELECT t.StudentId FROM T_ExamResults t WHERE t.Scores = 100);
  4. -- 更新语句(连接写法)
  5. UPDATE T_Students SET Remark='考过满分'
  6. FROM T_Students t1 JOIN T_ExamResults t2 ON t1.Id = t2.StudentId AND t2.Scores = 100;
  7. -- 删除语句(子查询写法)
  8. DELETE T_ExamResults WHERE Counts = 10 AND StudentId = (
  9. SELECT t.Id FROM T_Students t WHERE t.Code = 'S330104010');
  10. -- 删除语句(连接写法)
  11. DELETE T_ExamResults FROM T_ExamResults t1
  12. JOIN T_Students t2 ON t1.StudentId = t2.Id AND t1.Counts = 10 AND t2.Code = 'S330104010';

在 T-SQL 中,包含子查询的语句和语义上等效的不包含子查询的语句在性能上通常是没有差别的。但在一些需要为外部查询的每个结果都执行内部查询的情况下,使用连接写法会产生更好的性能(如果数据很少,这种差别也很难体现出来),如某些非必须的相关子查询。示例如下:

  1. -- 查询所有学生第 1 次课程 2 考试的成绩(子查询写法)
  2. SELECT (SELECT t2.Name FROM T_Students t2 WHERE t2.Id = t1.StudentId) StudentName,Scores
  3. FROM T_ExamResults t1
  4. WHERE t1.Counts = 1 AND t1.CourseId = 2;
  5. -- 查询所有学生第 1 次课程 2 考试的成绩(连接写法)
  6. SELECT t2.Name StudentName,Scores
  7. FROM T_ExamResults t1
  8. JOIN T_Students t2 ON t1.StudentId=t2.Id
  9. WHERE t1.Counts = 1 AND t1.CourseId = 2;

2、公用表表达式 CTE

在 T-SQL 中,WITH 语句用于指定临时命名的结果集,这些结果集被称为公用表表达式(Common Table Expression,简称 CTE)。基本语法如下:

  1. WITH cte-name (column-names) AS (cte-query) [,...]

参数释义如下:

  • cte-name 代表公用表表达式的有效标识符。类似于子查询的别名,在一个语句中不能出现重复的 cte-name,但可以与 CTE 引用的基表名称相同。引用 CTE 中的任何字段都得用 cte-name 来限定,而不能使用字段原本所属的基表来限定。
  • column-names 代表公用表表达式的字段名列表,只要 column-name 的个数与 cte-query 中定义字段数相同即可。如果为 cte-query 中的所有字段都提供了不同的名称,那么 column-names 就是可选的了(一般大家都这么干,毕竟有谁会喜欢没必要的繁琐呢?)。
  • cte-query 代表一个公用表表达式的查询语句,可以是任意合法的 SELECT 语句。

2.1、普通公用表表达式

CTE 可在单条 INSERT、DELETE、UPDATE 或 SELECT 语句的执行范围内定义。

CTE & INSERT 如要把 2000 年之前出生的女生信息插入到好学生表中,用 CTE 定义女生数据,示例如下:

  1. WITH temp AS(
  2. SELECT t.Id,t.Name,t.Gender,t.Birthday FROM T_Students t WHERE t.Gender = 0
  3. )
  4. INSERT INTO T_GoodStudents(Id,Name,Gender,Birthday)
  5. SELECT * FROM temp WHERE Birthday < '2000-01-01';

CTE & DELETE 如要把姓名和性别都是空的学生信息删除,用 CTE 定义姓名为空的数据,示例如下:

  1. WITH t AS(
  2. SELECT t.* FROM T_GoodStudents t WHERE t.Name IS NULL
  3. )
  4. DELETE FROM t WHERE t.Gender IS NULL;

CTE & UPDATE 如要把历次语文成绩的平均分更新到学生备注中,用 CTE 定义学生平均分数据,示例如下:

  1. WITH temp AS(
  2. SELECT t.StudentId,t.CourseId,AVG(t.Scores) AvgScore
  3. FROM T_ExamResults t
  4. GROUP BY t.StudentId,t.CourseId
  5. )
  6. UPDATE T_Students SET Remark = t1.AvgScore
  7. FROM temp t1
  8. JOIN T_Courses t2 ON t1.CourseId = t2.Id
  9. WHERE T_Students.Id = t1.StudentId AND t2.Name = '语文';

CTE & SELECT(多次引用同一个 CTE)如要查询前 3 次考试的总成绩及平均成绩,用 CTE 定义各次的成绩数据,示例如下:

  1. WITH temp AS(
  2. SELECT t.StudentId,t.Counts,SUM(t.Scores) SumScore
  3. FROM T_ExamResults t
  4. WHERE t.Counts IN(1,2,3)
  5. GROUP BY t.StudentId,t.Counts
  6. )
  7. SELECT t1.Code,t1.Name,
  8. t2.SumScore FirstSumScore,t3.SumScore SecondSumScore,t4.SumScore ThirdSumScore,
  9. (t2.SumScore + t3.SumScore + t4.SumScore)/3 AvgSumScore
  10. FROM T_Students t1
  11. JOIN temp t2 ON t1.Id = t2.StudentId AND t2.Counts = 1
  12. JOIN temp t3 ON t1.Id = t3.StudentId AND t3.Counts = 2
  13. JOIN temp t4 ON t1.Id = t4.StudentId AND t4.Counts = 3;

CTE & SELECT(一个 WITH 定义多个 CTE)如要查询男生们前 3 次课程 1 的考试成绩,用 CTE 定义各次的成绩数据,示例如下:

  1. WITH t1 AS(
  2. SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 1
  3. ),
  4. t2 AS(
  5. SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 2
  6. ),
  7. t3 AS(
  8. SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 3
  9. )
  10. SELECT t4.Code,t4.Name,t1.Scores FirstScore,t2.Scores SecondScore,t3.Scores ThirdScore
  11. FROM T_Students t4
  12. JOIN t1 ON t4.Id = t1.StudentId
  13. JOIN t2 ON t4.Id = t2.StudentId
  14. JOIN t3 ON t4.Id = t3.StudentId
  15. WHERE t4.Gender = 1;

2.2、递归公用表表达式

CTE 可以包含对自身的引用,这种表达式被称为递归公用表表达式。一个递归 CTE 中至少要包含两个查询定义,一个定位点成员和一个递归成员,递归成员的 FROM 子句只能引用一次 CTE。另外,定位点成员和递归成员二者的字段数必须相同,字段的数据类型也需要保持一致。

从上到下递归,如要查询浙江省及以下各级别的行政区,示例如下:

  1. WITH temp AS(
  2. SELECT t1.Id,t1.Name FROM T_Districts t1 WHERE t1.Code = '330000'
  3. UNION ALL
  4. SELECT t2.Id,t2.Name FROM T_Districts t2,temp t1 WHERE t2.ParentId = t1.Id
  5. )
  6. SELECT temp.Name FROM temp;

从下到上递归,如要查询西湖区及其所有上级行政区,示例如下:

  1. WITH temp AS(
  2. SELECT t1.ParentId,t1.Name FROM T_Districts t1 WHERE t1.Code = '330106'
  3. UNION ALL
  4. SELECT t2.ParentId,t2.Name FROM T_Districts t2,temp t1 WHERE t2.Id = t1.ParentId
  5. )
  6. SELECT temp.Name FROM temp;

可以定义多个定位点成员和递归成员,但必须将所有定位点成员查询定义置于第一个递归成员定义之前。在起点成员之间可以用任意集合运算符,而在最后一个定位点成员和第一个递归成员之间,以及多个递归成员之间,必须用 UNION ALL 来连接。示例如下(查询卢小妹的所有祖先):

  1. WITH temp(Id) AS(
  2. SELECT t1.Father FROM T_Persons t1 WHERE t1.Name = '卢小妹'
  3. UNION
  4. SELECT t2.Mother FROM T_Persons t2 WHERE t2.Name = '卢小妹'
  5. UNION ALL
  6. SELECT t3.Father FROM T_Persons t3,temp WHERE t3.Id = temp.Id
  7. UNION ALL
  8. SELECT t4.Mother FROM T_Persons t4,temp WHERE t4.Id = temp.Id
  9. )
  10. SELECT t1.Id,t1.Name,t1.Father,t1.Mother
  11. FROM T_Persons t1,temp
  12. WHERE t1.Id=temp.Id;

递归运算一定要有出口,否则就是死循环了!SQL Server 提供了一个 MAXRECURSION 提示来限制递归级数,以防止出现无限循环。但我个人觉得应该尽可能的通过 WHERE 条件或业务逻辑来定义更合理的出口。例如要显示的限定只返回某一递归级别的数据,示例如下(查询浙江省下所有县一级的行政区):

  1. WITH temp AS(
  2. SELECT t1.Id,t1.Name,t1.Code,t1.Level
  3. FROM T_Districts t1
  4. WHERE t1.Code = '330000'
  5. UNION ALL
  6. SELECT t2.Id,t2.Name,t2.Code,t2.Level
  7. FROM T_Districts t2,temp
  8. WHERE t2.ParentId = temp.Id
  9. )
  10. SELECT temp.Code,temp.Name,temp.Level FROM temp WHERE temp.Level = 3;

尽管看上去很简单,但在实际开发中很可能并没有类似 Level 这种标识级别的字段可用。如果是这样,那我们还可以通过递归成员的递归次数来实现同样的过滤效果。示例如下:

  1. WITH temp AS(
  2. SELECT t1.Id,t1.Name,t1.Code,t1.Level,0 Step
  3. FROM T_Districts t1
  4. WHERE t1.Code = '330000'
  5. UNION ALL
  6. SELECT t2.Id,t2.Name,t2.Code,t2.Level,temp.Step + 1
  7. FROM T_Districts t2,temp
  8. WHERE t2.ParentId = temp.Id
  9. )
  10. SELECT temp.Code,temp.Name,temp.Level FROM temp WHERE temp.Step = 2;

3、本文小结

本文主要介绍了 T-SQL 中最常见的几种子查询以及公用表表达式 CTE。本文还专门说明了递归 CTE,它可以实现类似于 PL/SQL 中的 CONNECT BY 层次查询。

本文参考链接:

去导航目录篇下载创建本系列博文通用库表及数据的 SQL 语句

本文链接http://www.cnblogs.com/hanzongze/p/tsql-subquery.html

版权声明:本文为博客园博主 韩宗泽 原创,作者保留署名权!欢迎通过转载、演绎或其它传播方式来使用本文,但必须在明显位置给出作者署名和本文链接!个人博客,能力有限,若有不当之处,敬请批评指正,谢谢!

SQL Server温故系列(3):SQL 子查询 & 公用表表达式 CTE的更多相关文章

  1. SQL Server调优系列基础篇 - 子查询运算总结

    前言 前面我们的几篇文章介绍了一系列关于运算符的介绍,以及各个运算符的优化方式和技巧.其中涵盖:查看执行计划的方式.几种数据集常用的连接方式.联合运算符方式.并行运算符等一系列的我们常见的运算符.有兴 ...

  2. SQL Server 调优系列基础篇 - 子查询运算总结

    前言 前面我们的几篇文章介绍了一系列关于运算符的介绍,以及各个运算符的优化方式和技巧.其中涵盖:查看执行计划的方式.几种数据集常用的连接方式.联合运算符方式.并行运算符等一系列的我们常见的运算符.有兴 ...

  3. SQL Server温故系列(0):导航目录

    创建本系列博文通用库表及数据的 SQL 语句:下载 SQL Server温故系列(0):导航目录 SQL Server温故系列(1):SQL 数据操作 CRUD 之增删改合 SQL Server温故系 ...

  4. SQL Server进阶(六)表表达式--派生表、公用表表达式(CTE)、视图和内联表值函数

    概述 表表达式是一种命名的查询表达式,代表一个有效地关系表.可以像其他表一样,在数据处理中使用表表达式. SQL Server支持四种类型的表表达式:派生表,公用表表达式,视图和内联表值函数. 为什么 ...

  5. SQL Server 表表达式--派生表、公用表表达式(CTE)、视图和内联表值函数

    概述 表表达式是一种命名的查询表达式,代表一个有效地关系表.可以像其他表一样,在数据处理中使用表表达式. SQL Server支持四种类型的表表达式:派生表,公用表表达式,视图和内联表值函数. 为什么 ...

  6. SQL Server 公用表表达式(CTE)实现递归

    公用表表达式简介: 公用表表达式 (CTE) 可以认为是在单个 SELECT.INSERT.UPDATE.DELETE 或 CREATE VIEW 语句的执行范围内定义的临时结果集.CTE 与派生表类 ...

  7. SQL Server 公用表表达式(CTE)实现递归的方法

    公用表表达式简介: 公用表表达式 (CTE) 可以认为是在单个 SELECT.INSERT.UPDATE.DELETE 或 CREATE VIEW 语句的执行范围内定义的临时结果集.CTE 与派生表类 ...

  8. SQL Server温故系列(1):SQL 数据操作 CRUD 之增删改合

    1.插入语句 INSERT INTO 1.1.用 INSERT 插入单行数据 1.2.用 INSERT 插入多行数据 1.3.用 INSERT 插入子查询结果行 1.4.INSERT 小结及特殊字段插 ...

  9. SQL Server温故系列(2):SQL 数据操作 CRUD 之简单查询

    1.查询语句 SELECT 1.1.查询语句的 SELECT 子句 1.2.查询语句的 FROM 子句 1.2.1.内连接查询 INNER JOIN 1.2.2.外连接查询 OUTER JOIN 1. ...

随机推荐

  1. JDBC学习笔记——增删改查

    1.数据库准备  要用JDBC操作数据库,第一步当然是建立数据表: ? 1 2 3 4 5 6 CREATE TABLE `user` (   `id` int(11) NOT NULL AUTO_I ...

  2. centos6.5安装apache2.x并更改默认80port

    因为现在nginx它已成为反向代理软件的主流.因此,linuxserver在系统.我们几乎总是安装nginx东方的,这使在同一时间使用apache带来了麻烦.因为apache它用于通过缺省80port ...

  3. UVA 10869 - Brownie Points II(树阵)

    UVA 10869 - Brownie Points II 题目链接 题意:平面上n个点,两个人,第一个人先选一条经过点的垂直x轴的线.然后还有一个人在这条线上穿过的点选一点作垂直该直线的线,然后划分 ...

  4. Tab切换效果的实现

    <!--引用jquery和bootstrap--> <link rel="stylesheet" href="~/Content/bootstrap.m ...

  5. 更新EF(更新表 添加表…)

           

  6. jquery 可以给事件传参数

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  7. 一步一步造个IoC轮子(一):IoC是什么

    一步一步造个Ioc轮子目录 一步一步造个IoC轮子(一):IoC是什么 一步一步造个IoC轮子(二):详解泛型工厂 一步一步造个IoC轮子(三):构造基本的IoC容器 前言 .net core正式版前 ...

  8. 隐藏system窗口你也行(就是那个cmd窗口,好多方法)

    新process的启动,可以通过system(const char*)函数启动另外一个程序,但是有一个小问题,就是每次执行system函数的时候,都会弹出dos窗口(命令行嘛~~~),有没有什么方法能 ...

  9. 将WriteableBitmap转为byte[]

    原文:将WriteableBitmap转为byte[] Win8 metro中的操作与之前的版本有所不同,因此作为一个新手,我将自己的一些问题解答记录了下来,希望与大家分享!! 下面是将Writeab ...

  10. delphi 获取当前进程的cpu占用率

    type  TProcessCpuUsage = record  private    FLastUsed, FLastTime: Int64;    FCpuCount:Integer;  publ ...