[置顶] 子查询改为左链接

SQL子查询与JOIN优化
本文探讨了SQL中子查询的应用及其与JOIN操作的关系,包括如何将子查询转换为JOIN来提高性能,以及LEFT JOIN结合IS NULL的使用方法。同时介绍了查询逻辑顺序,帮助读者更好地理解SQL执行流程。

子查询是一个 SELECT 查询,它嵌套在 SELECT、INSERT、UPDATE、DELETE 语句或其它子查询中。
  任何允许使用表达式的地方都可以使用子查询,子查询可以使我们的编程灵活多样,可以用来实现一些特殊的功能。但是在性能上,往往一个不合适的子查询用法会形成一个性能瓶颈。如果子查询的条件中使用了其外层的表的字段,这种子查询就叫作相关子查询。


相关子查询可以用IN、NOT IN、EXISTS、NOT EXISTS引入。 关于相关子查询,应该注意:
(1) NOT IN、NOT EXISTS的相关子查询可以改用LEFT JOIN代替写法。比如:

SELECT PUB_NAME FROM PUBLISHERS WHERE PUB_ID NOT IN (SELECT PUB_ID FROM TITLES WHERE TYPE ='BUSINESS')

可以改写成:
SELECT A.PUB_NAME FROM PUBLISHERS A LEFT JOIN TITLES B ON B.TYPE ='BUSINESS'AND A.PUB_ID=B. PUB_ID WHERE B.PUB_ID IS NULL
  
比如NOT EXISTS:
SELECT TITLE FROM TITLES  WHERE NOT EXISTS  (SELECT TITLE_ID FROM SALES WHERE TITLE_ID = TITLES.TITLE_ID)
可以改写成:
SELECT TITLE  FROM TITLES LEFT JOIN SALES  ON SALES.TITLE_ID = TITLES.TITLE_ID  WHERE SALES.TITLE_ID ISNULL

2)如果保证子查询没有重复 ,IN、EXISTS的相关子查询可以用INNER JOIN 代替。比如:
SELECT PUB_NAME  FROM PUBLISHERS  WHERE PUB_ID IN  (SELECT PUB_ID  FROM TITLES  WHERE TYPE ='BUSINESS')
可以改写成:
SELECT DISTINCT A.PUB_NAME  FROM PUBLISHERS  A JOIN TITLES  B ON A. PUB_ID=B. PUB_ID WHERE B. TYPE ='BUSINESS'

 

  •  LEFT JOIN 和 is null 的理解

SELECT *  from  t1   LEFT JOIN t2   ON   t1.id=t2.id     WHERE t2.id IS NULL;
假定t2.id定义为NOT NULL。在这种情况下,MySQL使用t1.id的值扫描t1并查找t2中的行。
如果MySQL在t2中发现一个匹配的行,它知道t2.id绝不会为NULL,并且不再扫描t2内有相同的id值的行。
换句话说,对于t1的每个行,MySQL只需要在t2中查找一次,无论t2内实际有多少匹配的行。
当通过t1扫描查找到t2记录时,后面的where条件不成立,所以t1停止查找。

当通过t1扫描没有查找到t2记录时,t2.id自然为空,后面的where条件成立,返回的结果就是不在t2中的记录。

  • 查询逻辑顺序

在学生成绩表中 (暂记为 tb_Grade),“考生姓名”内容不为空的记录按照“考生姓名”分组,并且筛选分组结果,选出“总成绩”大于 600分的,标准顺序的 SQL语句为:

select考生姓名, max(总成绩) as max总成绩

from tb_Grade

where考生姓名 is not null

group by考生姓名

having max(总成绩) > 600

order by max总成绩

 

在上面的示例中 SQL语句的执行顺序如下:

(1).首先执行 FROM子句, tb_Grade表组装数据源的数据

(2).执行 WHERE子句,筛选 tb_Grade表中所有数据不为 NULL的数据

(3).执行 GROUP BY子句, tb_Grade表按“学生姓名”列进行分组

(4).计算 max()聚集函数,“总成绩”求出总成绩中最大的一些数值

(5).执行 HAVING子句,筛选课程的总成绩大于 600分的.

(7).执行 ORDER BY子句,把最后的结果按Max成绩”进行排序.

 

