前言

原因:

受限jdbc规范、数据库底层实现,参数数量最多为 65,535 个

解决方案:

分批处理:

将大量数据分成多个较小的批次进行处理——部分场景无法分批,例如语句需做分页

使用临时表:

将数据先插入到一个临时表中,然后通过 SQL 查询或存储过程处理这些数据。—— 考虑过会话级临时表,验证时发现数据插入很慢,影响整体查询效率

直接生成 SQL:

将需要使用的参数,直接拼接到sql语句中,即下方内容。
*

【fix】 PreparedStatement can have at most 65,535 parameters 的问题


*

语句由 t1.assets_id in (:assetsIdList) 变为 EXISTS (SELECT 1 FROM (VALUES (1), (2), (3), …, (10000)) AS ids(id) WHERE t.id = ids.id)

实现

sql展示

数据构造临时表sql语法

-- 将传入的数据,例如 'A350550343'、'A350550288'、'A3505502881‘ 转为sql中直接可用的临时表数据 
---------- Oracle 语法 ----------                                                 
select assets_id from (                                                         
    SELECT 'A350550343' as assets_id from  dual                                 
    UNION                                                                       
    SELECT 'A350550288' as assets_id from dual                                  
    UNION                                                                       
    SELECT 'A3505502881' as assets_id from dual                                 
 ) temp                                                                         
----------  mysql/pg/cmdb 语法 ----------                                         
select assets_id                                                                
from (                                                                          
         -- 需去重时 union,不去重时 UNION ALL                                      
         SELECT 'A350550343' AS assets_id                                       
         UNION                                                                  
         SELECT 'A350550288' AS assets_id                                       
         UNION                                                                  
         SELECT 'E350000858' AS assets_id                                       
 )  temp                                                                        
----------  pg/cmdb 语法 ----------                                               
select assets_id                                                                
from (values ('A350550343'),('A350550288'),('A3505502881'))  temp       

java

DAO层使用

使用SqlBuildUtil.sqlBuildExistsSyntax,将参数转为临时表关联查询

        if(CollectionUtils.isNotEmpty(assetsReqBean.getAssetsIdList())){
            // oracle in 1000 超1000会报错,由于asssetsId in 在最后的查询语句且带有分页,没法使用拆分多次查询,先使用 or 支撑,
            // 后续若有性能问题,考虑改为会话级临时表
            if( SqlBuildUtil.isOracleOutInNum(dbType,assetsReqBean.getAssetsIdList())) {
                SqlBuildUtil.splitIdConcatSql(assetsReqBean.getAssetsIdList(), splitAssetsIdList, sql ,"t1","assets_id","assetsIdList");
            } else if( SqlBuildUtil.isOutInNumLimit(dbType,assetsReqBean.getAssetsIdList()) ) {
                // PreparedStatement can have at most 65,535 parameters
                outJdbcLimit = Boolean.TRUE;
                sqlForOutJdbcLimit.append("(exists 语句过滤id数:"+assetsReqBean.getAssetsIdList().size() +")");
                SqlBuildUtil.sqlBuildExistsSyntax(dbType,"and",assetsReqBean.getAssetsIdList(),"temp",sql,"t1","assets_id");
            } else {
                sql.append(" and t1.assets_id in (:assetsIdList) ");
            }
        } else {
            sql.append(" and 1 <> 1 ");
        }

常量类

public class AssetsConst {
    public static final int MAX_IN_NUM = 800;//oracle in包含的最大条件数
    public static final int MAX_IN_NUM_JDBC = 65535;//jdbc in包含的最大条件数
    public static final int FAST_IN_NUM_CMDB = 800;//磐维使用in 比exists 快的临界值
 }

SqlBuildUtil

/**
 * sql语句构建工具类
 * @author yangqingwang
 * @create 2024/12/10 10:19
 */
public class SqlBuildUtil {
    private static final Logger log = LoggerFactory.getLogger(SqlBuildUtil.class);

    private SqlBuildUtil(){}

