.NET 批量插入数据,先查后插,性能优化

本文分享了批量插入数据到数据库并进行查重优化的经验,包括使用SqlBulkCopy提升插入效率,通过创建索引和存储过程加速查重过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    本文写的背景是一次批量Insert 5000~10000条记录到数据库表,每一条数据插入前要去查是否与原有数据重复,重复则不插入。

    本文就当记录这个过程。过程挺痛苦的。第一次使用索引、第一次使用存储过程,所以记录的比较啰嗦。见谅。

    第一部分,先不谈“插入前查是否重复”问题。就拿批量插入来说,使用最原本的一次insert一条效率太低,即使使用存储过程效率也没提高多少。

    后来看到这篇文章【http://www.cnblogs.com/wlb/archive/2010/03/02/1676136.html】,尝试使用SQlBlukCopy来批量insert数据。

    ·代码创建DataTable:

private DataTable getTagetDataTabel()
        {
            DataTable dt = new DataTable("temp_dt");
            dt.Columns.Add("jlh", typeof(string));
            dt.Columns.Add("rq", typeof(string));            
            dt.Columns.Add("file_name", typeof(string));
            dt.Columns.Add("deal_result", typeof(string));
            dt.Columns.Add("lock", typeof(string));
            dt.Columns.Add("file_id", typeof(int));
            dt.Columns.Add("indexx", typeof(string));//新增索引字段:抽取记录关键信息作为索引
            dt.Columns.Add("sql", typeof(string));
            return dt;
        }

    ·给创建的DataTable添加行数据:

for (int i = 1; i < length; i++)
            { 
                //数据来源操作
                dt.Rows.Add(new object[]
                { 
                    jlh,
                    rq,                    
                    "0",
                    "0",
                    file_id,
                    indexx,
                    ab.ToString()
                });
                totalCount++;
            }
    

    ·为了后面的数据筛选效率,给DataTable设置主键非常重要!:

//延迟至此处设置主键的原因是:装好数据再设主键,效率会比较高
            dt.PrimaryKey = new DataColumn[] { dt.Columns["indexx"] };
    ·设置映射关系,一次批量写进数据库
//2.设置映射,并执行(这种方式效率很高,4000条平均300毫秒)
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
                {
                    try
                    {
                        bulkCopy.DestinationTableName = "dt_citizencard_trade_data";//设置目标表名
                        bulkCopy.ColumnMappings.Add("jlh", "jlh");//设置表字段映射
                        bulkCopy.ColumnMappings.Add("rq", "rq");                        
                        bulkCopy.ColumnMappings.Add("deal_result", "deal_result");
                        bulkCopy.ColumnMappings.Add("lock", "lock");
                        bulkCopy.ColumnMappings.Add("file_id", "file_id");
                        bulkCopy.ColumnMappings.Add("indexx", "indexx");
                        connection.Open();
                        bulkCopy.WriteToServer(dt);
                        return dt.Rows.Count;
                    }
                    catch (System.Data.SqlClient.SqlException e)
                    {
                        connection.Close();
                        throw e;
                    }
                }
            }

    第一部分完结。重要讲的就是代码创建DataTable和使用SqlBlukCopy

    第二部分:先查后插的优化

    当库里数据量较少的时候,每一条记录都先查后插费时一般般(4000条记录先查后插用了4秒); 当库里数据量达到10W时,直接先查后插就像噩梦一样了。

    使用存储过程去查,也没什么效果。后来索引出场了!因为表数据除了不带信息的自增ID外,没有能唯一标识记录信息的字段了。故改变表格式,新增一个名为indexx的字段,插入前按数据规律抽取每条记录的信息组成唯一的记录标识,这样就不用全字段去COUNT(*)去判断是否已有重复记录,只查这个字段就行了。

    然后在这个indexx字段上建立索引,这一步的贡献的效率非常高!

    现在的情况是:索引+存储过程。

    ·创建索引的语法是:

