7.3 动态SQL与性能优化

面试重要程度:⭐⭐⭐⭐⭐

常见提问方式:动态SQL标签使用、批量操作优化、SQL性能调优

预计阅读时间:30分钟

开场白

兄弟,动态SQL绝对是MyBatis的核心特性!面试官特别喜欢考这个,因为它直接体现你对复杂业务场景的处理能力。特别是批量操作优化,这在实际项目中太重要了!

今天我们就把动态SQL的各种标签用法和性能优化技巧全部搞定,让你在面试中展现出对数据访问层的深度理解。

🔄 动态SQL标签详解

核心标签全解析

面试必问:

面试官:"MyBatis的动态SQL有哪些标签?如何处理复杂的查询条件?"

1. if标签 - 条件判断

<mapper namespace="com.example.mapper.UserMapper">
    
    <!-- 基础条件查询 -->
    <select id="selectByCondition" resultType="User">
        SELECT * FROM user
        WHERE 1=1
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="createTimeStart != null">
            AND create_time >= #{createTimeStart}
        </if>
        <if test="createTimeEnd != null">
            AND create_time <= #{createTimeEnd}
        </if>
    </select>
    
    <!-- 复杂条件判断 -->
    <select id="selectByComplexCondition" resultType="User">
        SELECT * FROM user
        WHERE 1=1
        <if test="condition != null">
            <if test="condition.name != null and condition.name.trim() != ''">
                AND name LIKE CONCAT('%', #{condition.name}, '%')
            </if>
            <if test="condition.ageRange != null">
                <if test="condition.ageRange.min != null">
                    AND age >= #{condition.ageRange.min}
                </if>
                <if test="condition.ageRange.max != null">
                    AND age <= #{condition.ageRange.max}
                </if>
            </if>
            <if test="condition.statusList != null and condition.statusList.size() > 0">
                AND status IN
                <foreach collection="condition.statusList" item="status" open="(" separator="," close=")">
                    #{status}
                </foreach>
            </if>
        </if>
    </select>
    
</mapper>

2. where标签 - 智能WHERE处理

<!-- where标签自动处理AND/OR -->
<select id="selectByConditionWithWhere" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
    </where>
    ORDER BY create_time DESC
</select>

<!-- 对比:不使用where标签的问题 -->
<select id="badExample" resultType="User">
    SELECT * FROM user
    WHERE 
    <if test="name != null and name != ''">
        name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="age != null">
        AND age = #{age}  <!-- 如果name为空,这里会出现语法错误 -->
    </if>
</select>

3. choose/when/otherwise - 多分支选择

<!-- 类似Java的switch-case -->
<select id="selectByPriority" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="id != null">
                id = #{id}
            </when>
            <when test="email != null and email != ''">
                email = #{email}
            </when>
            <when test="phone != null and phone != ''">
                phone = #{phone}
            </when>
            <when test="username != null and username != ''">
                username = #{username}
            </when>
            <otherwise>
                status = 1  <!-- 默认查询激活用户 -->
            </otherwise>
        </choose>
    </where>
</select>

<!-- 复杂业务场景应用 -->
<select id="selectByUserType" resultType="User">
    SELECT 
        <choose>
            <when test="userType == 'VIP'">
                id, name, email, vip_level, vip_expire_time
            </when>
            <when test="userType == 'ADMIN'">
                id, name, email, role, permissions
            </when>
            <otherwise>
                id, name, email
            </otherwise>
        </choose>
    FROM user
    <where>
        <choose>
            <when test="userType == 'VIP'">
                vip_level > 0 AND vip_expire_time > NOW()
            </when>
            <when test="userType == 'ADMIN'">
                role IN ('ADMIN', 'SUPER_ADMIN')
            </when>
            <otherwise>
                status = 1
            </otherwise>
        </choose>
    </where>
</select>

4. foreach标签 - 循环处理

<!-- IN查询 -->
<select id="selectByIds" resultType="User">
    SELECT * FROM user
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<!-- 批量条件查询 -->
<select id="selectByMultipleConditions" resultType="User">
    SELECT * FROM user
    WHERE 
    <foreach collection="conditions" item="condition" separator=" OR ">
        (name = #{condition.name} AND age = #{condition.age})
    </foreach>
</select>

<!-- 动态表名查询(注意SQL注入风险) -->
<select id="selectFromMultipleTables" resultType="Map">
    <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
        SELECT '${tableName}' as table_name, COUNT(*) as count FROM ${tableName}
    </foreach>
</select>

<!-- 批量插入VALUES -->
<insert id="batchInsert" parameterType="list">
    INSERT INTO user (name, age, email, create_time)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email}, NOW())
    </foreach>