    /**
     * 是否为Oracle 超出 in的最大数量限制
     * @param dbType
     * @param originIdList
     * @return
     */
    public static boolean isOracleOutInNum(DbTypes dbType,List<String> originIdList){
        return Objects.nonNull(dbType) && CollectionUtils.isNotEmpty(originIdList)
                &&   dbType.equals(DbTypes.ORACLE) && (originIdList.size() > AssetsConst.MAX_IN_NUM);
    }

    /**
     * 是否超出 in的最大数量限制
     * @param dbType
     * @param originIdList
     * @return
     */
    public static boolean isOutInNumLimit(DbTypes dbType,List<String> originIdList){
        if (Objects.isNull(dbType) || CollectionUtils.isEmpty(originIdList)) {
            return false;
        } else if (dbType.equals(DbTypes.CHINAMOBILEDB)) {
            return originIdList.size() > AssetsConst.FAST_IN_NUM_CMDB;
        } else if (dbType.equals(DbTypes.MYSQL)) {
            // 未验证mysql,先使用最大值
            return originIdList.size() > AssetsConst.MAX_IN_NUM_JDBC;
        } else {
            return isOracleOutInNum(dbType,originIdList);
        }
    }

    /**
     * 是否超出 in的最大数量限制
     * @param dbType
     * @param originIdList
     * @param limitNum 指定限制数量
     * @return
     */
    public static boolean isOutInNumLimit(DbTypes dbType,List<String> originIdList,int limitNum){
        if (Objects.isNull(dbType) || CollectionUtils.isEmpty(originIdList)) {
            return false;
        } else if (dbType.equals(DbTypes.CHINAMOBILEDB) || dbType.equals(DbTypes.MYSQL)) {
            return originIdList.size() > limitNum;
        } else {
            return isOracleOutInNum(dbType,originIdList);
        }
    }

    /**
     * 拆分id,拼接sql
     * 对 Oracle in 超一千的问题处理
     * <p>语句由 t1.assets_id in (:assetsIdList0) 变为 t1.assets_id in (:assetsIdList0) or t1.assets_id in (:assetsIdList1) or t1.assets_id in (:assetsIdList2)</p>
     * @param originIdList 原始id列表
     * @param splitIdList 存放拆分后的id列表
     * @param sql 拼接的sql
     * @param tableName 条件表
     * @param fieldName 条件字段
     * @param listPrefix 参数列表前缀
     */
    public static   void splitIdConcatSql(List<String> originIdList, List<List<String>> splitIdList, StringBuilder sql,
                                          String tableName, String fieldName, String listPrefix) {
        String realAssetsIdSQL = "";
        int size = originIdList.size();
        int offSet = 0;
        List<String> assetsIdList = new ArrayList<>();
        int splitListIndex = 0;
        while (offSet < size){
            List<String> idList = originIdList.subList(offSet, offSet + AssetsConst.MAX_IN_NUM > size ? size : offSet + AssetsConst.MAX_IN_NUM);
            splitIdList.add(idList);
            assetsIdList.add(tableName + "." + fieldName + " in (:" + listPrefix +splitListIndex + ")");
            offSet += AssetsConst.MAX_IN_NUM;
            splitListIndex++;
        }
        realAssetsIdSQL = " and (" + StringUtils.join(assetsIdList," or ") + ") ";
        sql.append(realAssetsIdSQL);
    }

    /**
     * 根据条件数据构建exists语法内容
     * <p>用于【fix】 PreparedStatement can have at most 65,535 parameters 的问题 </p>
     * <p>语句由 t1.assets_id in (:assetsIdList) 变为  EXISTS (SELECT 1 FROM (VALUES (1), (2), (3), ..., (10000)) AS ids(id) WHERE t.id = ids.id)</p>
     * @param dbType 当前配置库类型,决定具体语法选择
     * @param whereOpt 条件操作符 or | and
     * @param originIdList 原始id列表
     * @param tempTableName 临时表名
     * @param sql 拼接的sql
     * @param tableName 条件表
     * @param fieldName 条件字段
     */
    public static   void sqlBuildExistsSyntax(DbTypes dbType, String whereOpt, List<String> originIdList, String tempTableName,
                                              StringBuilder sql, String tableName, String fieldName) {
        // PreparedStatement can have at most 65,535 parameters
        StopWatch stopWatch = new StopWatch("sqlBuildExistsSyntax-");
        AssetsUtil.startAndStopPrevious(stopWatch, "sqlBuildTempTableByData");
        if (Objects.nonNull(dbType)) {
            sql.append(" " + whereOpt);
            sql.append(" exists (");
            SqlBuildUtil.sqlBuildTempTableByData(dbType,originIdList,sql,tempTableName,fieldName);
            sql.append("     where "+tempTableName+"."+fieldName+" = "+ tableName+"."+fieldName);
            sql.append(" )");
        } else {
            throw new PubMetaInfoException("sqlBuildExistsSyntax,数据库类型不能为空", PubMetaInfoExceptionCode.BUS_HANDLE_ERROR_DESC);
        }
        log.info("\n" + AssetsUtil.stopWatchPrettyPrint(stopWatch));
    }