<think>我们有一个需求:在Oracle数据库中,编写查询语句,关联两个表(假设表b和cthr_ry_lczj),并按照特定排序规则获取每个人员符合条件的记录,且每个人员只取一条符合条件的记录(可能是按某个规则排序后的第一条)。 根据问题,我们可能需要使用到分组(GROUP BY)或者窗口函数(如ROW_NUMBER())来为每个人员选择一条记录,同时需要自定义排序规则。 假设场景: 表b:人员基本信息表(例如,包含人员ID:ry_id,姓名等) 表cthr_ry_lczj:人员的操作记录表(例如,包含记录ID,人员ID:ry_id,操作时间:czsj,操作类型:czlx等) 要求:每个人员取一条最新的操作记录(按操作时间倒序),但是有一个特定的要求:如果存在某种操作类型(比如czlx='某个特定值')的记录,那么这条记录要优先排在第一位(即使它不是时间最新的),如果没有这种特定操作类型,则取时间最新的记录。 注意:这里每个人员只取一条记录。 步骤: 1. 关联两个表,获取每个人员的操作记录。 2. 对于每个人员,我们需要按照自定义规则排序(特定操作类型优先,然后按操作时间倒序)。 3. 然后使用ROW_NUMBER()窗口函数,按人员分组,并按上述规则排序,取每个人员的第一条。 具体实现: 假设我们想要的结果是:每个人员的记录,如果有操作类型为'特定值'(例如'特殊操作')的记录,则取这条记录(如果有多个,取时间最新的那个?或者按其他规则?),如果没有,则取操作时间最新的记录。 但问题中并没有明确特定值记录的排序规则,我们假设:对于同一个人员,如果有多个特定值记录,我们取时间最新的。所以排序规则为: - 首先,按是否是特定值排序:特定值的记录排在最前面(我们可以用CASE表达式将特定值记录的标志置为1,其他为2,然后按这个标志升序) - 其次,按操作时间倒序(这样每个人员的最新记录就会排在前面) 然后,我们使用ROW_NUMBER(),按人员分组,按上述规则排序,取第一行。 示例SQL: ```sql SELECT * FROM ( SELECT b.ry_id, b.name, c.czid, c.czsj, c.czlx, ROW_NUMBER() OVER ( PARTITION BY c.ry_id -- 按人员分组 ORDER BY CASE WHEN c.czlx = '特殊操作' THEN 1 -- 特定操作类型排第一优先级 ELSE 2 END, -- 先按是否是特定操作类型排序(特定操作在前) c.czsj DESC -- 再按操作时间倒序(最新时间在前) ) AS rn FROM b INNER JOIN cthr_ry_lczj c ON b.ry_id = c.ry_id WHERE -- 可以添加其他条件 ) WHERE rn = 1; -- 每个人员取第一条 ``` 解释: 1. 子查询中,我们关联两个表,并选择需要的列。 2. 使用ROW_NUMBER()窗口函数,按人员ID分组(PARTITION BY c.ry_id),在组内按照自定义规则排序:先按操作类型是否为'特殊操作'(是则排前面,用1表示,否则2),然后按操作时间倒序(即最新的时间排前面)。 3. 这样,对于每个人员: 如果有'特殊操作'的记录,那么这些记录会排在前面(然后按时间倒序,所以最新的特殊操作会排在最前),然后取第一条(rn=1)就是该人员最新的特殊操作记录(如果有的话)。 如果没有'特殊操作',那么所有记录都是优先级2,然后按时间倒序,所以第一条就是最新的普通操作记录。 注意:如果同一个人员有多条'特殊操作'记录,我们取其中时间最新的(因为排序中在优先级1内按时间倒序)。如果不需要取最新的特殊操作,而是只要有一条特殊操作就取(不管时间),那么可以去掉时间排序,或者根据需求调整。 如果需求是:只要有一条特殊操作记录,就取该记录(并且取其中一条,比如按时间取最新的),那么上述方法满足。 如果需求是:特殊操作记录中取时间最早的,那么将时间排序改为升序(ASC)即可。 另外,如果表中有其他条件,可以在WHERE子句中添加。 如果两个表关联后数据量很大,可以考虑在子查询中先过滤(比如时间范围等)以减少数据量。 性能优化:在窗口函数的分组字段(ry_id)和排序字段(czlx, czsj)上建立索引可能会提高性能。 关于引用:用户提供的引用[1]中提到了数据库设计规范化和可维护性,我们在设计查询时也应考虑代码的可读性和可维护性。引用[2]和[3]与当前问题关联不大。 因此,按照上述方法编写SQL即可满足需求。 相关问题: 1. 如何修排序规则以实现多个特定值的优先级排序(例如,同时要求'特殊操作1'排第一,'特殊操作2'排第二)? 2. 如果同一个人员有多个相同优先级的记录,如何同时获取前N条记录? 3. 除了ROW_NUMBER(),还有其他窗口函数可以实现类似需求吗? 4. 在Oracle中,如何优化窗口函数的查询性能?</think>在 Oracle 数据库中,当需要关联多个表并按特定规则排序(如指定某个值优先显示),同时获取每个人员符合条件的记录时,可以通过以下步骤实现: ### 解决方案:使用窗口函数 + 自定义排序 ```sql SELECT * FROM ( SELECT ry.ry_id, -- 人员ID ry.name, -- 姓名 lczj.lczj_id, -- 操作记录ID lczj.czsj, -- 操作时间 lczj.czlx, -- 操作类型 ROW_NUMBER() OVER ( PARTITION BY ry.ry_id -- 按人员分组 ORDER BY CASE WHEN lczj.czlx = '特殊操作' THEN 1 -- 特定值优先 ELSE 2 END, -- 自定义排序优先级 lczj.czsj DESC -- 次级排序:操作时间倒序 ) AS rn FROM b ry -- 人员表 INNER JOIN cthr_ry_lczj lczj -- 操作记录表 ON ry.ry_id = lczj.ry_id WHERE lczj.czsj > SYSDATE - 30 -- 示例条件:30天内记录 ) WHERE rn = 1; -- 每个人员取第一条记录 ``` ### 关键说明 1. **核心组件** - `ROW_NUMBER() OVER (PARTITION BY ...)`:窗口函数实现分组排序 - `CASE WHEN ... THEN 1 ELSE 2 END`:自定义排序逻辑(特定值优先) - `INNER JOIN`:关联人员表和操作记录表 2. **自定义排序逻辑** ```sql ORDER BY CASE WHEN lczj.czlx = '特殊操作' THEN 1 -- 优先级1(置顶) WHEN lczj.czlx = '重要操作' THEN 2 -- 优先级2 ELSE 3 -- 其他 END, lczj.czsj DESC -- 次级排序:时间倒序 ``` 3. **多条件过滤示例** ```sql WHERE lczj.czlx IN ('特殊操作','常规操作') -- 操作类型过滤 AND ry.status = '在职' -- 人员状态过滤 AND lczj.czsj BETWEEN TO_DATE('2023-01-01','YYYY-MM-DD') AND SYSDATE -- 时间范围 ``` ### 性能优化建议 1. **索引策略** ```sql -- 操作记录表索引 CREATE INDEX idx_lczj_ry ON cthr_ry_lczj(ry_id, czlx, czsj); -- 人员表索引 CREATE INDEX idx_ry_status ON b(ry_id, status); ``` 2. **执行计划检查** ```sql EXPLAIN PLAN FOR -- 您的查询语句 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); ``` ### 实际应用场景 假设需要获取每个员工最近的特殊报销记录(特殊报销优先显示): ```sql SELECT * FROM ( SELECT emp.employee_id, emp.name, exp.expense_id, exp.amount, exp.expense_type, ROW_NUMBER() OVER ( PARTITION BY emp.employee_id ORDER BY CASE WHEN exp.expense_type = '紧急报销' THEN 1 ELSE 2 END, exp.submit_date DESC ) AS rn FROM employees emp JOIN expenses exp ON emp.employee_id = exp.employee_id WHERE exp.status = '已审批' ) WHERE rn = 1; ``` ### 技术优势 1. **精准控制排序**:通过`CASE`表达式实现任意自定义排序规则 2. **高效分组处理**:窗口函数避免多次表扫描,性能优于自连接 3. **灵活结果过滤**:通过`rn=1`轻松获取每组首条记录 4. **可维护性**:清晰分离排序逻辑与业务逻辑[^1] > **提示**:当需要获取每组前N条记录时,只需修外层`WHERE rn <= N`。此模式也适用于分页查询等场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值