</insert>

5. set标签 - 动态更新

<!-- 动态更新字段 -->
<update id="updateSelective">
    UPDATE user
    <set>
        <if test="name != null and name != ''">
            name = #{name},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="email != null and email != ''">
            email = #{email},
        </if>
        <if test="phone != null and phone != ''">
            phone = #{phone},
        </if>
        <if test="status != null">
            status = #{status},
        </if>
        update_time = NOW()  <!-- 总是更新修改时间 -->
    </set>
    WHERE id = #{id}
</update>

<!-- 批量更新不同字段 -->
<update id="batchUpdateSelective">
    <foreach collection="users" item="user" separator=";">
        UPDATE user
        <set>
            <if test="user.name != null and user.name != ''">
                name = #{user.name},
            </if>
            <if test="user.age != null">
                age = #{user.age},
            </if>
            <if test="user.email != null and user.email != ''">
                email = #{user.email},
            </if>
            update_time = NOW()
        </set>
        WHERE id = #{user.id}
    </foreach>
</update>

6. trim标签 - 自定义格式化

<!-- 自定义前缀和后缀处理 -->
<select id="selectByConditionWithTrim" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="name != null and name != ''">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
        <if test="status != null">
            OR status = #{status}
        </if>
    </trim>
</select>

<!-- 复杂的trim应用 -->
<insert id="insertSelective">
    INSERT INTO user
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="name != null">name,</if>
        <if test="age != null">age,</if>
        <if test="email != null">email,</if>
        <if test="phone != null">phone,</if>
        create_time,
    </trim>
    <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
        <if test="name != null">#{name},</if>
        <if test="age != null">#{age},</if>
        <if test="email != null">#{email},</if>
        <if test="phone != null">#{phone},</if>
        NOW(),
    </trim>
</insert>

SQL片段复用

面试加分项:

<!-- 定义可复用的SQL片段 -->
<sql id="userColumns">
    id, name, age, email, phone, status, create_time, update_time
</sql>

<sql id="userConditions">
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="ageMin != null">
            AND age >= #{ageMin}
        </if>
        <if test="ageMax != null">
            AND age <= #{ageMax}
        </if>
        <if test="statuses != null and statuses.size() > 0">
            AND status IN
            <foreach collection="statuses" item="status" open="(" separator="," close=")">
                #{status}
            </foreach>
        </if>
        <if test="createTimeStart != null">
            AND create_time >= #{createTimeStart}
        </if>
        <if test="createTimeEnd != null">
            AND create_time <= #{createTimeEnd}
        </if>
    </where>
</sql>

<!-- 使用SQL片段 -->
<select id="selectByComplexCondition" resultType="User">
    SELECT <include refid="userColumns"/>
    FROM user
    <include refid="userConditions"/>
    <if test="orderBy != null and orderBy != ''">
        ORDER BY ${orderBy}
    </if>
    <if test="limit != null and limit > 0">
        LIMIT #{offset}, #{limit}
    </if>
</select>

<select id="countByComplexCondition" resultType="int">
    SELECT COUNT(*)
    FROM user
    <include refid="userConditions"/>
</select>

<!-- 带参数的SQL片段 -->
<sql id="paginationSql">
    <if test="pageSize != null and pageSize > 0">
        LIMIT #{offset}, #{pageSize}
    </if>
</sql>

<sql id="orderBySql">
    <choose>
        <when test="orderBy != null and orderBy != ''">
            ORDER BY ${orderBy}
        </when>
        <otherwise>
            ORDER BY create_time DESC
        </otherwise>
    </choose>
