命名空间:Oracle.DataAccess.Client

组件:Oracle.DataAccess.dll(2.112.1.0)

ODP.NET 版本:ODP.NET for .NET Framework 2.0 或 ODP.NET for .NET Framework 4

工具:Microsoft Visual Studio Ultimate 2013 + Oracle SQL Developer 1.5.5 + Oracle Database 11g Enterprise Edition 11.2.0.1.0(32位) + TNS for 32-bit Windows 11.2.0.1.0

方式一:ArrayBind

当插入一条数据时,SQL 语句如下:

INSERT INTO table_name VALUES (:col1, :col2, :col3, :col4, :col5)
 public void InsertDataRow(Dictionary<string, object> dataRow)
{
StringBuilder sbCmdText = new StringBuilder();
sbCmdText.AppendFormat("INSERT INTO {0}(", m_TableName);
sbCmdText.Append(string.Join(",", dataRow.Keys.ToArray()));
sbCmdText.Append(") VALUES (");
sbCmdText.Append(":" + string.Join(",:", dataRow.Keys.ToArray()));
sbCmdText.Append(")"); using (OracleConnection conn = new OracleConnection())
{
using (OracleCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = sbCmdText.ToString();
OracleParameter parameter = null;
OracleDbType dbType = OracleDbType.Object;
foreach (string colName in dataRow.Keys)
{
dbType = GetOracleDbType(dataRow[colName]);
parameter = new OracleParameter(colName, dbType);
parameter.Direction = ParameterDirection.Input;
parameter.OracleDbTypeEx = dbType;
parameter.Value = dataRow[colName];
cmd.Parameters.Add(parameter);
}
conn.Open();
int result = cmd.ExecuteNonQuery();
}
}
}

此时,每一个 OracleParameter 的 Value 值都赋予单个字段的 一个具体值,这种也是最为传统的插入数据的方法。

Oracle V6 中 OCI 编程接口加入了数组接口特性。

当采用 ArrayBind 时,OraleParameter 的 Value 值则是赋予单个字段的 一个数组,即多条数据的该字段组合成的一个数组。此时 Oracle 仅需要执行一次 SQL 语句,即可在内存中批量解析并导入数据,减少程序与数据库之间来回的操作,其优点就是数据导入的总体时间明显减少,尤其是进程占用 CPU 的时间。

如果数据源是 DataTable 类型,首先把 DataTable 数据源,转换成 object[][] 类型,然后绑定 OracleParameter 的 Value 值为对应字段的一个 Object[] 数组即可;参考代码如下:

 /// <summary>
/// 批量插入大数据量
/// </summary>
/// <param name="columnData">列名-列数据字典</param>
/// <param name="dataCount">数据量</param>
/// <returns>插入数据量</returns>
public int InsertBigData(Dictionary<string, object> columnData, int dataCount)
{
int result = 0;
if (columnData == null || columnData.Count < 1)
{
return result;
}
string[] colHeaders = columnData.Keys.ToArray();
StringBuilder sbCmdText = new StringBuilder();
if (columnData.Count > 0)
{
// 拼接INSERT的SQL语句
sbCmdText.AppendFormat("INSERT INTO {0}(", m_TableName);
sbCmdText.Append(string.Join(",", colHeaders));
sbCmdText.Append(") VALUES (");
sbCmdText.Append(m_ParameterPrefix + string.Join("," + m_ParameterPrefix, colHeaders));
sbCmdText.Append(")");
OracleConnection connection = null;
try
{
connection = new OracleConnection(GetConnectionString());
using (OracleCommand command = connection.CreateCommand())
{
command.ArrayBindCount = dataCount;
command.BindByName = true;
command.CommandType = CommandType.Text;
command.CommandText = sbCmdText.ToString();
command.CommandTimeout = 1800;
OracleParameter parameter;
OracleDbType dbType = OracleDbType.Object;
foreach (string colName in colHeaders)
{
dbType = GetOracleDbType(columnData[colName]);
parameter = new OracleParameter(colName, dbType);
parameter.Direction = ParameterDirection.Input;
parameter.OracleDbTypeEx = dbType;
parameter.Value = columnData[colName];
command.Parameters.Add(parameter);
}
connection.Open();
OracleTransaction trans = connection.BeginTransaction();
try
{
command.Transaction = trans;
result = command.ExecuteNonQuery();
trans.Commit();
}
catch (Exception ex)
{
trans.Rollback();
throw ex;
}
}
}
finally
{
if (connection != null)
{
connection.Close();
connection.Dispose();
}
GC.Collect();
GC.WaitForFullGCComplete();
}
}
return result;
}
 /// <summary>
