【问题标题】:Bulk Insert to Oracle using .NET使用 .NET 批量插入到 Oracle
【发布时间】:2010-09-25 11:47:13
【问题描述】:

使用 .NET 向 Oracle 进行批量插入的最快方法是什么?我需要使用 .NET 将大约 160K 记录传输到 Oracle。目前,我正在使用插入语句并执行 160K 次。大约需要 25 分钟才能完成。作为从另一个数据库(MySQL)查询的结果,源数据存储在 DataTable 中,

有没有更好的方法来做到这一点?

编辑:我目前正在使用 System.Data.OracleClient,但愿意接受使用其他提供商(ODP.NET、DevArt 等)的解决方案。

【问题讨论】:

  • 像 SQL*Loader 这样的实用程序有什么问题?
  • 你尝试过 DevArt 吗?我想知道 Devart 是否有 OracleBulkCopy。

标签: .net sql oracle bulkinsert


【解决方案1】:

我使用 ODP.NET 中的数组绑定在 15 秒左右的时间内加载了 50,000 条记录

它通过重复调用您指定的存储过程(并且您可以在其中执行更新/插入/删除)来工作,但它会将多个参数值从 .NET 批量传递到数据库。

您可以为每个参数指定一个数组,而不是为存储过程的每个参数指定一个值。

Oracle 一次性将 .NET 中的参数数组传递到数据库,然后使用您指定的参数值重复调用您指定的存储过程。

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

/达米安

【讨论】:

  • 这适用于数组,但他正在将数据表写回数据库。如何在 ODP.Net 中使用数组绑定到数据表?
  • 那个链接好像失效了。我在 oracle 论坛中搜索数组绑定,它具有相同的非功能链接。有新地点吗?
  • 我已经更新了帖子,添加了一个指向 Oracle 示例的工作链接。
  • 试试这个链接c-sharpcorner.com/article/…。这对我使用 .NET 框架 4.5.1 将数据从 sql server 2012 导入到 Oracle 11c 有用
  • 另一个关于数组绑定的参考blogs.oracle.com/oraclemagazine/put-your-arrays-in-a-bind
【解决方案2】:

我最近发现了一个非常适合批量插入 (ODP.NET) 的专用类。 Oracle.DataAccess.Client.OracleBulkCopy!它以数据表为参数,然后调用WriteTOServer方法……非常快速有效,祝你好运!!