</sql>

⚡ 批量操作优化

批量插入优化策略

面试重点:

面试官:"如何优化MyBatis的批量插入性能?有哪些方案?"

方案对比分析:

@Service
public class BatchInsertService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private SqlSessionFactory sqlSessionFactory;
    
    // 方案1:单条SQL批量插入(推荐)
    @Transactional
    public void batchInsertOptimal(List<User> users) {
        if (users == null || users.isEmpty()) {
            return;
        }
        
        // 分批处理,避免SQL过长
        int batchSize = 1000;
        for (int i = 0; i < users.size(); i += batchSize) {
            int endIndex = Math.min(i + batchSize, users.size());
            List<User> batch = users.subList(i, endIndex);
            
            // 单条SQL插入多行数据
            userMapper.batchInsert(batch);
        }
    }
    
    // 方案2:使用ExecutorType.BATCH
    @Transactional
    public void batchInsertWithBatchExecutor(List<User> users) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            
            for (int i = 0; i < users.size(); i++) {
                mapper.insert(users.get(i));
                
                // 每1000条提交一次
                if (i % 1000 == 0) {
                    sqlSession.flushStatements();
                }
            }
            
            sqlSession.flushStatements();
            sqlSession.commit();
        }
    }
    
    // 方案3:多条SQL批量插入(不推荐)
    @Transactional
    public void batchInsertMultipleSQL(List<User> users) {
        // 会产生多条INSERT语句,性能较差
        userMapper.batchInsertMultiple(users);
    }
    
    // 性能测试对比
    public void performanceTest() {
        List<User> users = generateTestUsers(10000);
        
        // 测试方案1
        long start1 = System.currentTimeMillis();
        batchInsertOptimal(users);
        long time1 = System.currentTimeMillis() - start1;
        System.out.println("方案1耗时: " + time1 + "ms");
        
        // 测试方案2
        long start2 = System.currentTimeMillis();
        batchInsertWithBatchExecutor(users);
        long time2 = System.currentTimeMillis() - start2;
        System.out.println("方案2耗时: " + time2 + "ms");
        
        // 结果:方案1 < 方案2 < 方案3
    }
}

批量插入XML配置:

<!-- 方案1:推荐的批量插入 -->
<insert id="batchInsert" parameterType="list">
    INSERT INTO user (name, age, email, create_time)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email}, NOW())
    </foreach>
</insert>

<!-- 方案2:支持返回主键的批量插入 -->
<insert id="batchInsertWithKeys" parameterType="list" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (name, age, email, create_time)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age}, #{user.email}, NOW())
    </foreach>
</insert>

<!-- 方案3:ON DUPLICATE KEY UPDATE -->
<insert id="batchInsertOrUpdate" parameterType="list">
    INSERT INTO user (id, name, age, email, create_time)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.id}, #{user.name}, #{user.age}, #{user.email}, NOW())
    </foreach>
    ON DUPLICATE KEY UPDATE
        name = VALUES(name),
        age = VALUES(age),
        email = VALUES(email),
        update_time = NOW()
</insert>

批量更新优化

批量更新策略:

@Service
public class BatchUpdateService {
    
    // 方案1:CASE WHEN批量更新(推荐)
    public void batchUpdateOptimal(List<User> users) {
        if (users == null || users.isEmpty()) {
            return;
        }
        
        userMapper.batchUpdateByCase(users);
    }
    
    // 方案2:分批执行单条更新
    @Transactional
    public void batchUpdateSeparate(List<User> users) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            
            for (User user : users) {
                mapper.updateById(user);
            }
            
            sqlSession.flushStatements();
            sqlSession.commit();
        }
    }
    
    // 方案3:临时表方案(大数据量)
    @Transactional
    public void batchUpdateWithTempTable(List<User> users) {
        // 1. 创建临时表
        userMapper.createTempTable();
        
        // 2. 批量插入临时表
        userMapper.batchInsertToTempTable(users);
        
        // 3. 关联更新
        userMapper.updateFromTempTable();
        
        // 4. 删除临时表
        userMapper.dropTempTable();
    }
}