/// 根据数据类型获取OracleDbType
/// </summary>
/// <param name="value">数据</param>
/// <returns>数据的Oracle类型</returns>
private static OracleDbType GetOracleDbType(object value)
{
OracleDbType dataType = OracleDbType.Object;
if (value is string[])
{
dataType = OracleDbType.Varchar2;
}
else if (value is DateTime[])
{
dataType = OracleDbType.TimeStamp;
}
else if (value is int[] || value is short[])
{
dataType = OracleDbType.Int32;
}
else if (value is long[])
{
dataType = OracleDbType.Int64;
}
else if (value is decimal[] || value is double[] || value is float[])
{
dataType = OracleDbType.Decimal;
}
else if (value is Guid[])
{
dataType = OracleDbType.Varchar2;
}
else if (value is bool[] || value is Boolean[])
{
dataType = OracleDbType.Byte;
}
else if (value is byte[])
{
dataType = OracleDbType.Blob;
}
else if (value is char[])
{
dataType = OracleDbType.Char;
}
return dataType;
}

GetOracleDbType

说明:如果采用分次(每次1万数据)执行 InsertBigData 方法,速度反而比一次性执行 InsertBigData 方法慢,详见下面测试结果;

测试结果:

无索引,数据类型:4列NVARCHAR2,2列NUMBER

30+万(7.36M):一次性导入用时 15:623,每次10000导入用时

60+万(14.6M):一次性导入用时 28:207,每次10000导入用时 1:2:300

100+万(24.9M):一次性导入报如下异常

此时实际上从资源监视器上可以得知仍有可用内存,但是仍旧报 OutOfMemoryException,所以猜测应该是一个 bug;

如果每次10000导入用时 2:9:252

如果每次50000导入用时 58:101

附加 InsertBigData 方法使用示例:

 // 每10000数据导入一次
Dictionary<string, object> columnsData = new Dictionary<string, object>();
int dataCount = m_SourceDataTable.Rows.Count;
int times = dataCount / 10000 + (dataCount % 10000 == 0 ? 0 : 1);
for (int i = 0; i < times; i++)
{
int startIndex = i * 10000;
int endIndex = (i + 1) * 10000;
endIndex = endIndex > dataCount ? dataCount : endIndex;
int currDataCount = endIndex - startIndex;
columnsData.Add("COL1", new string[currDataCount]);
columnsData.Add("COL2", new string[currDataCount]);
columnsData.Add("COL3", new decimal[currDataCount]);
columnsData.Add("COL4", new string[currDataCount]);
columnsData.Add("COL5", new decimal[currDataCount]);
columnsData.Add("COL6", new string[currDataCount]);
for (int rowIndex = startIndex; rowIndex < endIndex; rowIndex++)
{
int dicRowIndex = rowIndex - startIndex;// 列数据行索引
foreach (string colName in columnsData.Keys)
{
object cell = m_SourceDataTable.Rows[rowIndex][colName];
string cellStr = (cell + "").TrimEnd(new char[] { '\0', ' ' });
if (colName == "COL3" || colName == "COL5")
{
decimal value = 0;
decimal.TryParse(cellStr, out value);
((decimal[])columnsData[colName])[dicRowIndex] = value;
}
else
{
((string[])columnsData[colName])[dicRowIndex] = cellStr;
}
}
}
m_DAL.InsertBigData(columnsData, currDataCount); columnsData.Clear();
GC.Collect();
GC.WaitForFullGCComplete();
}

方式二:OracleBulkCopy

说明:

1. OracleBulkCopy 采用 direct path 方式导入;

2. 不支持 transaction,无法 Rollback;

3. 如果该表存在触发器时,无法使用 OracleBulkCopy(报异常信息 Oracle Error: ORA-26086),除非先禁用该表的所有触发器;

4. 过程中会自动启用 NOT NULL、UNIQUE 和 PRIMARY KEY 三种约束,其中 NOT NULL 约束在列数组绑定时验证,任何违反 NOT NULL 约束条件的行数据都会舍弃;UNIQUE 约束是在导入完成后重建索引时验证,但是在 bulk copy 时,允许违反索引约束,并在完成后将索引设置成禁用(UNUSABLE)状态;而且,如果索引一开始状态就是禁用(UNUSABLE)状态时,OracleBulkCopy 是会报错的。

