我有两个DataTable:



dt:从具有超过170万行的CSV文件填充。

大约也有一百万行该代码需要48个小时才能完成。我已将应用程序的属性更改为x64,以允许它使用更多的进程内存。现在它大约使用2.5GB。

我的问题是,有没有一种更有效的方法可以减少运行时间?

>如何填充dataStructure.Tables["AccountData"]

//set is_legal column value for each row
foreach (DataRow row in dt.Rows)
{
    var acctNum = row[0].ToString().Replace("\"", "");
    foreach (DataRow queryRow in dataStructure.Tables["AccountData"].Rows)
    {
        var queryAcctNum = queryRow[0].ToString();
        if (acctNum.Equals(queryAcctNum))
        {
            row[12] = "Y";
            Console.WriteLine("Yes count: " + cnt);
        }
        else
        {
            row[12] = "N";
        }
    }
    cnt++;
};


评论

Console.WriteLine是否在您的真实代码中?您必须运行剖析器才能找到所需的时间,但这是我投入金钱的地方。

看起来好像坏了。您不会中断;找到匹配项之后,所有记录都将具有row [12] ==“ N”。

@Johnbot多数民众赞成在一个很好的收获,你应该让这个答案!

为什么首先要在内存中执行此操作?将所有数据倒入数据库,添加适当的索引,用SQL编写要编写的查询,并使数据库优化该查询;这就是它的优点。

我感到惊讶的是,您有耐心等待48小时才能看到需要多长时间。我的意思是,这段代码效率极低,但是这种耐心必须有所作为。

#1 楼

您的内部循环似乎是不必要的。为什么不创建查找:

var knownAccountNumbers = new HashSet<string>(
    dataStructure.Tables["AccountData"].Rows
        .Cast<DataRow>()
        .Select(row => row[0].ToString()));


现在您的循环很简单:

foreach (DataRow row in dt.Rows)
{
    var accountNumber = row[0].ToString().Replace("\"", "");
    row[12] = knownAccountNumbers.Contains(accountNumber) ? "Y" : "N";
}


我想我记得读取一次,HashSet的内存使用量为每个条目12字节+条目大小。因此,您正在查看的是12MB + 1,000,000 *(2 * accountNumber.Length)。因此,在宏伟的计划中基本上什么也没有。但是,您将获得不断的时间查询,这对于此类工作将是一个巨大的好处。请勿缩写,例如acctNum-> accountNumber

评论


\ $ \ begingroup \ $
此解决方案花费了17秒。
\ $ \ endgroup \ $
– HappyCoding
16年2月16日在15:36

\ $ \ begingroup \ $
@HappyCoding-真高兴! :)
\ $ \ endgroup \ $
– RobH
16年2月16日在15:48

\ $ \ begingroup \ $
每次调用Contains时,都会懒惰地计算knownAccountNumbers。最后应该确实有一个.ToList()或.ToDictionary()。
\ $ \ endgroup \ $
– Esben Skov Pedersen
16-2-17在14:27

\ $ \ begingroup \ $
@EsbenSkovPedersen-它不是这样工作的。可枚举在HashSet的构造函数中评估。您可以使用参考源进行确认。最终调用了枚举可枚举的UnionWith。
\ $ \ endgroup \ $
– RobH
16年2月17日在15:04

\ $ \ begingroup \ $
@EsbenSkovPedersen-我将不得不再次不同意。我不需要字典(键->值),我只需要基于键的简单快速查找-即哈希集。
\ $ \ endgroup \ $
– RobH
16年2月17日在20:01

#2 楼

该代码似乎已损坏,找到匹配项后您没有break;,因此所有记录都可能具有row[12] == "N"

您应该对accountNumber进行联接: >
var matchingRows =
    from DataRow row in dt.Rows
    let rowKey = row[0].ToString().Replace("\"", "")
    join DataRow queryRow in dataStructure.Tables["AccountData"].Rows
    on rowKey equals queryRow[0]
    select row;

foreach(var row in matchingRows)
{
    row[12] = "Y";
}