批量更新XML实现:

<!-- 方案1:CASE WHEN批量更新 -->
<update id="batchUpdateByCase" parameterType="list">
    UPDATE user
    SET
        name = CASE id
            <foreach collection="list" item="user">
                WHEN #{user.id} THEN #{user.name}
            </foreach>
        END,
        age = CASE id
            <foreach collection="list" item="user">
                WHEN #{user.id} THEN #{user.age}
            </foreach>
        END,
        email = CASE id
            <foreach collection="list" item="user">
                WHEN #{user.id} THEN #{user.email}
            </foreach>
        END,
        update_time = NOW()
    WHERE id IN
    <foreach collection="list" item="user" open="(" separator="," close=")">
        #{user.id}
    </foreach>
</update>

<!-- 方案2:临时表方案 -->
<update id="createTempTable">
    CREATE TEMPORARY TABLE temp_user_update (
        id BIGINT PRIMARY KEY,
        name VARCHAR(100),
        age INT,
        email VARCHAR(100)
    )
</update>

<insert id="batchInsertToTempTable" parameterType="list">
    INSERT INTO temp_user_update (id, name, age, email)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.id}, #{user.name}, #{user.age}, #{user.email})
    </foreach>
</insert>

<update id="updateFromTempTable">
    UPDATE user u
    INNER JOIN temp_user_update t ON u.id = t.id
    SET u.name = t.name,
        u.age = t.age,
        u.email = t.email,
        u.update_time = NOW()
</update>

<update id="dropTempTable">
    DROP TEMPORARY TABLE temp_user_update
</update>

🚀 SQL性能优化

分页查询优化

面试热点:

面试官:"大数据量分页查询如何优化?深分页问题怎么解决?"

分页优化方案:

@Service
public class PaginationOptimizationService {
    
    // 方案1:传统LIMIT分页(小数据量)
    public PageResult<User> selectByPage(int pageNum, int pageSize, UserQuery query) {
        int offset = (pageNum - 1) * pageSize;
        
        List<User> users = userMapper.selectByPageLimit(query, offset, pageSize);
        int total = userMapper.countByQuery(query);
        
        return new PageResult<>(users, total, pageNum, pageSize);
    }
    
    // 方案2:游标分页(推荐)
    public PageResult<User> selectByPageCursor(Long lastId, int pageSize, UserQuery query) {
        List<User> users = userMapper.selectByPageCursor(query, lastId, pageSize);
        
        // 不需要查询总数,提高性能
        return new PageResult<>(users, null, null, pageSize);
    }
    
    // 方案3:延迟关联分页(大数据量)
    public PageResult<User> selectByPageDeferred(int pageNum, int pageSize, UserQuery query) {
        int offset = (pageNum - 1) * pageSize;
        
        // 先查询ID
        List<Long> ids = userMapper.selectIdsByPage(query, offset, pageSize);
        
        if (ids.isEmpty()) {
            return new PageResult<>(Collections.emptyList(), 0, pageNum, pageSize);
        }
        
        // 再根据ID查询详细信息
        List<User> users = userMapper.selectByIds(ids);
        int total = userMapper.countByQuery(query);
        
        return new PageResult<>(users, total, pageNum, pageSize);
    }
}

分页查询XML实现:

<!-- 方案1:传统LIMIT分页 -->
<select id="selectByPageLimit" resultType="User">
    SELECT * FROM user
    <include refid="userConditions"/>
    ORDER BY create_time DESC
    LIMIT #{offset}, #{pageSize}
</select>

<!-- 方案2:游标分页 -->
<select id="selectByPageCursor" resultType="User">
    SELECT * FROM user
    <where>
        <if test="lastId != null">
            AND id > #{lastId}
        </if>
        <include refid="userConditions"/>
    </where>
    ORDER BY id ASC
    LIMIT #{pageSize}
</select>

<!-- 方案3:延迟关联分页 -->
<select id="selectIdsByPage" resultType="Long">
    SELECT id FROM user
    <include refid="userConditions"/>
    ORDER BY create_time DESC
    LIMIT #{offset}, #{pageSize}