CREATE [ VIRTUAL ] [ UNIQUE ] [ CLUSTERED ] INDEX index-name
ON [ owner.]table-name
( column-name [ ASC | DESC ], ...
    | function-name ( argument [ ,... ] ) AS column-name )
[ { IN | ON } dbspace-name ]
    其中 CLUSTERED  和NONCLUSTERED 表示聚集索引和非聚集索引,详细要另行百度。

    ·我创建的索引:

CREATE CLUSTERED INDEX my_indexx ON dt_citizencard_trade_data (indexx)
    ·我建的存储过程:

CREATE PROCEDURE [dbo].[queryIndexx]
@indexx VARCHAR(29),--这个是参数
--@reval01 VARCHAR(30) output, -- 这个是输出
--@reval02 VARCHAR(30) output --输出可以有多个
AS
DECLARE @exits int
SELECT @exits=COUNT(*) FROM dt_citizencard_trade_data
WHERE indexx=@indexx AND deal_result='0'
IF(@exits<1)
BEGIN
RETURN 0 --这个是返回值
END
ELSE
BEGIN
RETURN 1 <pre name="code" class="sql" style="font-size: 14px; line-height: 26px;">--这个是返回值
ENDGO

 

    存储过程可以带参数、有输出,有返回值。(上面两个注释仅仅是为了说明,我的代码用不到输出)

    ·顺便把C#调用存储过程也写上(创建参数、设置参数属性,传参执行存储过程,获取返回值/输出):

//创建参数
                SqlParameter[] paramss = 
                {
                    new SqlParameter("@indexx", SqlDbType.VarChar, 29),
                    new SqlParameter("@re_count", SqlDbType.Int),//新增一个作为返回值
                    //new SqlParameter("@reval01", SqlDbType.VarChar, 30),//作为输出
                    //new SqlParameter("@reval02", SqlDbType.VarChar, 30)//作为输出
                };
                //设置参数属性
                paramss[0].Value = dt.Rows[i]["indexx"];
                paramss[1].Direction = ParameterDirection.ReturnValue;//设置为返回值
                //paramss[n].Direction = ParameterDirection.Output;//设置为输出
                //paramss[m].Direction = ParameterDirection.Output;//设置为输出
    ·调用存储过程、获取返回值和输出值:

private bool queryProcedure(string procedueNname, SqlParameter[] parameters)
        {
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                using (SqlCommand cmd = new SqlCommand())
                {
                    try
                    {
                        cmd.Connection = connection;
                        cmd.CommandType = CommandType.StoredProcedure;
                        cmd.CommandText = procedueNname;
                        foreach (IDataParameter p in parameters)
                        {
                            cmd.Parameters.Add(p);
                        }
                        connection.Open();
                        cmd.ExecuteNonQuery();//这里调用返回的是影响行数
                        if (int.Parse(parameters[1].Value.ToString()) == 1)//这里获取的是return返回值
                        {
                            return true;
                        }
                        return false;
                        //string output_n = parameters[n].Value.ToString();//获取output输出值(示例代码)
                        //string output_m = parameters[m].Value.ToString();//获取output输出值(示例代码)
                    }
                    catch (System.Data.SqlClient.SqlException e)
                    {
                        connection.Close();
                        throw e;
                    }
                }
            }
        }
    以上是存储过程的主要用法了( 创建参数、设置参数属性,传参执行存储过程,获取返回值/输出)。


    ·关键的地方到了。先查后插,思路是把所有要插的记录读到上面内存里创建的Datatable里,然后遍历这个Datatable获取每一条待插记录的indexx标识信息,调用上面准备好的存储过程,如果库里有这条记录了,则从Datatable里删除,这样筛选之后剩下就是一个完全不合库里记录重复的DataTable了,就可以使用SqlBlukCopy一次写进数据库。

    这里有三个陷阱,发生在对Datatable动态删除的过程。看代码应该很容易明白吧(三个旧版的查重复代码,还没用到存储过程):

