单表
分页
PageHelper是早期MyBatis技术栈中最为常用的分页方式。(早先常见的面试题就有:如何实现数据库的分页?有哪几种方式?)
PageHelper有哪些使用方式呢?
//第一种,RowBounds方式的调用
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<User> list = userMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));
如何实现分页
我们知道如何使用PageHelper后,我们发现使用PageHelper.startPage(pageNum, pageSize, orderBy)方法后的第一个select是具备分页能力的,那它是如何做到的呢?
理解它的原理,有两个点:
第一,相对对于JDBC这种嵌入式的分页而言,PageHelper分页是独立的,能做到独立分页查询,那它必然是通过某个拦截点进行了拦截,这样它才能够进行解耦分离出分页。
第二,我们通过PageHelper.startPage(pageNum, pageSize, orderBy)方法后的第一个select是具备分页能力的,那它必然缓存了分页信息,同时结合线程知识,这里必然使用的是本地栈ThreadLocal,即每个线程有一个本地缓存。
所以结合这两点,聪明的你就会想到它大概是如何实现的,关键就是两点(拦截,ThreadLocal).
如何评价PageHelper的封装和使用?
就使用PageHelper本身而言,第二种和第三种方式较为中规中矩,它是早前绝大多数的使用方式。并不是说中规中矩就是不好的,很多场景下保守能让团队的绝大多数人接受便是好的方式。当更多的人接受Java8 lambda方式后,Page
就本例对PageHelper的封装来看,通常而言针对PageHelper封装有两个方式,一种是封装PageUtils,另一种就是放在上层Controller中, 这个例子中使用第二种。它并不是非常完美的封装方式,但确实是在有限的需求内是一个完整的封装。需要记住一点,过多的设计和封装也没有必要,重点在于封装是伴随着需求的改变而适度调整而来的。这个例子中可能出现需求变更从而影响其封装的点,比如排序可能是组合排序,比如先按照A字段的升序再按照B字段的降序排序,比如SQLUtil等带来的Util冗余等。
就分页未来来看,PageHelper不是长远的趋势,真正长远的趋势应该是被ORM融合,并且考虑更多的适用场景提供各种常见实现方式的封装功能(即约定大于实现中的约定),同时结合builder参数构建和lambda方式写法等语言特性写法上做到优雅。所以你可以看到JPA,MyBatis-plus等中相关的封装和实现就是这种趋势。
多表
业务
用户管理:关联角色,关联部门,及通过部门过滤用户
角色管理
部门管理(树形结构)
需求
我们来看下角色(一对多,一个用户有多个角色)和部门(一对一,一个用户属于一个部门)是如何实现的。
项目
User
public class User extends BaseEntity {
// ...
/**
* 部门对象
*/
private Dept dept;
private List<Role> roles;
}
User和Role的关联类
public class UserRole {
/**
* 用户ID
*/
private Long userId;
/**
* 角色ID
*/
private Long roleId;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("userId", getUserId())
.append("roleId", getRoleId())
.toString();
}
}
UserMapper中通过association关联dept,和通过collection关联role集合。
<resultMap type="tech.pdai.mybatis.xml.web.system.user.domain.User" id="UserResult">
<id property="userId" column="user_id" />
<result property="deptId" column="dept_id" />
<result property="loginName" column="login_name" />
<result property="userName" column="user_name" />
<result property="email" column="email" />
<result property="phonenumber" column="phonenumber" />
<result property="sex" column="sex" />
<result property="avatar" column="avatar" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<association property="dept" column="dept_id" javaType="tech.pdai.mybatis.xml.web.system.dept.domain.Dept" resultMap="deptResult" />
<collection property="roles" javaType="java.util.List" resultMap="RoleResult" />
</resultMap>
<resultMap id="deptResult" type="tech.pdai.mybatis.xml.web.system.dept.domain.Dept">
<id property="deptId" column="dept_id" />
<result property="parentId" column="parent_id" />
<result property="deptName" column="dept_name" />
<result property="orderNum" column="order_num" />
<result property="leader" column="leader" />
<result property="status" column="dept_status" />
</resultMap>
<resultMap id="RoleResult" type="tech.pdai.mybatis.xml.web.system.role.domain.Role">
<id property="roleId" column="role_id" />
<result property="roleName" column="role_name" />
<result property="roleKey" column="role_key" />
<result property="roleSort" column="role_sort" />
<result property="dataScope" column="data_scope" />
<result property="status" column="role_status" />
</resultMap>
增
<insert id="insertSelective" parameterType="com.example.newbie.entity.AdminUser">
insert into tb_newbee_mall_admin_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="adminUserId != null">
admin_user_id,
</if>
<if test="loginUserName != null">
login_user_name,
</if>
<if test="loginPassword != null">
login_password,
</if>
<if test="nickName != null">
nick_name,
</if>
<if test="locked != null">
locked,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="adminUserId != null">
#{adminUserId,jdbcType=BIGINT},
</if>
<if test="loginUserName != null">
#{loginUserName,jdbcType=VARCHAR},
</if>
<if test="loginPassword != null">
#{loginPassword,jdbcType=VARCHAR},
</if>
<if test="nickName != null">
#{nickName,jdbcType=VARCHAR},
</if>
<if test="locked != null">
#{locked,jdbcType=TINYINT},
</if>
</trim>
</insert>
<insert id="insertUser" parameterType="tech.pdai.mybatis.xml.web.system.user.domain.User" useGeneratedKeys="true" keyProperty="userId">
insert into sys_user(
<if test="userId != null and userId != 0">user_id,</if>
<if test="deptId != null and deptId != 0">dept_id,</if>
<if test="loginName != null and loginName != ''">login_name,</if>
<if test="userName != null and userName != ''">user_name,</if>
<if test="email != null and email != ''">email,</if>
<if test="avatar != null and avatar != ''">avatar,</if>
<if test="phonenumber != null and phonenumber != ''">phonenumber,</if>
<if test="sex != null and sex != ''">sex,</if>
<if test="status != null and status != ''">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null and remark != ''">remark,</if>
create_time
)values(
<if test="userId != null and userId != ''">#{userId},</if>
<if test="deptId != null and deptId != ''">#{deptId},</if>
<if test="loginName != null and loginName != ''">#{loginName},</if>
<if test="userName != null and userName != ''">#{userName},</if>
<if test="email != null and email != ''">#{email},</if>
<if test="avatar != null and avatar != ''">#{avatar},</if>
<if test="phonenumber != null and phonenumber != ''">#{phonenumber},</if>
<if test="sex != null and sex != ''">#{sex},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null and remark != ''">#{remark},</if>
sysdate()
)
</insert>
查
<select id="findCarouselList" parameterType="Map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tb_newbee_mall_carousel
where is_deleted = 0
order by carousel_rank desc
<if test="start!=null and limit!=null">
limit #{start},#{limit}
</if>
</select>
<sql id="selectUserVo">
<!-- 四张表联查 -->
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_time, u.remark,
d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user_role ur on u.user_id = ur.user_id
left join sys_role r on r.role_id = ur.role_id
</sql>
<select id="selectUserList" parameterType="tech.pdai.mybatis.xml.web.system.user.domain.User" resultMap="UserResult">
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<if test="loginName != null and loginName != ''">
AND u.login_name like concat('%', #{loginName}, '%')
</if>
<if test="status != null and status != ''">
AND u.status = #{status}
</if>
<if test="phonenumber != null and phonenumber != ''">
AND u.phonenumber like concat('%', #{phonenumber}, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(u.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
</if>
<if test="deptId != null and deptId != 0">
AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET (#{deptId},ancestors) ))
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
<select id="selectAllocatedList" parameterType="tech.pdai.mybatis.xml.web.system.user.domain.User" resultMap="UserResult">
select distinct u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.avatar, u.phonenumber, u.status, u.create_time
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user_role ur on u.user_id = ur.user_id
left join sys_role r on r.role_id = ur.role_id
where u.del_flag = '0' and r.role_id = #{roleId}
<if test="loginName != null and loginName != ''">
AND u.login_name like concat('%', #{loginName}, '%')
</if>
<if test="phonenumber != null and phonenumber != ''">
AND u.phonenumber like concat('%', #{phonenumber}, '%')
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
<select id="selectUnallocatedList" parameterType="tech.pdai.mybatis.xml.web.system.user.domain.User" resultMap="UserResult">
select distinct u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.avatar, u.phonenumber, u.status, u.create_time
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user_role ur on u.user_id = ur.user_id
left join sys_role r on r.role_id = ur.role_id
where u.del_flag = '0' and (r.role_id != #{roleId} or r.role_id IS NULL)
and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and ur.role_id = #{roleId})
<if test="loginName != null and loginName != ''">
AND u.login_name like concat('%', #{loginName}, '%')
</if>
<if test="phonenumber != null and phonenumber != ''">
AND u.phonenumber like concat('%', #{phonenumber}, '%')
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
<select id="selectUserByLoginName" parameterType="String" resultMap="UserResult">
<include refid="selectUserVo"/>
where u.login_name = #{userName}
</select>
<select id="selectUserByPhoneNumber" parameterType="String" resultMap="UserResult">
<include refid="selectUserVo"/>
where u.phonenumber = #{phonenumber}
</select>
<select id="selectUserByEmail" parameterType="String" resultMap="UserResult">
<include refid="selectUserVo"/>
where u.email = #{email}
</select>
<select id="checkLoginNameUnique" parameterType="String" resultType="int">
select count(1) from sys_user where login_name=#{loginName}
</select>
<select id="checkPhoneUnique" parameterType="String" resultMap="UserResult">
select user_id, phonenumber from sys_user where phonenumber=#{phonenumber}
</select>
<select id="checkEmailUnique" parameterType="String" resultMap="UserResult">
select user_id, email from sys_user where email=#{email}
</select>
<select id="selectUserById" parameterType="Long" resultMap="UserResult">
<include refid="selectUserVo"/>
where u.user_id = #{userId}
</select>
删
<delete id="deleteUserById" parameterType="Long">
delete from sys_user where user_id = #{userId}
</delete>
<delete id="deleteUserByIds" parameterType="Long">
update sys_user set del_flag = '2' where user_id in
<foreach collection="array" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</delete>
改
<update id="updateUser" parameterType="tech.pdai.mybatis.xml.web.system.user.domain.User">
update sys_user
<set>
<if test="deptId != null and deptId != 0">dept_id = #{deptId},</if>
<if test="loginName != null and loginName != ''">login_name = #{loginName},</if>
<if test="userName != null and userName != ''">user_name = #{userName},</if>
<if test="email != null and email != ''">email = #{email},</if>
<if test="phonenumber != null and phonenumber != ''">phonenumber = #{phonenumber},</if>
<if test="sex != null and sex != ''">sex = #{sex},</if>
<if test="avatar != null and avatar != ''">avatar = #{avatar},</if>
<if test="status != null and status != ''">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where user_id = #{userId}
</update>
<update id="updateByPrimaryKeySelective" parameterType="com.example.newbie.entity.AdminUser">
update tb_newbee_mall_admin_user
<set>
<if test="loginUserName != null">
login_user_name = #{loginUserName,jdbcType=VARCHAR},
</if>
<if test="loginPassword != null">
login_password = #{loginPassword,jdbcType=VARCHAR},
</if>
<if test="nickName != null">
nick_name = #{nickName,jdbcType=VARCHAR},
</if>
<if test="locked != null">
locked = #{locked,jdbcType=TINYINT},
</if>
</set>
where admin_user_id = #{adminUserId,jdbcType=BIGINT}
</update>
<update id="deleteBatch">
update tb_newbee_mall_carousel
set is_deleted=1,update_time=now() where carousel_id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</update>
注意
User和Role之间是一对多关系,所以是独立的表,在操作用户时需要同步删除User和Role的关联表UserRole
/**
* 通过用户ID删除用户
*
* @param userId 用户ID
* @return 结果
*/
@Override
public int deleteUserById(Long userId) {
// 删除用户与角色关联
userRoleMapper.deleteUserRoleByUserId(userId);
return userMapper.deleteUserById(userId);
}
代码生成
-
使用模板(vm, freemarker等)生成
-
使用IDE插件, 比如EasyCode
Mybatis-Plus
之前的是最优雅的实现么
第一,从代码风格来看,源作者应该是吸收了早先SSM传统CRM后台框架的经验演进过来的,这个后面我会有两篇文章来帮你构建十年前项目是如何开发的认知;因为不是从头写的(考虑到代价, 沿用了原有的一些配置和Util等),所以必然不会是最新最优雅的方式,后续几点具体说明。
第二,过多的自定义的Util,而现有的项目项目中,Util一定不要自己造轮子,原则是优先使用Guava,commons等开源工具类;还有一些新的hutool等工具库是可以满足绝大部分需求的,如果没有再添加。于此同时,如果项目规模稍微大一点和要求,还可以使用外观模式进行小量的封装,比如你同时使用了Guava,Common库中StringUtils,那么可以增加一个MyStringUtils,将常用方法融入这个类,对项目中所有类来说只使用MyStringUtils。此外这里的例子还有一些标注为@Component等模块作为util,造成了代码的耦合度增加。
第三,没有完全注解化,还有一部分是xml;使用约定大于配置一定是最优雅的必要条件,因为大部分starters是支持的,比如这里的mybatis-springboot-starter中已经提供了mybatis的配置,无需再使用xml方式的配置了。
第四,Mapper和Service层抽象的缺失
第五,更为优雅的Lamada语法使用,比如MyBatis-Plus WrapperQuery的lambda查询方式。
/**
* 名字为王姓并且(年龄小于40或者邮箱不为空)
* sql:name like '王%' and (age < 40 or email is not null)
*/
@Test
public void selectLambda2() {
LambdaQueryWrapper<User> lambdaQuery = Wrappers.<User>lambdaQuery();
lambdaQuery.likeLeft(User::getName, "王")
.lt(User::getAge, 40)
.isNotNull(User::getEmail);
List<Object> userList = userMapper.selectObjs(lambdaQuery);
userList.forEach(System.out::println);
}
@Test
public void selectLambda3() {
List<User> userList = new LambdaQueryChainWrapper<User>(userMapper)
.like(User::getName, "雨")
.ge(User::getAge, 20)
.list();
userList.forEach(System.out::println);
}
可以看下MyBatis-Plus官方测试用例 WrapperTest.java
第六,删除Fastjson等漏洞较多的使用库,改用Jackson;
解题思路
思维
1.顺序思维
- 从里到外:从条件开始
复杂时, 也可以反过来先写出子查询, 用辅助表辅助做题.
- 从外到内: 从结果开始
先select出所有所求的结果, 再join所需的条件
- 核心本质: 拿到题先看最后的表格是什么样,然后跟原表进行对比,然后再思考具体思路.
2.链条思维
各种条件都存在顺序关系, 需要理顺它们, 然后采取从里往外或者从外往内的方式去解题.
在考虑当前条件a时, 还必须考虑条件a的前提条件b,包括需要排除的条件.
3. 行转列思维
SQL中的数据统统都是通过select在列中取的,所以如果发现需求的字段并非原表中的列, 则果断考虑使用条件语句进行行转列.
4. 思考特殊情况
- null值处理(是转换还是过滤掉)
- 除法遇到分母为0的情况(可能会报错)
- 两个int值相除, 可能还是一个int(根据不同的DBMS)
易错点
窗口函数作为条件必须放在外层查询中, 而不是当前查询的where语句中.
模型
查询
一对一和一对多关系可以直接多表查, 如下所示, 但是多对多关系需要用连接查询.
select
SCORE.SNO,
CNO,
DEGREE
from STUDENT, SCORE
where STUDENT.SNO = SCORE.SNO;
两表联查
left join/right join :
- 特点:主表的所有数据均显示,另外一张表中有匹配的数据,则显示,若无匹配的数据,该字段显示为null
inner join:
- 特点:查询的数据是满足所有条件的数据,若没有满足所有条件的数据,则结果集为空
group by
group by cola
可以查询到分组后的cola, 相关的聚合函数, 与cola具有一对一关系的列, 除此之外, 其他字段都查询不到.
case…when
类似Java中的if…else
select name,course,score,
(case when score>=90 then '优秀'
when score>=80 then '良好'
when score>=70 then '中等'
when score>=60 then '及格'
ELSE '不及格' end)level
from score
经常和聚合函数配合使用.
注意点:
case when 和sum()和count()使用时,用法不同(意义不同)
- sum()配合使用,满足条件返回1,表示进行+1 else 0表示+0
- count()配合使用,返回的值只要不是null,就进行一次计数,所以使用count()必须注意:若满足条件想进行一次计数,不满足条件不计数,应写成:
count(case when ....then 1 else null end)
或count(case when ...then 1 end)
没有else,表示不满足条件,返回null
描述查询语句中各部分的执行顺序
- where: 筛选条件— 分组之前
- having: 筛选条件----前提: 必然先分组, 对分组后的结果再次筛选
- where, group by, having三者同时出现, 则顺序一定为: where … group by … having …
- limit用法----选择需要的数据
- limit m,n 从索引m处开始, 筛选n条件数据–实现分页
- limit m, 从第一条数据开始, 筛选前m条
具体的执行顺序
- from join 产生虚拟表V1 —产生笛卡尔积—两表完全连接的结果叫做笛卡尔积, 会产生m*n条数据.
- on 根据连接条件进行筛选, 基于v1, 产生虚拟表v2
- left/right join 若是外连接,将主表中的所有数据进行补充到v2表中
- where 根据查询条件,从v2中进行数据筛选,产生虚拟表v3
- group by 对v3分组,产生虚拟表v4
- 经常会和聚合函数配合使用,聚合函数的结果在group by之后就会产生
- 使用场景: 统计每个班级的学生人数
select count(xx) ....group by bj_id
- 统计每个部门的人数
select count(xx) ... group by d.id
- having 对分组后的结果再次筛选 ,产生虚拟表v5
- select 选择最终要显示的字段,产生虚拟表v6
- distinct 对结果集进行去重,产生虚拟表v7
- order by 对结果根据某字段进行升序/降序排列
- limit 选择需要的数据
步骤
- 判断主表: A的B, C, D; 则A为主表.
- 判断on筛选条件: 找到共同的字段.
- 判断查询方式: 内连接—一一对应/外连接—需要读取别的表信息, 关键字“所有”.
- 判断分组条件: 找出分组依据–需要聚合函数的表的id.
- 如果嵌套查询(有更多限制信息),则写出相应sql语句, 利用having进行再次筛选.
- 表中唯一存在不需要别名.
三表联查
多表联查,既有内联又有外联的语法:
select...from A join B on A.xx=B.xx left join C on A.xx=C.xx
, 内联之后结果集和C外连接
不要写成:
select..from A join B left join C on A.xx=B.xx and A.xx=C.xx
若三表全都内联语法:
select..from A join B join C on A.xx=B.xx and A.xx=C.xx
不要写成:
select..from A join B on A.XX=B.xx join C on A.xx=c.XX
语法通过,结果也是正确的
步骤
- 题目特征: 查询信息横跨多个表
- 判断主表: A的B, C, D; 则A为主表.
- 判断on筛选条件: 找到共同字段
- 判断连接方式: 既有内连接又有外连接–存在为空的情况, 存在为空表和主表外连接/全部内连接.
- 判断分组条件: 找出分组依据–需要聚合函数的表的id.
- 如果嵌套查询(有更多限制信息),则写出相应sql语句, 利用having进行再次筛选.
- 表中唯一存在不需要别名.
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Sep 15,2022