【问题标题】:Fastest method to write a DataSet to a character delimited file将数据集写入字符分隔文件的最快方法
【发布时间】:2016-02-24 04:49:36
【问题描述】:

这是迄今为止我发现的从 Oracle DB 检索响应记录集并将其写入分隔文件的最快方法。更快会更好。请提出建议。

检索结果集:

using (var oracleConnection = new OracleConnection(ContextInfo.ConnectionString))
{
    oracleConnection.Open();
    try
    {
        using (var oracleCommand = new OracleCommand(extractToRun, OracleConnection))
        {

            oracleCommand.CommandType = CommandType.StoredProcedure;
            oracleCommand.BindByName = true;
            oracleCommand.FetchSize = oracleCommand.FetchSize * 128;
            oracleCommand.InitialLONGFetchSize = 5000;
            oracleCommand.Parameters.Add(refCursorOracleParameter);
            oracleCommand.Parameters.Add(startDateOracleParameter);
            oracleCommand.Parameters.Add(endDateOracleParameter);
            oracleCommand.Parameters.Add(jobIdOracleParameter);

            using (var oracleDataAdapter = new OracleDataAdapter(oracleCommand))
            {
                oracleDataAdapter.Fill(ds);
                return ds;
            }
        }
    }
    finally
    {
        oracleConnection.Close();
        oracleConnection.Dispose();
    }
}

处理数据并将其写入文件:

public static void ExportDataTableToDelimitedFile(DataTable table, string filename, string encloseWith, string delimiter, bool includeHeader, string fieldsToExclude, bool fixedLengthValues)
{
    String excludeList = String.Empty;

    if (!String.IsNullOrEmpty(fieldsToExclude))
    {
        excludeList = fieldsToExclude.ToUpper();
    }

    using (FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, 131072, FileOptions.None))
    {
        BinaryWriter sw = new BinaryWriter(fs);
        if (table.Rows.Count == 0)
        {
            sw.Write(String.Empty);
            sw.Close();
            sw.Dispose();
            return;
        }
        //Handle header
        if (includeHeader)
        {
            string header = String.Empty;
            String formattedHeader = String.Empty;
            foreach (DataColumn clm in table.Columns)
            {
                if (excludeList.Contains(clm.ColumnName.ToUpper()))
                    continue;

                if (clm.ColumnName.Length > 0)
                {
                    formattedHeader = String.Empty;
                    formattedHeader = encloseWith + clm.ColumnName + encloseWith;

                    if (header.Length > 0)
                        header = String.Join(delimiter, new string[] { header, formattedHeader });
                    else
                        header = formattedHeader;
                }
            }
            sw.Write(header);
        }
        // handle  values in data rows now
        Boolean hasEnlosedCharacter = !String.IsNullOrEmpty(encloseWith);
        ParallelOptions rowOptions = new ParallelOptions();
        rowOptions.MaxDegreeOfParallelism = Environment.ProcessorCount;
        Parallel.ForEach(table.Rows.Cast<DataRow>(), rowOptions, row =>
        {
            char[] rowValue = new char[8192];
            Int32 rowValueIndex = 0;

            string[] dcc = row.ItemArray.Select(field => field.ToString()).ToArray();
            foreach (String dc in dcc)
            {
                if (rowValueIndex > 0)
                {
                    if (!String.IsNullOrEmpty(dc) && hasEnlosedCharacter)
                    {
                        rowValue[rowValueIndex++] = delimiter[0];
                        rowValue[rowValueIndex++] = encloseWith[0];
                        foreach (char c in dc)
                        {
                            rowValue[rowValueIndex++] = c;
                        }
                        rowValue[rowValueIndex++] = encloseWith[0];
                    }
                    else
                    {
                        rowValue[rowValueIndex++] = delimiter[0];
                        foreach (char c in dc)
                        {
                            rowValue[rowValueIndex++] = c;
                        }
                    }
                }
                else
                {
                    if (!String.IsNullOrEmpty(dc) && hasEnlosedCharacter)
                    {
                        rowValue[rowValueIndex++] = encloseWith[0];
                        foreach (char c in dc)
                        {
                            rowValue[rowValueIndex++] = c;
                        }
                        rowValue[rowValueIndex++] = encloseWith[0];
                    }
                    else
                    {
                        foreach (char c in dc)
                        {
                            rowValue[rowValueIndex++] = c;
                        }
                    }
                }
            }

            rowValue[rowValueIndex++] = '\r';
            rowValue[rowValueIndex++] = '\n';
            lock (sw)
            {
                sw.Write(rowValue, 0, rowValueIndex);
            }
        });
        sw.Close();
        sw.Dispose();
        table.Dispose();
        fs.Close();
    }
}