//1.先查数据库,筛掉dt里与库里重复的//这样会报错:集合已修改;枚举操作可能无法执行
            //foreach (DataRow dr in dt.Rows)
            //{
            //    if (checkSql(dr["sql"].ToString()))
            //    {
            //        dt.Rows.Remove(dr);
            //    }
            //}

            //2.先查数据库,筛掉dt里与库里重复的//这样是不行的,删了记录saveDt的行数会减少,出现边界溢出
            //int length = dt.Rows.Count;
            //DataTable saveDt = dt.Copy();//用Clone是不行的,只克隆了框架
            //for (int i = 0; i < length; i++)
            //{
            //    if (checkSql(dt.Rows[i]["sql"].ToString()))
            //    {
            //        saveDt.Rows.RemoveAt(i);//删了记录saveDt的行数会减少,出现边界溢出
            //    }
            //}

            //3.先查数据库,筛掉dt里与库里重复的
            //int length = dt.Rows.Count;
            //DataTable saveDt = dt.Copy();//Clone只是复制框架;Copy是复制框架和数据
            //HttpContext.Current.Response.Write("dt.Rows.Count:" + dt.Rows.Count);
            //HttpContext.Current.Response.Write("saveDt.Rows.Count:" + saveDt.Rows.Count);
            //HttpContext.Current.Response.End();
            //foreach (DataRow dr in dt.Rows)
            //{
            //    if (checkSql(dr["sql"].ToString()))
            //    {
            //        saveDt.Rows.Remove(dr);//这样是不现实的,因为在两个dataTable中dr都不是同一个对象了!
            //    }
            //}
    ·最后这样子实现(重点是DataTable动态删除记录部分,这里新版代码改用存储过程查重复):

//33.先查数据库,筛掉dt里与库里重复的//使用存储过程+索引
            int length = dt.Rows.Count;
            DataTable saveDt = dt.Copy();//Clone只是复制框架;Copy是复制框架和数据
            for (int i = 0; i < length; i++)
            {
                //创建参数
                SqlParameter[] paramss = 
                {
                    new SqlParameter("@indexx", SqlDbType.VarChar, 29),
                    new SqlParameter("@re_count", SqlDbType.Int),//新增一个作为返回值
                    //new SqlParameter("@reval01", SqlDbType.VarChar, 30),//作为输出(示例代码)
                    //new SqlParameter("@reval02", SqlDbType.VarChar, 30)//作为输出(示例代码)
                };
                //设置参数属性
                paramss[0].Value = dt.Rows[i]["indexx"];
                paramss[1].Direction = ParameterDirection.ReturnValue;//设置为返回值
                //paramss[n].Direction = ParameterDirection.ReturnValue;//设置为输出(示例代码)
                //paramss[m].Direction = ParameterDirection.Output;//设置为输出(示例代码)

                if (queryProcedure("queryIndexx", paramss))
                {
                    DataRow[] drs = saveDt.Select("indexx='" + dt.Rows[i]["indexx"] + "'");//这部分代码效率太低了!!给dt设置主键/改成Find()方法,
                    foreach (DataRow drr in drs)
                    {
                        saveDt.Rows.Remove(drr);
                    }
                }
            }

    好了,可以动态删除DataTable里的记录了。现在,遍历删除的性能瓶颈不是查数据库了(因为使用了索引+存储过程),而是 Datatable.Select()方法!感谢这篇文章【http://www.cnblogs.com/yangecnu/archive/2013/05/20/3087896.html

    要点1 是用Datatable.Row.Find()方法而不是Datatable.Select()方法,这样要看个人需求。

    要点2 是给装载好数据的Datatable设置主键为indexx!这样效率真的一下实现社会主义啊!

    要点3 是DataTable先装数据后建主键,这是特性。原作者说:“在填充数据之前创建主键,然后填充数据,比填充数据完之后创建主键消耗的时间要多。这是由于,创建主键后,再向其中添加数据,会导致需要重新生成索引,这和数据库中,不适合在频繁变动的字段上创建主键的原理是一样的”。很好理解。

    所以本文开始那里有一段代码是这样的:(就是为了这个):