    /**
     * 根据数据构造sql临时表
     *
     * <p> 将传入的数据,例如 'A350550343'、'A350550288'、'A3505502881‘ 转为sql中直接可用的临时表数据 </p>
     * <p> ---------- Oracle 语法 ----------                                                                 </p>
     * <p> select assets_id from (                                                          </p>
     * <p>     SELECT 'A350550343' as assets_id from  dual                                  </p>
     * <p>     UNION                                                                        </p>
     * <p>     SELECT 'A350550288' as assets_id from dual                                   </p>
     * <p>     UNION                                                                        </p>
     * <p>     SELECT 'A3505502881' as assets_id from dual                                  </p>
     * <p>  ) temp                                                                          </p>
     * <p> ----------  mysql/pg/cmdb 语法 ----------                                                             </p>
     * <p> select assets_id                                                                 </p>
     * <p> from (                                                                           </p>
     * <p>          -- 需去重时 union,不去重时 UNION ALL                                       </p>
     * <p>          SELECT 'A350550343' AS assets_id                                        </p>
     * <p>          UNION                                                                   </p>
     * <p>          SELECT 'A350550288' AS assets_id                                        </p>
     * <p>          UNION                                                                   </p>
     * <p>          SELECT 'E350000858' AS assets_id                                        </p>
     * <p>  )  temp                                                                         </p>
     * <p> ----------  pg/cmdb 语法 ----------                                                                </p>
     * <p> select assets_id                                                                 </p>
     * <p> from (values ('A350550343'),('A350550288'),('A3505502881'))  temp                </p>
     * @param dbType 当前配置库类型,决定具体语法选择
     * @param originIdList 原始id列表
     * @param sql 拼接的sql
     * @param tempTableName 临时表名
     * @param fieldName 条件表字段
     */
    public static   void sqlBuildTempTableByData(DbTypes dbType,List<String> originIdList, StringBuilder sql,
                                                 String tempTableName,String fieldName) {
        if ( dbType.equals(DbTypes.POSTGRESQL) || dbType.equals(DbTypes.CHINAMOBILEDB) ) {
            // 使用  pg/cmdb 语法
            sql.append(" select " + fieldName+ "  from (values ");
            sql.append(originIdList.stream()
                    .map(id -> "('" + id + "')")
                    .collect(Collectors.joining(",")));
            sql.append(")  "+tempTableName+"("+fieldName+")");
        } else if (dbType.equals(DbTypes.MYSQL)  ) {
            // 使用 mysql/pg/cmdb 语法
            sql.append("  select " + fieldName);
            sql.append("  from (");
            sql.append(originIdList.stream()
                    .map(id -> " SELECT '" + id + "' AS " + fieldName)
                    .collect(Collectors.joining(" UNION ")));
            sql.append("  ) " + tempTableName);
        }  else if (dbType.equals(DbTypes.ORACLE)  ) {
            // 使用 Oracle 语法
            sql.append("  select " + fieldName);
            sql.append("  from (");
            sql.append(originIdList.stream()
                    .map(id -> " SELECT '" + id + "' AS " + fieldName + " from  dual ")
                    .collect(Collectors.joining(" UNION ")));
            sql.append("  ) " + tempTableName);
        } else {
            throw new PubMetaInfoException("sqlBuildTempTableByData不支持的数据库类型:" + dbType,PubMetaInfoExceptionCode.BUS_HANDLE_ERROR_DESC);
        }
    }
}
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