我知道我应该重命名一些变量并以相同的方式处理标题(我不是在写标题)所以这确实是一个纯粹的逻辑问题,风格的答案无助于提高性能。

令人费解的是网络性能。当它快速返回 5 个几千行的数据集时,它只使用了 1.5% 的带宽?我正在对 11g 数据库使用最新的 ODP.Net (Oracle)。我尝试了 Devarts 提供程序,但它完全被我炸毁了。

Network Performance

处理器负载反映了 Parallel.ForEach 对数据表中的行的影响,这是一件好事。

Processor Performance

【问题讨论】:

  • 我建议使用using statement 而不是自己处理对象,这样更不容易出错。
  • 1) 你没有显示你的查询,所以我不能评论它的效率。 2) 您没有显示DataTable 是如何从ds 变量中填充的,您没有显示该变量被实例化。 3)我不知道为什么在数据检索后使用您的fieldToExclude 变量而不是使用它来创建有效的查询。有了这些限制,您是否尝试过使用StreamWriter
  • 实际上,在大多数情况下,从 oracle DB 中检索数据的时间应该比任何与后续处理数据表中的数据有关的操作都高出一个数量级。那么你真正感兴趣的是什么? (1) 将您的数据从 oracle 获取到数据表 (2) 将您的数据从数据表获取到文件 (3) 将您的数据从 oracle 获取到文件?
  • 其实从Oracle检索数据的时间还不错。我正在并行运行 7 个任务以运行 7 个查询并将数据集返回到主线程。完成后,我调用上面列出的过程 (ExportDataTableToDelimitedFile) 将内容写入分隔文件。查询会在几秒钟内返回。不过,将数据集带回客户端确实需要时间。大约30秒。导出 413K 行的总时间为 50 秒。我们有一个客户,他将通过 DAILY 导出数百万行。
  • 我已经尝试了多种将数据集转换为 char[] 数组的其他方法,这是可取的,因为二进制写入器是迄今为止最快的流写入器。它有一个 16 mb 的缓冲区。行 string[] dcc = row.ItemArray.Select(field => field.ToString()).ToArray();显着提高了速度,但没有办法将其转换为 char[][]?

标签: c# performance oracle11g


【解决方案1】:

这是我能得到的最快的。

检索数据:

public static DataTable GetData(String extractToRun, DateTime startDate, DateTime endDate)
{
    //RefCursor
    OracleParameter refCursorOracleParameter = new OracleParameter
                                            {
                                                ParameterName = "pCursor",
                                                Direction = ParameterDirection.Output,
                                                OracleDbType = OracleDbType.RefCursor
                                            };

    OracleParameter startDateOracleParameter = new OracleParameter
    {
        ParameterName = "pStartDate",
        Direction = ParameterDirection.Input,
        OracleDbType = OracleDbType.Varchar2,
        Value =   startDate
    };

    OracleParameter endDateOracleParameter = new OracleParameter
    {
        ParameterName = "pEndDate",
        Direction = ParameterDirection.Input,
        OracleDbType = OracleDbType.Varchar2,
        Value =   endDate
    };

    OracleParameter jobIdOracleParameter = new OracleParameter
    {
        ParameterName = "pJobId",
        Direction = ParameterDirection.Input,                
        Value =   "123456"
    };

    using (var oracleConnection = new OracleConnection(ContextInfo.ConnectionString))
    {
        oracleConnection.Open();
        try
        {
            using (var oracleCommand = new OracleCommand(extractToRun, oracleConnection))
            {

                oracleCommand.CommandType = CommandType.StoredProcedure;
                oracleCommand.BindByName = true;
                oracleCommand.FetchSize = oracleCommand.FetchSize * 128;
                oracleCommand.InitialLONGFetchSize = 5000;
                oracleCommand.Parameters.Add(refCursorOracleParameter);
                oracleCommand.Parameters.Add(startDateOracleParameter);
                oracleCommand.Parameters.Add(endDateOracleParameter);
                oracleCommand.Parameters.Add(jobIdOracleParameter);

                using (OracleDataReader rdr = oracleCommand.ExecuteReader())
                {
                    rdr.FetchSize = rdr.RowSize * 65536;
                    DataTable dt = new DataTable();
                    dt.MinimumCapacity = 400000;
                    dt.BeginLoadData();
                    dt.Load(rdr, LoadOption.Upsert);
                    dt.EndLoadData();
                    rdr.Close();
                    rdr.Dispose();
                    oracleCommand.Dispose();
                    return dt;
                }
            }
        }
        finally
        {
            oracleConnection.Close();
            oracleConnection.Dispose();
        }
    }
}