这样,您将在第一次匹配时停止搜索,仅更新匹配的行。

评论


\ $ \ begingroup \ $
此解决方案花费了16秒。
\ $ \ endgroup \ $
– HappyCoding
16年2月16日在15:37

\ $ \ begingroup \ $
执行后,matchingRows仅包含那些匹配的行,而不是初始数据集。
\ $ \ endgroup \ $
– HappyCoding
16年2月16日在15:45

#3 楼

一些评论:

避免使用内存密集型数据类型

而不是使用DataTable进行dt,而是直接从csv读取,一次只读取一行(readLine()和然后split(',')。这将大大减少您的内存使用量,而不是一次只使用一个时一次加载全部170万行。

排序数据更快br />
按帐号对数据结构进行排序。之后,您可以进行二进制搜索来查找帐号,并在对其进行迭代后将其中断,这将大大减少循环时间。在将数据读入dataStructure之前,您甚至可以使数据库进行排序,甚至更好。

替代方案

您还可以尝试将所有dt加载到临时表中运行dataStructure的同一数据库,然后使用存储过程进行更新。该数据库比使用C#循环可以更有效地执行此更新。

评论


\ $ \ begingroup \ $
-1用于推荐朴素的csv解析
\ $ \ endgroup \ $
–温斯顿·埃韦特(Winston Ewert)
16-2-17的1:23

\ $ \ begingroup \ $
@WinstonEwert您能解释为什么这是一件坏事吗?
\ $ \ endgroup \ $
–约西亚
16-2-17在13:47

\ $ \ begingroup \ $
如果CSV有很多转义符(逗号是文本的一部分,然后将其引号,然后引号需要转义),则拆分可能会出现问题。如果csv数据是“安全的”,那么我认为该建议没有任何不利之处
\ $ \ endgroup \ $
–扎克
16年2月17日在15:04

\ $ \ begingroup \ $
根据数据库的不同,您甚至可以将CSV文件声明为外部表并运行一个UPDATE语句,这将使用优化的查询批量更新所有行,而无需担心IO。
\ $ \ endgroup \ $
– Falco
16年2月17日在17:12

\ $ \ begingroup \ $
如果CSV具有单个转义的单元格,则拆分会给您不正确的结果。当您可以确保所有单元格都不会逃脱时,您就可以摆脱它。但是,对于您一无所知的170万行CSV文件,建议采用幼稚的解析策略,我认为这是个坏建议。
\ $ \ endgroup \ $
–温斯顿·埃韦特(Winston Ewert)
16-2-18的3:42

#4 楼

基于@Johnbot注释

正确地知道您正在遍历dataStructure.Tables["AccountData"]的所有记录,无论是否找到匹配项。如果break,您确实应该从此循环中退出acctNum.Equals(queryAcctNum)。这应该可以大大加快您的任务速度(至少可以找到数据)。


另一个可能的增强功能是对表的行进行排序,并存储内部循环的最后找到的“索引”,以将其用作下一次迭代的开始。这需要将循环从foreach更改为正常的for循环,这反而可能会更快。

假设dt的第一列也命名为“ AccountNumber”,这将加快处理过程。

    dt.DefaultView.Sort = "AccountNumber";

    var accountDataTable = dataStructure.Tables["AccountData"];
    accountDataTable.DefaultView.Sort  = "AccountNumber";
    int numberOfAccountDataRows = accountDataTable.Rows.Count;
    int currentIndex = 0;

    foreach (DataRow row in dt.Rows)
    {
        var acctNum = row[0].ToString().Replace("\"", "");
        for(int i = currentIndex; i < numberOfAccountDataRows; i++) 
        {

            var queryAcctNum = accountDataTable.Rows[i][0].ToString();
            if (acctNum.Equals(queryAcctNum))
            {
                row[12] = "Y";
                currentIndex = i;
                break;
            }

        }
    }


#5 楼

免责声明:我没有用C#编写代码,因此您可能会感到失望。如果有人愿意将此移植到C#,请这样做,这可能会对OP有所帮助。

比较两个包含或排除的数据集,只要它们经过排序就可以完成。

该算法接近“合并排序”的合并过程;以伪代码

left = /*sorted stream 1*/
right = /*sorted stream 2*/
while not left.empty() and not right.empty():
    if left.current() == right.current():
        print "Common item", left.current()
        left.moveToNext()
        right.moveToNext()
        continue
    if left.current() < right.current():
        print "Left specific item", left.current()
        left.moveToNext()
        continue
    if left.current() > right.current():
        print "Right specific item", right.current()
        right.moveToNext()
        continue

while not left.empty()
    print "Left specific item", left.current()
    left.moveToNext()

while not right.empty()
    print "Right specific item", right.current()
    right.moveToNext()


优点:


恒定内存
快速

缺点:


要求两个流都需要预先排序

如果您可以轻松地获得两个流的排序顺序,那么很可能会失败。 >
如果必须准备一个数据集(对它进行排序),那么对于非常大的数据集(几乎不能容纳在内存中)还是有好处的。一个不进行排序,然后使用另一种解决方案(将一个较小的解决方案拖入一个哈希图中,并在迭代较大的解决方案时进行查找)。

#6 楼

感谢@RobH的回答,并在其上进行了微代码优化,例如:在String.Replace中使用字符串常量
使用char数据类型而不是字符串数据类型

在检索自身时已添加knownAccountNumbers


var knownAccountNumbers = new HashSet<string>();

//// If AccountData DataTable used only for condition then 
//// Delete Code Related to AccountData DataTable and Use knownAccountNumbers HashSet alone.
DataTable accountDt = dataStructure.Tables["AccountData"];

while (readFile.Read())
{
    string accountNumber = readFile.GetString(0).Trim();

    knownAccountNumbers.Add(accountNumber);

    DataRow dr = accountDt.NewRow();

    dr[AppConst.AccountNumber] = accountNumber;
    dr[AppConst.LegalStatus] = readFile.GetString(1);

    accountDt.Rows.Add(dr);
}

/* ----------------------------------------------------------------------------------------------- */

foreach (DataRow row in dt.Rows)
{
    //// For single char string replace use data type as char instead of string.
    //// Gives more perfromance
    //// var accountNumber = row[0].ToString().Replace(AppConst.BackSlashChar, AppConst.EmptyChar);
    var accountNumber = row[0].ToString().Replace(AppConst.BackSlash, string.Empty);

    //// Use Constant String will not create unnecessary memory allocation.
    row[12] = knownAccountNumbers.Contains(accountNumber) ? AppConst.Yes : AppConst.No;
}

/*------------------------------------------------------------------------------------------------ */

public static class AppConst
{
    public const string Yes      = "Y";
    public const string No       = "N";
    //// public const char   BackSlashChar = '\"';
    //// public const char   EmptyChar = 'q4312078q';
    public const string BackSlash = "\"";

    public const string AccountNumber = "AccountNumber";
    public const string LegalStatus   = "LegalStatus";
}


评论


\ $ \ begingroup \ $
几乎可以肯定,代码的性能优势来自在构建DataTable的同时初始化Hashset。我的答案为所有数据添加了一个额外的循环,以构建您的代码无法执行的数据。
\ $ \ endgroup \ $
– RobH
17年7月5日在11:17

\ $ \ begingroup \ $
@RobH:这只是在答案之上完成的微型代码优化。避免强制转换类型转换,拆箱,选择循环,并且使用字符串常量来减少内存消耗和节省几秒钟的时间....:-0)
\ $ \ endgroup \ $
– Thulasiram
17年7月5日在13:05



\ $ \ begingroup \ $
字符串常量不会对性能造成任何影响-尤其是内存消耗,因为C#会保留字符串文字。我不知道您认为您没有像以前那样拳击。我可以看到的唯一区别是我之前说过的:通过DataTable保存整个迭代。
\ $ \ endgroup \ $
– RobH
17年7月5日在13:23