</select>

<select id="selectByIds" resultType="User">
    SELECT * FROM user
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
    ORDER BY FIELD(id, 
        <foreach collection="ids" item="id" separator=",">
            #{id}
        </foreach>
    )
</select>

查询条件优化

索引友好的查询:

<!-- 优化前:不走索引 -->
<select id="badQuery" resultType="User">
    SELECT * FROM user
    WHERE YEAR(create_time) = #{year}  <!-- 函数导致不走索引 -->
    AND name LIKE '%${keyword}%'       <!-- 前缀模糊查询不走索引 -->
    AND age + 1 > #{minAge}           <!-- 计算导致不走索引 -->
</select>

<!-- 优化后:索引友好 -->
<select id="goodQuery" resultType="User">
    SELECT * FROM user
    WHERE create_time >= #{yearStart}     <!-- 范围查询走索引 -->
    AND create_time < #{yearEnd}
    AND name LIKE CONCAT(#{keyword}, '%') <!-- 后缀模糊查询走索引 -->
    AND age > #{minAge}                   <!-- 直接比较走索引 -->
</select>

<!-- 复合索引优化 -->
<select id="optimizedComplexQuery" resultType="User">
    SELECT * FROM user
    WHERE status = #{status}        <!-- 第一个索引字段 -->
    AND create_time >= #{startTime} <!-- 第二个索引字段 -->
    AND create_time <= #{endTime}   <!-- 范围查询 -->
    <if test="name != null and name != ''">
        AND name = #{name}          <!-- 精确匹配 -->
    </if>
    ORDER BY create_time DESC       <!-- 利用索引排序 -->
</select>

查询结果优化:

<!-- 只查询需要的字段 -->
<select id="selectUserSummary" resultType="UserSummary">
    SELECT id, name, status FROM user  <!-- 避免SELECT * -->
    <include refid="userConditions"/>
</select>

<!-- 使用EXISTS替代IN -->
<select id="selectUsersWithOrders" resultType="User">
    SELECT * FROM user u
    WHERE EXISTS (
        SELECT 1 FROM orders o 
        WHERE o.user_id = u.id 
        AND o.status = 'PAID'
    )
</select>

<!-- 合理使用UNION -->
<select id="selectActiveUsers" resultType="User">
    SELECT * FROM user WHERE status = 'ACTIVE'
    UNION ALL
    SELECT * FROM user WHERE vip_level > 0 AND vip_expire_time > NOW()
</select>

💡 面试重点总结

高频面试题

1. 动态SQL标签使用

必答要点:
- if标签:条件判断,注意null和空字符串
- where标签:自动处理AND/OR前缀
- foreach标签:循环处理,支持IN查询和批量操作
- choose/when/otherwise:多分支选择
- set标签:动态更新字段
- trim标签:自定义前缀后缀处理

2. 批量操作优化

必答要点:
- 单条SQL批量插入性能最优
- ExecutorType.BATCH适合大量单条操作
- CASE WHEN实现批量更新
- 分批处理避免SQL过长
- 临时表方案适合超大数据量

3. 分页查询优化

必答要点:
- 深分页使用游标分页
- 延迟关联减少数据传输
- 避免COUNT(*)查询
- 利用覆盖索引优化
- 缓存总数减少重复计算

答题技巧

技巧1:结合性能数据

"通过批量插入优化,我们的数据导入性能提升了80%..."

技巧2:对比不同方案

"相比逐条插入,批量插入减少了网络交互次数..."

技巧3:提及实际应用

"在处理百万级数据导入时,我们采用了分批+事务的策略..."

本节核心要点:

  • ✅ 动态SQL六大标签的使用场景和最佳实践
  • ✅ SQL片段复用提高代码维护性
  • ✅ 批量插入和更新的多种优化方案
  • ✅ 分页查询的性能优化策略
  • ✅ 索引友好的查询条件编写

下一节预告: MyBatis-Plus扩展功能 - 代码生成器、条件构造器、分页插件

#java秋招面试#
Java面试圣经 文章被收录于专栏

Java面试圣经

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务