【讨论】:

    【解决方案3】:

    Rob Stevenson-Legget 的解决方案很慢,因为他不绑定他的值,但他使用 string.Format()。

    当您要求 Oracle 执行一条 sql 语句时,它首先计算该语句的 has 值。之后,它会在哈希表中查找它是否已经知道该语句。如果它已经知道它的语句,它可以从这个哈希表中检索它的执行路径并非常快地执行这个语句,因为 Oracle 之前已经执行过这个语句。这称为库缓存,如果您不绑定 sql 语句,它将无法正常工作。

    例如不要这样做:

    int n;

        for (n = 0; n < 100000; n ++)
        {
          mycommand.CommandText = String.Format("INSERT INTO [MyTable] ([MyId]) VALUES({0})", n + 1);
          mycommand.ExecuteNonQuery();
        }
    

    但是做:

          OracleParameter myparam = new OracleParameter();
          int n;
    
          mycommand.CommandText = "INSERT INTO [MyTable] ([MyId]) VALUES(?)";
          mycommand.Parameters.Add(myparam);
    
          for (n = 0; n < 100000; n ++)
          {
            myparam.Value = n + 1;
            mycommand.ExecuteNonQuery();
          }
    

    不使用参数也会导致sql注入。

    【讨论】:

    • 5 年前发布的好答案! :) 西奥在哪里?!
    【解决方案4】:

    SQL Server 的 SQLBulkCopy 速度非常快。不幸的是,我发现 OracleBulkCopy 慢得多。它也有问题:

    • 如果您打算 使用 OracleBulkCopy。如果发生主键冲突,ORA-26026 被提出,它似乎是不可恢复的。试图重建 索引无济于事,并且表上的任何后续插入都失败, 也正常插入。
    • 即使数据是干净的,我发现 OracleBulkCopy 有时会卡在 WriteToServer 中。问题 似乎取决于批量大小。在我的测试数据中,问题会 当我重复时,发生在我的测试中完全相同的时间点。用一个 更大或更小的批量大小,并且不会发生问题。我懂了 大批量的速度更不规则,这点 与内存管理相关的问题。

    如果你想用小记录但很多行填充一个表,实际上 System.Data.OracleClient.OracleDataAdapter 比 OracleBulkCopy 快。您需要调整批处理大小,OracleDataAdapter 的最佳 BatchSize 小于 OracleBulkCopy。

    我在一台装有 x86 可执行文件和 32 位 ODP.Net 客户端 2.112.1.0 的 Windows 7 机器上运行我的测试。 . OracleDataAdapter 是 System.Data.OracleClient 2.0.0.0 的一部分。我的测试集大约有 600,000 行,最大记录大小。 102 字节(平均大小 43 个字符)。数据源是一个 25 MB 的文本文件,以流的形式逐行读取。

    在我的测试中,我将输入数据表构建为固定的表大小,然后使用 OracleBulkCopy 或 OracleDataAdapter 将数据块复制到服务器。我在 OracleBulkCopy 中将 BatchSize 保留为 0(以便将当前表内容作为一个批次复制)并将其设置为 OracleDataAdapter 中的表大小(同样应该在内部创建一个批次)。 最佳结果:

    • OracleBulkCopy:表大小 = 500,总持续时间 4'22"
    • OracleDataAdapter:表大小 = 100,总持续时间 3'03"

    比较:

    • SqlBulkCopy:表大小 = 1000,总持续时间 0'15"
    • SqlDataAdapter:表大小 = 1000,总持续时间 8'05"

    同一台客户端机器,测试服务器是 SQL Server 2008 R2。对于 SQL Server,大容量复制显然是最好的方法。它不仅总体上最快,而且服务器负载也低于使用数据适配器时。遗憾的是 OracleBulkCopy 没有提供完全相同的体验 - BulkCopy API 比 DataAdapter 更易于使用。

    【讨论】:

    • 您对 Oracle 数据驱动程序中内存管理的脆弱性是完全正确的。当插入行数不同时,我遇到了完全相同的问题。此外,当我迭代大批量插入时,性能会随着时间的推移而下降,直到 10 个插入的简单批次可能需要长达 5 分钟。
    • 还有一件事要告诉 - 当它执行插入时它会禁用索引。这意味着如果您尝试插入重复行,您将以 ORA-01502 - dba-oracle.com/… 结尾。非常讨厌的错误。
    【解决方案5】:

    解决这个问题的一个非常快速的方法是建立一个从 Oracle 数据库到 MySQL 数据库的数据库链接。您可以创建指向非 Oracle 数据库的数据库链接。创建数据库链接后,您可以使用 ... create table mydata as select * from ... 语句从 MySQL 数据库中检索数据。这称为异构连接。这样,您无需在 .net 应用程序中执行任何操作即可移动数据。

    另一种方法是使用 ODP.NET。在 ODP.NET 中,您可以使用 OracleBulkCopy 类。

    但我不认为使用 System.Data.OracleClient 在 Oracle 表中插入 160k 记录应该需要 25 分钟。我认为你犯了太多次。您是使用参数将您的值绑定到插入语句还是连接您的值。绑定要快得多。

    【讨论】:

    • 删除了我的答案,因为这是一个更好的解决方案。
    • 我实际上已经在我们的开发环境中设置了 dblink,但是当涉及到生产时,事实证明 oracle 和 MySQL 甚至没有驻留在同一个网络中,因此我的快速而肮脏的解决方法使用 .net 上传器。我真的需要看看那个 OracleBulkCopy。有指针吗?
    【解决方案6】:

    发现链接的示例有些混乱,我编写了一些代码来演示如何将有效的数组插入到测试表 (jkl_test)。这是表格:

    create table jkl_test (id number(9));
    

    这是一个简单的控制台应用程序的 .Net 代码,它使用 ODP.Net 连接到 Oracle 并插入一个 5 个整数的数组:

    using Oracle.DataAccess.Client;
    
    namespace OracleArrayInsertExample
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Open a connection using ODP.Net
                var connection = new OracleConnection("Data Source=YourDatabase; Password=YourPassword; User Id=YourUser");
                connection.Open();
    
                // Create an insert command
                var command = connection.CreateCommand();
                command.CommandText = "insert into jkl_test values (:ids)";
    
                // Set up the parameter and provide values
                var param = new OracleParameter("ids", OracleDbType.Int32);
                param.Value = new int[] { 22, 55, 7, 33, 11 };
    
                // This is critical to the process; in order for the command to 
                // recognize and bind arrays, an array bind count must be specified.
                // Set it to the length of the array.
                command.ArrayBindCount = 5;
                command.Parameters.Add(param);
                command.ExecuteNonQuery();
            }
        }
    }
    

    【讨论】:

      【解决方案7】:

      用我的发现跟进 Theo 的建议(抱歉 - 我目前没有足够的声誉来发表评论)

      首先,这是如何使用几个命名参数:

      String commandString = "INSERT INTO Users (Name, Desk, UpdateTime) VALUES (:Name, :Desk, :UpdateTime)";
      using (OracleCommand command = new OracleCommand(commandString, _connection, _transaction))
      {
          command.Parameters.Add("Name", OracleType.VarChar, 50).Value = strategy;
          command.Parameters.Add("Desk", OracleType.VarChar, 50).Value = deskName ?? OracleString.Null;
          command.Parameters.Add("UpdateTime", OracleType.DateTime).Value = updated;
          command.ExecuteNonQuery();
      }
      

      但是,我发现:

      • 为每一行构造一个新的 commandString (String.Format)
      • 为每一行构造一个现在参数化的 commandString
      • 使用单个 commandString 并更改参数

      我正在使用 System.Data.OracleClient,在事务中删除和插入 2500 行

      【讨论】:

        【解决方案8】:

        甲骨文说 (http://www.oracle.com/technology/products/database/utilities/htdocs/sql_loader_overview.html)

        SQL*Loader 是主要的方法 快速填充 Oracle 表 来自外部文件的数据

        我的经验是,他们的加载器加载表格的速度比其他任何东西都快。

        【讨论】:

        • 不确定它是否具有 .NET API - 我认为最初的问题是关于 .NET。此外,它还不足以满足我的目的,因为它只是插入 - 我想根据记录是否已经存在来更新/插入。不过对于提问者来说可能就足够了。
        • SQL*Loader 是一个oracle数据加载工具。它可以处理存储在 csv、txt 或其他文件中的大量数据。将你的数据从 mysql 导出为 csv 格式,并编写一个加载器脚本将数据泵入 oracle。
        【解决方案9】:

        如果您使用的是非托管 oracle 客户端 (Oracle.DataAccess),那么最快的方法是使用 OracleBulkCopy,正如 Tarik 指出的那样。

        如果您使用的是最新的托管 oracle 客户端 (Oracle.ManagedDataAccess),那么最快的方法是使用数组绑定,正如 Damien 所指出的那样。如果您希望保持应用程序代码不受数组绑定细节的影响,您可以使用数组绑定编写自己的 OracleBulkCopy 实现。

        以下是真实项目的使用示例:

        var bulkWriter = new OracleDbBulkWriter();
            bulkWriter.Write(
                connection,
                "BULK_WRITE_TEST",
                Enumerable.Range(1, 10000).Select(v => new TestData { Id = v, StringValue=v.ToString() }).ToList());
        

        10K 记录在 500 毫秒内插入!

        这里是实现:

        public class OracleDbBulkWriter : IDbBulkWriter
        {
            public void Write<T>(IDbConnection connection, string targetTableName, IList<T> data, IList<ColumnToPropertyMapping> mappings = null)
            {
                if (connection == null)
                {
                    throw new ArgumentNullException(nameof(connection));
                }
                if (string.IsNullOrEmpty(targetTableName))
                {
                    throw new ArgumentNullException(nameof(targetTableName));
                }
                if (data == null)
                {
                    throw new ArgumentNullException(nameof(data));
                }
                if (mappings == null)
                {
                    mappings = GetGenericMappings<T>();
                }
        
                mappings = GetUniqueMappings<T>(mappings);
                Dictionary<string, Array> parameterValues = InitializeParameterValues<T>(mappings, data.Count);
                FillParameterValues(parameterValues, data);
        
                using (var command = CreateCommand(connection, targetTableName, mappings, parameterValues))
                {
                    command.ExecuteNonQuery();
                }
            }
        
            private static IDbCommand CreateCommand(IDbConnection connection, string targetTableName, IList<ColumnToPropertyMapping> mappings, Dictionary<string, Array> parameterValues)
            {
                var command = (OracleCommandWrapper)connection.CreateCommand();
                command.ArrayBindCount = parameterValues.First().Value.Length;
        
                foreach(var mapping in mappings)
                {
                    var parameter = command.CreateParameter();
                    parameter.ParameterName = mapping.Column;
                    parameter.Value = parameterValues[mapping.Property];
        
                    command.Parameters.Add(parameter);
                }
        
                command.CommandText = $@"insert into {targetTableName} ({string.Join(",", mappings.Select(m => m.Column))}) values ({string.Join(",", mappings.Select(m => $":{m.Column}")) })";
                return command;
            }
        
            private IList<ColumnToPropertyMapping> GetGenericMappings<T>()
            {
                var accessor = TypeAccessor.Create(typeof(T));
        
                var mappings = accessor.GetMembers()
                    .Select(m => new ColumnToPropertyMapping(m.Name, m.Name))
                    .ToList();
        
                return mappings;
            }
        
            private static IList<ColumnToPropertyMapping> GetUniqueMappings<T>(IList<ColumnToPropertyMapping> mappings)
            {
                var accessor = TypeAccessor.Create(typeof(T));
                var members = new HashSet<string>(accessor.GetMembers().Select(m => m.Name));
        
                mappings = mappings
                                .Where(m => m != null && members.Contains(m.Property))
                                .GroupBy(m => m.Column)
                                .Select(g => g.First())
                                .ToList();
                return mappings;
            }
        
            private static Dictionary<string, Array> InitializeParameterValues<T>(IList<ColumnToPropertyMapping> mappings, int numberOfRows)
            {
                var values = new Dictionary<string, Array>(mappings.Count);
                var accessor = TypeAccessor.Create(typeof(T));
                var members = accessor.GetMembers().ToDictionary(m => m.Name);
        
                foreach(var mapping in mappings)
                {
                    var member = members[mapping.Property];
        
                    values[mapping.Property] = Array.CreateInstance(member.Type, numberOfRows);
                }
        
                return values;
            }
        
            private static void FillParameterValues<T>(Dictionary<string, Array> parameterValues, IList<T> data)
            {
                var accessor = TypeAccessor.Create(typeof(T));
                for (var rowNumber = 0; rowNumber < data.Count; rowNumber++)
                {
                    var row = data[rowNumber];
                    foreach (var pair in parameterValues)
                    {
                        Array parameterValue = pair.Value;
                        var propertyValue = accessor[row, pair.Key];
                        parameterValue.SetValue(propertyValue, rowNumber);
                    }
                }
            }
        }
        

        注意:此实现使用 Fastmember 包来优化对属性的访问(比反射快得多)

        【讨论】:

        • 您的代码示例不可用,因为该示例缺少一些类。 * ColumnToPropertyMapping, * OracleCommandWrapper, * IDbBulkWriter
        【解决方案10】:

        我猜 OracleBulkCopy 是最快的方法之一。我有一些困难要学习,我需要一个新的 ODAC 版本。参照。 Where is type [Oracle.DataAccess.Client.OracleBulkCopy] ?

        这是从查询复制到合适的现有 Oracle 表的完整 PowerShell 代码。我尝试了 Sql-Server 一个数据源,但其他有效的 OLE-DB 源将转到。

        if ($ora_dll -eq $null)
        {
            "Load Oracle dll"
            $ora_dll = [System.Reflection.Assembly]::LoadWithPartialName("Oracle.DataAccess") 
            $ora_dll
        }
        
        # sql-server or Oracle source example is sql-server
        $ConnectionString ="server=localhost;database=myDatabase;trusted_connection=yes;Provider=SQLNCLI10;"
        
        # Oracle destination
        $oraClientConnString = "Data Source=myTNS;User ID=myUser;Password=myPassword"
        
        $tableName = "mytable"
        $sql = "select * from $tableName"
        
        $OLEDBConn = New-Object System.Data.OleDb.OleDbConnection($ConnectionString)
        $OLEDBConn.open()
        $readcmd = New-Object system.Data.OleDb.OleDbCommand($sql,$OLEDBConn)
        $readcmd.CommandTimeout = '300'
        $da = New-Object system.Data.OleDb.OleDbDataAdapter($readcmd)
        $dt = New-Object system.Data.datatable
        [void]$da.fill($dt)
        $OLEDBConn.close()
        #Write-Output $dt
        
        if ($dt)
        {
            try
            {
                $bulkCopy = new-object ("Oracle.DataAccess.Client.OracleBulkCopy") $oraClientConnString
                $bulkCopy.DestinationTableName = $tableName
                $bulkCopy.BatchSize = 5000
                $bulkCopy.BulkCopyTimeout = 10000
                $bulkCopy.WriteToServer($dt)
                $bulkcopy.close()
                $bulkcopy.Dispose()
            }
            catch
            {
                $ex = $_.Exception
                Write-Error "Write-DataTable$($connectionName):$ex.Message"
                continue
            }
        }
        

        顺便说一句:我用它来复制带有 CLOB 列的表。我没有使用链接服务器cf. question on dba 来实现它。我没有用新的 ODAC 重试链接服务。

        【讨论】:

          猜你喜欢
          • 2017-05-22
          • 1970-01-01
          • 2018-03-10
          • 2012-01-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-12
          相关资源
          最近更新 更多