处理数据:

public static void ExportDataTableToDelimitedFile(DataTable table, string filename, string encloseWith, string delimiter, bool includeHeader, string fieldsToExclude, bool fixedLengthValues)
{
    String excludeList = String.Empty;

    if (!String.IsNullOrEmpty(fieldsToExclude))
    {
        excludeList = fieldsToExclude.ToUpper();
    }

    using (FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, 2097152, FileOptions.None))
    {
        BinaryWriter sw = new BinaryWriter(fs);
        if (table.Rows.Count == 0)
        {
            sw.Write(String.Empty);
            sw.Close();
            sw.Dispose();
            return;
        }
        //Handle header
        if (includeHeader)
        {
            string header = String.Empty;
            String formattedHeader = String.Empty;
            foreach (DataColumn clm in table.Columns)
            {
                if (excludeList.Contains(clm.ColumnName.ToUpper()))
                    continue;

                if (clm.ColumnName.Length > 0)
                {
                    formattedHeader = String.Empty;
                    formattedHeader = encloseWith + clm.ColumnName + encloseWith;

                    if (header.Length > 0)
                        header = String.Join(delimiter, new string[] { header, formattedHeader });
                    else
                        header = formattedHeader;
                }
            }
            sw.Write(header);
        }
        // handle  values in data rows now
        Boolean hasEnlosedCharacter = !String.IsNullOrEmpty(encloseWith);
        Parallel.ForEach(table.Rows.Cast<DataRow>(), row =>
        {
            char[] rowValue = new char[8192];
            Int32 rowValueIndex = 0;

            char[][] rowData = row.ItemArray.Select(field => field.ToString().ToCharArray()).ToArray();
            for (int i = 0; i < rowData.Length; i++)
            {
                Boolean useEnclosed = rowData[i].Length > 0 && hasEnlosedCharacter;
                if (rowValueIndex > 0)
                {
                    if (useEnclosed)
                    {
                        rowValue[rowValueIndex++] = delimiter[0];
                        rowValue[rowValueIndex++] = encloseWith[0];
                        rowData[i].CopyTo(rowValue, rowValueIndex);
                        rowValueIndex += rowData[i].Length;
                        rowValue[rowValueIndex++] = encloseWith[0];
                    }
                    else
                    {
                        rowValue[rowValueIndex++] = delimiter[0];
                        rowData[i].CopyTo(rowValue, rowValueIndex);
                        rowValueIndex += rowData[i].Length;
                    }
                }
                else
                {
                    if (useEnclosed)
                    {
                        rowValue[rowValueIndex++] = encloseWith[0];
                        rowData[i].CopyTo(rowValue, rowValueIndex);
                        rowValueIndex += rowData[i].Length;
                        rowValue[rowValueIndex++] = encloseWith[0];
                    }
                    else
                    {
                        rowData[i].CopyTo(rowValue, rowValueIndex);
                        rowValueIndex += rowData[i].Length;
                    }
                }
            }

            rowValue[rowValueIndex++] = '\r';
            rowValue[rowValueIndex++] = '\n';
            lock (sw)
            {
                sw.Write(rowValue, 0, rowValueIndex);
            }
        });
        sw.Close();
        sw.Dispose();
        table.Dispose();
        fs.Close();
    }
}

有几个关键点需要注意。使用 Load 将 DataReader 放入 DataTable 比 Dataset.Fill 快 40%,但不要将 fetchsize 设置为 64K 以上。之后性能下降。 32K可能是最好的。字符数组甚至比 StringBuilder 快得多。以我的拙见,C# 残缺不全,我们不能拥有汇编程序子例程。我考虑过编写一个 C++ dll,这样我就可以有一个汇编语言子例程来复制内存。然后我就不需要调用 ToCharArray()。诚然,我没有查看 IL 来确切了解 ToCharArray() 的作用,但性能分析器指出该行代码占用了 26% 的时间。

令人惊讶的是,这些更改将网络利用率提高了高达 4.5%(这对于公司网络上的单台 PC 来说是很高的),并将 CPU 利用率降低到 80% 左右,因为它现在主要等待磁盘写入方法,而不是等待忙着复制字符串。

我没有显示原始代码,但它过去需要 13-15 分钟才能将数据导出到管道分隔文件。通过这些更改,导出完全相同的数据需要 40-45 秒。

我也没有表明原始数据库查询中有七个查询全部联合在一起。我把它们分开了,这样我就可以并行运行它们。性能修复需要作为一个整体来解决。许多在专注于数据库之前试图解决这个问题的人。没有人真正关注客户端并试图找出真正的问题是什么。