参考代码如下:

 /// <summary>
/// 批量插入数据
/// 该方法需要禁用该表所有触发器,并且插入的数据如果为空,是不会采用默认值
/// </summary>
/// <param name="table">数据表</param>
/// <param name="targetTableName">数据库目标表名</param>
/// <returns></returns>
public bool InsertBulkData(DataTable table, string targetTableName)
{
bool result = false;
string connStr = GetConnectionString();
using (OracleConnection connection = new OracleConnection(connStr))
{
using (OracleBulkCopy bulkCopy = new OracleBulkCopy(connStr, OracleBulkCopyOptions.Default))
{
if (table != null && table.Rows.Count > 0)
{
bulkCopy.DestinationTableName = targetTableName;
for (int i = 0; i < table.Columns.Count; i++)
{
string col = table.Columns[i].ColumnName;
bulkCopy.ColumnMappings.Add(col, col);
}
connection.Open();
bulkCopy.WriteToServer(table);
result = true;
}
bulkCopy.Close();
bulkCopy.Dispose();
}
} return result;
}

测试结果:

数据类型:4列NVARCHAR2,2列NUMBER

30+万(7.36M):用时 14:590

60+万(14.6M):用时 28:28

1048576(24.9M):用时 52:971

附加,禁用表的所有外键SQL:

ALTER TABLE table_name DISABLE ALL TRIGGERS

总结

1、在30+万和60+万数据时,ArrayBind一次性导入和OracleBulkCopy时间相差不是很大,但是ArrayBind方式一般都需要转换数据形式,占用了一些时间,而 OracleBulkCopy 则只需要简单处理一下 DataTable 数据源即可导入;

2、当数据量达到100+万时,ArrayBind 很容易出现内存不足异常,此时只能采用分批次执行导入,根据测试结果可知,次数越少,速度越快;而采用 OracleBulkCopy 方式则很少出现内存不足现象,由此可见 OracleBulkCopy 占用内存比 ArrayBind 方式少;

3、采用 OracleBulkCopy 导入时,先要禁用该表所有触发器,如果该表存在自增 ID 触发器,就比较麻烦了,得先禁用改变的自增 ID 的触发器,然后手动自增设置要导入的数据,最后才可以导入;同时,这种导入方式不可并发,一个时刻只能有一个用户在导入(因为自增ID交由程序处理),此时还需要锁表,防止其他人同时批量导入数据;

参考资料:

1、ArrayBind http://www.oracle.com/technetwork/issue-archive/2009/09-sep/o59odpnet-085168.html

2、ArrayBind http://www.soaspx.com/dotnet/csharp/csharp_20130911_10501.html

3、Oracle数据导入方法 http://dbanotes.net/Oracle/All_About_Oracle_Data_Loading.htm

4、介绍OracleBulkCopy类 https://docs.oracle.com/cd/E11882_01/win.112/e23174/OracleBulkCopyClass.htm#ODPNT7446

5、http://dba.stackexchange.com/questions/7287/what-specifically-does-oraclebulkcopy-do-and-how-can-i-optimize-its-performance