//延迟至此处设置主键的原因是:装好数据再设主键,效率会比较高
            dt.PrimaryKey = new DataColumn[] { dt.Columns["indexx"] };


    好像讲完了。

    主要思路是把所有内待插入数据读进内存的Datatable,然后遍历这个DataTable查重,筛选完后SqlBlukCopy一次写入数据库。

    整体看来效率瓶颈有三个地方:插入、查重、Datatable.Select()

    使用SqlBlukCopy可以解决批量插入效率问题。使用索引+存储过程提升查重效率。给内存里的DataTable建主键解决DataTable.Select()的效率。

    第一次这么认真写。今后得坚持才行。





  

<think>好的,我现在需要帮助用户解决在Java中如何批量插入具有父子关系的省市数据的问题,特别是当子记录需要维护父记录的ID,而数据库的ID是自动生成的情况下。我需要仔细思考这个问题的各个方面,确保给出的解决方案是正确且可行的。 首先,用户提到省市具有父子关系,这意味着数据中存在层级结构,比如省是父节点,市是子节点。子节点需要保存父节点的ID,而数据库中的ID是自动生成的,比如使用自增主键或者序列。在这种情况下,批量插入数据时,子节点的父ID依赖于父节点插入后生成的ID,这带来了挑战,因为父节点的ID在插入是未知的。 接下来,我需要考虑如何解决这种依赖关系。通常,数据库的自动生成ID是在插入操作之后才能获取的。因此,处理批量插入的关键在于先插入父节点,获取其生成的ID,然后再将这些ID分配给对应的子节点进行插入。 可能的步骤包括: 1. **插入父节点(省)并获取生成的ID**:首先需要将所有的省数据插入数据库中,并且立即获取数据库自动生成的ID。这里需要注意,如果使用JDBC,可能需要使用`Statement.RETURN_GENERATED_KEYS`来获取生成的主键。对于批量插入父节点,可能需要逐条插入或者使用支持返回生成键的批量操作方法。 2. **组织数据结构,建立父子关联**:在Java中,可能需要将省和市的数据结构设计成对象,比如`Province`和`City`类,其中`City`对象持有其所属的`Province`的ID。在插入省之后,将生成的ID设置到对应的`Province`对象中,然后为每个`City`对象设置对应的省ID。 3. **批量插入子节点(市)**:一旦所有的省都插入完成,并且它们的ID已经获取并设置到对应的市对象中,就可以进行市的批量插入。这里可以使用JDBC的批量操作(如`addBatch()`和`executeBatch()`)来提高效率。 然而,需要考虑的问题包括: - **事务管理**:确保整个插入过程在一个事务中,这样如果在插入过程中出现错误,可以回滚,保证数据的一致性。比如,省的插入和市的插入应该在一个事务里,避免部分插入导致的数据不一致。 - **批量插入父节点的效率**:如果父节点数量很大,逐条插入可能会影响性能。需要找到一种高效的方式批量插入父节点,并获取所有生成的主键。但大多数数据库在批量插入时可能不支持返回多个生成的主键,这可能需要逐条处理或者寻找其他方法。 - **对象与数据库记录的映射**:可能需要使用ORM框架如MyBatis或Hibernate来简化操作。例如,MyBatis的`@Options(useGeneratedKeys=true, keyProperty="id")`可以在插入后自动将生成的ID填充到对象中,这对于批量处理可能更高效。 接下来,具体实现的思路可能如下: 1. **数据准备**:将省和市的数据组织成列表,确保每个市对象可以关联到对应的省对象。例如,有一个`List<Province>`,每个`Province`对象有一个`List<City>`。 2. **插入省并获取ID**:遍历省的列表,逐个插入省记录,并获取每个插入后的生成的ID。这里可能需要使用JDBC的`PreparedStatement`,并在执行后通过`getGeneratedKeys()`获取ID,然后设置回省对象。 3. **关联市到省**:对于每个省,遍历其对应的市列表,将市的`provinceId`设置为省的ID。 4. **批量插入市**:收集所有市的数据,使用JDBC批量插入操作,一次性插入所有市记录,以提高效率。 如果使用MyBatis这样的ORM框架,可以更简化这个过程。例如,在MyBatis的Mapper接口中定义插入省的方法,并配置`useGeneratedKeys`来获取ID,然后对每个省插入后,立即插入其对应的市。此外,MyBatis还支持批量操作,可以通过`@InsertProvider`或者动态SQL来优化批量插入。 另外,需要考虑事务的处理。例如,在Spring框架中,可以使用`@Transactional`注解来管理事务,确保所有插入操作要么全部成功,要么全部回滚。 可能的代码结构示例: - **Province类**:包含id、name等字段。 - **City类**:包含id、name、provinceId等字段。 - **DAO或Mapper接口**:提供插入省和市的方法,其中插入省的方法需要返回生成的ID。 - **服务层**:处理业务逻辑,先插入所有省,然后处理市的数据,最后批量插入市。 在JDBC示例中,处理插入省并获取ID的步骤可能如下: ```java // 假设有一个省份列表 List<Province> provinces try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 开始事务 String insertProvinceSQL = "INSERT INTO province (name) VALUES (?)"; PreparedStatement pstmt = conn.prepareStatement(insertProvinceSQL, Statement.RETURN_GENERATED_KEYS); for (Province province : provinces) { pstmt.setString(1, province.getName()); pstmt.executeUpdate(); ResultSet generatedKeys = pstmt.getGeneratedKeys(); if (generatedKeys.next()) { int provinceId = generatedKeys.getInt(1); province.setId(provinceId); } generatedKeys.close(); } // 接下来处理市的插入 String insertCitySQL = "INSERT INTO city (name, province_id) VALUES (?, ?)"; PreparedStatement cityStmt = conn.prepareStatement(insertCitySQL); for (Province province : provinces) { for (City city : province.getCities()) { cityStmt.setString(1, city.getName()); cityStmt.setInt(2, province.getId()); cityStmt.addBatch(); } } cityStmt.executeBatch(); conn.commit(); // 提交事务 } catch (SQLException e) { conn.rollback(); // 回滚事务 // 异常处理 } ``` 但需要注意的是,上述JDBC示例中,插入省是逐条执行的,这可能在大批量数据时效率不高。为了优化省的插入,可能需要使用批量插入,但标准JDBC在批量插入时无法直接获取每个生成的主键,这可能需要其他方法,比如分批处理或者使用数据库特定的功能。 如果使用MyBatis,可以更高效地处理批量插入。例如,在MyBatis的Mapper XML中配置插入省的语句,并使用`useGeneratedKeys`: ```xml <insert id="insertProvince" useGeneratedKeys="true" keyProperty="id"> INSERT INTO province (name) VALUES (#{name}) </insert> ``` 然后在Java中调用该方法,插入省后,其id会被自动填充到对象中: ```java @Autowired private ProvinceMapper provinceMapper; @Autowired private CityMapper cityMapper; @Transactional public void insertProvincesWithCities(List<Province> provinces) { for (Province province : provinces) { provinceMapper.insertProvince(province); // id自动设置到province对象 cityMapper.batchInsertCities(province.getCities()); } } ``` 而`batchInsertCities`可以使用MyBatis的批量插入功能,例如使用`<foreach>`标签: ```xml <insert id="batchInsertCities"> INSERT INTO city (name, province_id) VALUES <foreach collection="list" item="city" separator=","> (#{city.name}, #{city.provinceId}) </foreach> </insert> ``` 不过,MyBatis的foreach批量插入在处理大量数据时可能会遇到SQL语句过长的问题,这时可以考虑分批次插入或者使用`ExecutorType.BATCH`模式。 此外,还需考虑异常处理,例如重复插入数据格式问题等,以及是否需要校验省市的名称是否已存在,避免重复数据。 总结来说,解决这个问题的关键在于: 1. **顺序插入**:先插入父节点(省),获取其生成的ID。 2. **维护关联**:将父节点ID赋给子节点(市)的父ID字段。 3. **批量操作**:使用批量插入提高子节点的插入效率。 4. **事务管理**:确保整个过程的原子性,避免部分插入导致的数据不一致。 需要根据具体的技术栈(纯JDBC、MyBatis、Hibernate等)选择合适的实现方式,同时考虑性能和数据的正确性。</think>在处理Java中批量插入具有父子关系的省市数据时,需确保子节点正确关联父节点自动生成的ID。以下是分步解决方案: --- ### **1. 数据结构设计** - **省(父节点)** 包含自动生成的ID和名称。 ```java public class Province { private Integer id; // 数据库自动生成 private String name; // 省略getter/setter } ``` - **市(子节点)** 包含名称和关联的省ID。 ```java public class City { private String name; private Integer provinceId; // 维护父节点ID // 省略getter/setter } ``` --- ### **2. 插入流程** #### **步骤一:插入数据并获取ID** - **方法**:逐条插入省,获取数据库生成的ID。 - **关键点**: - 使用JDBC的`Statement.RETURN_GENERATED_KEYS`或ORM框架(如MyBatis)的`useGeneratedKeys`特性。 - **JDBC示例**: ```java String sql = "INSERT INTO province (name) VALUES (?)"; try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { for (Province province : provinces) { pstmt.setString(1, province.getName()); pstmt.executeUpdate(); // 获取生成的ID ResultSet rs = pstmt.getGeneratedKeys(); if (rs.next()) province.setId(rs.getInt(1)); } } ``` #### **步骤二:为市关联省ID** - **逻辑**:遍历每个省的市列表,设置`provinceId`字段。 ```java for (Province province : provinces) { for (City city : province.getCities()) { city.setProvinceId(province.getId()); // 绑定父ID } } ``` #### **步骤三:批量插入数据** - **方法**:使用JDBC批量操作或ORM批量插入。 - **JDBC批量示例**: ```java String sql = "INSERT INTO city (name, province_id) VALUES (?, ?)"; try (PreparedStatement pstmt = conn.prepareStatement(sql)) { for (Province province : provinces) { for (City city : province.getCities()) { pstmt.setString(1, city.getName()); pstmt.setInt(2, city.getProvinceId()); pstmt.addBatch(); // 加入批处理 } } pstmt.executeBatch(); // 执行批量插入 } ``` --- ### **3. 使用ORM框架优化(如MyBatis)** #### **配置Mapper** - **插入省的Mapper XML**: ```xml <insert id="insertProvince" useGeneratedKeys="true" keyProperty="id"> INSERT INTO province (name) VALUES (#{name}) </insert> ``` - **批量插入市的Mapper XML**: ```xml <insert id="batchInsertCities"> INSERT INTO city (name, province_id) VALUES <foreach collection="list" item="city" separator=","> (#{city.name}, #{city.provinceId}) </foreach> </insert> ``` #### **Service层代码** ```java @Transactional // 事务管理 public void saveProvincesWithCities(List<Province> provinces) { for (Province province : provinces) { provinceMapper.insertProvince(province); // 插入省,ID自动回填 cityMapper.batchInsertCities(province.getCities()); // 批量插入市 } } ``` --- ### **4. 关键注意事项** 1. **事务一致性** 确保整个插入过程在事务中,避免部分数据失败导致脏数据。 2. **性能优化** - 使用JDBC批处理或ORM的批量模式(如MyBatis的`ExecutorType.BATCH`)。 - 分批次插入(如每1000条提交一次),避免内存溢出。 3. **异常处理** 捕获`SQLException`或数据库约束异常(如唯一键冲突),确保回滚事务。 --- ### **总结** 通过“先父后子”的顺序,结合数据库事务和批量操作技术,可高效实现层级数据批量插入。若使用ORM框架,能进一步简化代码并提升可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值