您的位置:首页 > 数据库 > Oracle

.net导入Oracle数据优化小记

2015-11-17 10:59 399 查看

.net导入Oracle数据优化小记

工作中遇到一个项目需要每次部署时导入Oracle数据库约4万条数据,原计划使用dmp格式导入,但是这种方式需要依赖数据库的imp.exe文件,环境影响度比较大,于是决定使用Excel进行导入。最初使用Excel导入时每次平均耗时7分钟,不符合项目要求,经过优化后导入时间缩短到30秒左右。以下为本次优化研究的优化小记,希望在以后遇到此类问题时有所帮助。欢迎各位大神评论指教!

一、数据环境

数据库版本为Oracle11g,导入数据涉及数据库中18张表,约4万条数据。数据预先保存为Excel文档,一个sheet为一张表数据,每个sheet第一行为表字段名称,第二行为表字段类型,用于导入时进行数据格式转换。



二、引用组件

1、NOPI,使用 NPOI 可以在没有安装 Office 或者相应环境的机器上对 Excel文档进行读写。本文的研究主要用到了Excel文档的读取,代码如下:

using (FileStream fs = File.OpenRead(@"d:/myMergeCell.xls"))   //打开myxls.xls文件
{
HSSFWorkbook wk = new HSSFWorkbook(fs);   //把xls文件中的数据写入wk中
for (int i = 0; i < wk.NumberOfSheets; i++)  //NumberOfSheets是myxls.xls中总共的表数
{
ISheet sheet = wk.GetSheetAt(i);   //读取当前表数据
IRow rowName = sheet.GetRow(0);//表字段名
IRow rowType = sheet.GetRow(1);//表字段类型
for (int j = 2; j <= sheet.LastRowNum; j++)  //前两行为字段行,从第3行开始为数据行
{
IRow row = sheet.GetRow(j);  //读取当前行数据
if (row != null)
{
for (int k = 0; k < row.LastCellNum; k++)  //LastCellNum 是当前行的总列数
{
ICell cell = row.GetCell(k); //读取当前单元格数据
if (cell != null)
{
                 //对单元格进行操作......
//cell.DateCellValue获取日期格式的值
//cell.NumericCellValue获取数值格式的值
//cell.StringCellValue获取字符串格式的值
                 }
}
}
}
}
}


2、Oracle.DataAccess,Oracle公司提供的驱动,相对.net中的System.Data. OracleClient来说,能够为Oracle数据库提供更多更好的支持,其中包含支持批量导入的OracleBulkCopy类,能够快速的向Oracle数据库中导入大量的数据。

OracleBulkCopy类批量导入的代码如下:

using (var conn = new OracleConnection(connStr))
{
conn.Open();//先打开数据库连接
using (OracleBulkCopy bcp = new OracleBulkCopy(conn))
{
bcp.BatchSize = 400;//这是批尺寸可以调整
bcp.BulkCopyTimeout = 1000; //设置超时
bcp.DestinationTableName = table;//要导入的数据库表名
bcp.ColumnMappings.Clear();
for (int k = 0; k < rowName.LastCellNum; k++)
{//设置表字段映射,不设置的话可能会导致导入的数据列错乱
bcp.ColumnMappings.Add(rowName.GetCell(k).StringCellValue, rowName.GetCell(k).StringCellValue);
}
bcp.WriteToServer(dataTable);//将表数据转化为DataTable再进行批量导入
}
}


三、研究过程

1、逐条导入。首先遍历Excel中的sheet来获取每个表的数据,并根据sheetName和sheet的前两行数据拼接插入语句,注意在此之前需检查表是否存在,并先删除表中全部数据再进行导入。代码如下:

string table = sheet.SheetName;//表名
command.CommandText = "select * from all_tables where table_name=:table_name";
command.Parameters.Clear();
command.Parameters.Add(":table_name", table);
var reader = command.ExecuteReader();
if (!reader.HasRows)
{
reader.Close();
System.Console.WriteLine(table+"表不存在" );
continue;
}
//删除表数据
command.CommandText = "delete from " + table;
command.Parameters.Clear();
command.ExecuteNonQuery();
string field = "";//字段名
string fieldValue = "";//字段值
for (int k = 0; k < rowName.LastCellNum; k++)
{
field += rowName.GetCell(k).StringCellValue + ",";//拼接字段名
fieldValue += ":" + rowName.GetCell(k).StringCellValue + ",";//拼接绑定字段值
}
command.CommandText = "insert into " + table + "(" + field.TrimEnd(',') + ") values(" + fieldValue.TrimEnd(',') + ")";


之后遍历数据行,根据字段类型获取不同类型的单元格数据进行绑定,代码如下:

if (cell != null)
{
if (rowType.GetCell(k).StringCellValue == "DATE")
command.Parameters.Add(":" + rowName.GetCell(k).StringCellValue, cell.DateCellValue);
else if (rowType.GetCell(k).StringCellValue == "NUMBER")
command.Parameters.Add(":" + rowName.GetCell(k).StringCellValue, cell.NumericCellValue);
else
command.Parameters.Add(":" + rowName.GetCell(k).StringCellValue, cell.StringCellValue);
}
else
command.Parameters.Add(":" + rowName.GetCell(k).StringCellValue, DBNull.Value);