[Oracle] Bulk Insert Data的更多相关文章

  1. Bulk Insert Data

    Bulk Insert Data 命名空间:Oracle.DataAccess.Client 组件:Oracle.DataAccess.dll(2.112.1.0) ODP.NET 版本:ODP.NE ...

  2. bulk insert data into database with table type .net

    1. Create Table type in Sqlserver2008. CREATE TYPE dbo.WordTable as table ( [WordText] [nchar]() NUL ...

  3. 从一个Bug说开去--解决问题的思路,Linked Server, Bulk Insert, DataTable 作为参数传递

    声名— 部分内容为杜撰,如有雷同,不胜荣幸! 版权所有,如要引用,请标明出处! 如果打赏,请自便! 1       背景介绍 最近一周在忙一个SQL Server 的Bug,一个简单的Bug,更新两张 ...

  4. Bulk Insert:将文本数据(csv和txt)导入到数据库中

    将文本数据导入到数据库中的方法有很多,将文本格式(csv和txt)导入到SQL Server中,bulk insert是最简单的实现方法 1,bulk insert命令,经过简化如下 BULK INS ...

  5. SQL Server Bulk Insert批量数据导入

    SQL Server的Bulk Insert语句可以将本地或远程的数据文件批量导入到数据库中,速度非常的快.远程文件必须共享才行,文件路径须使用通用约定(UNC)名称,即"\\服务器名或IP ...

  6. Bulk Insert的用法 .

    /******* 导出到excel */EXEC master..xp_cmdshell 'bcp SettleDB.dbo.shanghu out c:/temp1.xls -c -q -S&quo ...

  7. Bulk Insert & BCP执行效率对比

    我们以BCP导出的CSV数据文件,分别使用Bulk insert与BCP导入数据库,对比两种方法执行效率 备注:导入目标表创建了分区聚集索引 1.BCP导出csv数据文件 数据量:15000000行, ...

  8. Bulk Insert命令具体

    Bulk Insert命令具体 BULK INSERT以用户指定的格式复制一个数据文件至数据库表或视图中. 语法: BULK INSERT [ [ 'database_name'.][ 'owner' ...

  9. Bulk Insert 高效快速插入数据

    BULK INSERT以用户指定的格式复制一个数据文件至数据库表或视图中. 语法: BULK INSERT [ [ 'database_name'.][ 'owner' ].]{ 'table_nam ...

随机推荐

  1. C#多线程之基础篇3

    在上一篇C#多线程之基础篇2中,我们主要讲述了确定线程的状态.线程优先级.前台线程和后台线程以及向线程传递参数的知识,在这一篇中我们将讲述如何使用C#的lock关键字锁定线程.使用Monitor锁定线 ...

  2. javascript单元测试框架mochajs详解

    关于单元测试的想法 对于一些比较重要的项目,每次更新代码之后总是要自己测好久,担心一旦上线出了问题影响的服务太多,此时就希望能有一个比较规范的测试流程.在github上看到牛逼的javascript开 ...

  3. Mac OS 使用 Vagrant 管理虚拟机(VirtualBox)

    Vagrant(官网.github)是一款构建虚拟开发环境的工具,支持 Window,Linux,Mac OS,Vagrant 中的 Boxes 概念类似于 Docker(实质是不同的),你可以把它看 ...

  4. 你真的会玩SQL吗?之逻辑查询处理阶段

    你真的会玩SQL吗?系列目录 你真的会玩SQL吗?之逻辑查询处理阶段 你真的会玩SQL吗?和平大使 内连接.外连接 你真的会玩SQL吗?三范式.数据完整性 你真的会玩SQL吗?查询指定节点及其所有父节 ...

  5. [数据结构]——链表(list)、队列(queue)和栈(stack)

    在前面几篇博文中曾经提到链表(list).队列(queue)和(stack),为了更加系统化,这里统一介绍着三种数据结构及相应实现. 1)链表 首先回想一下基本的数据类型,当需要存储多个相同类型的数据 ...

  6. 子类继承父类时JVM报出Error:Implicit super constructor People() is undefined for default constructor. Must define an explicit constructor

    当子类继承父类的时候,若父类没有定义带参的构造方法,则子类可以继承父类的默认构造方法 当父类中定义了带参的构造方法,子类必须显式的调用父类的构造方法 若此时,子类还想调用父类的默认构造方法,必须在父类 ...

  7. Membership三步曲之入门篇 - Membership基础示例

    Membership 三步曲之入门篇 - Membership基础示例 Membership三步曲之入门篇 -  Membership基础示例 Membership三步曲之进阶篇 -  深入剖析Pro ...

  8. 在 Linux 中使用搜狗拼音输入法以及搞定 Flash 和支付宝

    在 Ubuntu 中安装搜狗输入法 在 Ubuntu Kylin 系统中,默认安装搜狗拼音输入法,但是在原生 Ubuntu 系统中则不是.这可以理解,毕竟搜狗输入法的 Linux 版有 Kylin 团 ...

  9. .NET里简易实现IoC

    .NET里简易实现IoC 前言 在前面的篇幅中对依赖倒置原则和IoC框架的使用只是做了个简单的介绍,并没有很详细的去演示,可能有的朋友还是区分不了依赖倒置.依赖注入.控制反转这几个名词,或许知道的也只 ...

  10. 适合WebApi的简单的C#状态机实现

    目标 采用了Restful WebApi的架构,再把业务逻辑状态转移放到后端就有点违背初衷了.实际上只要后端Api的权限设置的好,把状态转移放到前端也未尝不可.我考虑的结果是,一般如果变更这个状态本身 ...