希望这对将来的某人有所帮助。

【讨论】:

    【解决方案2】:

    好的!这是一个更好的答案!

    public static List<ROW_DATA> GetData(String extractToRun, DateTime startDate, DateTime endDate)
    {
        List<ROW_DATA> dataTable = new List<ROW_DATA>();
        //RefCursor
        OracleParameter refCursorOracleParameter = new OracleParameter
                                                {
                                                    ParameterName = "pCursor",
                                                    Direction = ParameterDirection.Output,
                                                    OracleDbType = OracleDbType.RefCursor
                                                };
    
        OracleParameter startDateOracleParameter = new OracleParameter
        {
            ParameterName = "pStartDate",
            Direction = ParameterDirection.Input,
            OracleDbType = OracleDbType.Varchar2,
            Value =   startDate
        };
    
        OracleParameter endDateOracleParameter = new OracleParameter
        {
            ParameterName = "pEndDate",
            Direction = ParameterDirection.Input,
            OracleDbType = OracleDbType.Varchar2,
            Value =   endDate
        };
    
        OracleParameter jobIdOracleParameter = new OracleParameter
        {
            ParameterName = "pJobId",
            Direction = ParameterDirection.Input,                
            Value =   "123456"
        };
    
        using (var oracleConnection = new OracleConnection(ContextInfo.ConnectionString))
        {
            oracleConnection.Open();
            try
            {
                using (var oracleCommand = new OracleCommand(extractToRun, oracleConnection))
                {
    
                    oracleCommand.CommandType = CommandType.StoredProcedure;
                    oracleCommand.BindByName = true;
                    oracleCommand.FetchSize = oracleCommand.FetchSize * 128;
                    oracleCommand.InitialLONGFetchSize = 5000;
                    oracleCommand.Parameters.Add(refCursorOracleParameter);
                    oracleCommand.Parameters.Add(startDateOracleParameter);
                    oracleCommand.Parameters.Add(endDateOracleParameter);
                    oracleCommand.Parameters.Add(jobIdOracleParameter);
    
                    using (OracleDataReader rdr = oracleCommand.ExecuteReader())
                    {
                        //byte[] columnBytes = new byte[16384];
                        Int32 tryCount = 0;
                        rdr.FetchSize = rdr.RowSize * 262144;
                                while (rdr.Read())
                                {
                                    Int32 charLength = (Int32)rdr.GetChars(0, 0, null, 0, 0);
                                    char[] colChars = new char[charLength];
                                    rdr.GetChars(0, 0, colChars, 0, charLength);
                                    //OracleString colValue = rdr.GetOracleString(0);
                                    //int valueLength = colValue.Length;
                                    //unsafe
                                    //{
                                    //    fixed (char* pcolValue = colValue.Value)
                                    //    {
                                    //        fixed (byte* pcolBytes = columnBytes)
                                    //        {
                                    //            for (int i = 0; i < valueLength; i++)
                                    //            {
                                    //                pcolBytes[i] = (byte)pcolValue[i];
                                    //            }
                                    //        }
                                    //    }
                                    //}
                                    ROW_DATA rowData = new ROW_DATA { length = charLength, rowValues = colChars };
                                    dataTable.Add(rowData);
                            }
                        }
                        rdr.Close();
                        rdr.Dispose();
                        oracleCommand.Dispose();
                        return dataTable;
                    }
                }
            }
            finally
            {
                oracleConnection.Close();
                oracleConnection.Dispose();
            }
        }
    }
    

    我故意留在注释掉的代码中,以表明我什至尝试了不安全的代码来将数据转换为我需要的格式。结果是,GetChars 以我想要的方式返回它,所以我可以简单地将它流式传输到磁盘。我的网络利用率高达 11%,需要 27 秒才能检索 413K 行并将它们写入磁盘。我还修改了存储过程以返回一个管道分隔的字符串,因此我只在客户端接收一列数据。它真的很快,但我有想法将时间缩短一半。敬请期待。

    【讨论】:

    • 这是仿照我的“让它走得快”的模式。
    • 在处理数据库时利用这些信息来提高应用程序的性能。我不会向您展示我对 Oracle 存储过程所做的事情,这显然也很重要。问汤姆可以在那个部门教育你。只知道 Dataset.Fill 的性能很糟糕。
    • 如果您知道 GetChars 将返回的最大字符长度,则分配该长度的缓冲区并且只调用一次 GetChars。这样会更快。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-01
    • 1970-01-01
    • 2015-02-07
    • 1970-01-01
    • 2016-08-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多