最终执行sql语句并记录影响行数,代码如下:

try
{
sumCount+=command.ExecuteNonQuery();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}


程序运行多次记录导入所有数据所需时间,平均耗时7min ,而如果让网页端等待后台数据导入,等待时间7min显然是不合理的,所以需要对数据导入方法进行优化。

2、多线程逐条导入。每一张表创建一个线程,用第1点的方法进行导入,代码如下:

for (int i = 0; i < wk.NumberOfSheets; i++)  //NumberOfSheets是myxls.xls中总共的表数
{
var t = new Thread(new ParameterizedThreadStart(impTheard));// impTheard方法中执行第1点的步骤
t.Start(i);
}


运行多次平均耗时缩短了1分钟左右,导入时间依然太长。由于不同的表数据量相差较大,数据量最大的一张表数据有2万多条,所以又将数据量较大的表拆分为多个线程进行导入,通过控制调整线程导入数据量来控制线程数量,因为线程运行需传入较多参数,所以创建impTable类来保存参数,代码如下:

int c = 2;//sheet行号
int yczxl = 4000;//每个线程导入行数
for (; c <= sheet.LastRowNum; c += yczxl+1)
{
var t = new Thread(new ThreadStart(new impTable(table, c, ((c + yczxl) > sheet.LastRowNum ? sheet.LastRowNum : (c + yczxl)), field, fieldValue, sheet, rowName, rowType).imp));
t.Start();
}


多次运行程序导入数据平均耗时6min,和未拆分前相差不大,所以还需进一步优化。

3、分别使用Oracle.DataAccess和System.Data.OracleClient进行导入,Oracle.DataAccess中的OracleCommand在进行变量绑定时可以使用command.Parameters.Add(string name,object val)方法,但是在4.0版本的System.Data.OracleClient中Parameters.Add(string name,object val) 方法已被否决,需替换为Parameters.AddWithValue(string name,object val)。运行程序对比发现两者导入耗时并没有明显变化。

4、使用OracleBulkCopy类批量导入,导入时需先将Excel文档中的sheet转为DataTable对象,注意必须设置DataTable字段类型,否则导入时会因数据类型不匹配而出现很多错误。OracleBulkCopy类批量导入的代码上一节已贴,这里就不再贴出来了。Sheet转DataTable代码如下:

DataTable dt = new DataTable();

//第一行是字段
IRow headRow = sheet.GetRow(0);
IRow rowType = sheet.GetRow(1);//表字段类型

//设置datatable字段
for (int i = headRow.FirstCellNum, len = headRow.LastCellNum; i < len; i++)
{
if (rowType.Cells[i].StringCellValue == "DATE")//设置DataTable字段为DATE类型
dt.Columns.Add(headRow.Cells[i].StringCellValue, typeof(DateTime));
else if (rowType.Cells[i].StringCellValue == "NUMBER")//设置DataTable字段为NUMBER类型
dt.Columns.Add(headRow.Cells[i].StringCellValue, typeof(Int32));
else
dt.Columns.Add(headRow.Cells[i].StringCellValue);
}
//遍历数据行
for (int i = 2, len = sheet.LastRowNum + 1; i < len; i++)
{
IRow tempRow = sheet.GetRow(i);
DataRow dataRow = dt.NewRow();
//遍历一行的每一个单元格
for (int r = 0, j = tempRow.FirstCellNum, len2 = tempRow.LastCellNum; j < len2; j++, r++)
{
ICell cell = tempRow.GetCell(j);
if (cell != null)
{
if (rowType.GetCell(j).StringCellValue == "DATE")
dataRow[r] = cell.DateCellValue;
else if (rowType.GetCell(j).StringCellValue == "NUMBER")
dataRow[r] = cell.NumericCellValue;
else
dataRow[r] = cell.StringCellValue;
}
else
dataRow[r] = DBNull.Value;
}
dt.Rows.Add(dataRow);
}
return dt;


OracleBulkCopy类批量导入无法得知导入受影响行数,所以在导入完成后,需再次访问数据库查询表中的数据总数作为数据导入受影响行数。

使用Stopwatch计时,代码如下:

Stopwatch MyStopWatch = new Stopwatch();
MyStopWatch.Start();
//......
MyStopWatch.Stop();
decimal t = MyStopWatch.ElapsedTicks;
System.Console.WriteLine(t / 10000000);


调节批尺为不同大小,运行多次耗时20s~70s,已达到网页端可接受的范围。当批尺为400时,导入所需时间为32秒。



5、分别使用release和debug方式进行编译运行。Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。但运行多次对比发现两者导入耗时相差不大。

四、研究结果总结、影响因素

多线程可以加快导入速度,但是也要注意线程数的控制。

使用OracleBulkCopy类批量导入能够非常快速的将大量数据导入数据库,调节批尺大小可以达到更好的效果。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: