已回答  扫描DataTable-一种高效的方法

etl2016

活跃的成员
已加入
2016年6月29日
留言内容
39
编程经验
3-5
你好

DataTable是否适合批量十万行的大规模数据处理?

我有一个场景,原型如下,可以有效地满足两个需求,如if-else所示。

场景:

有一个数据表。有一个相同数量的一维列表/数组。 DataTable的特定列中保存的值将替换为列表中相应的位置值。如果找到空值,则这些位置值需要即时计算,从而将{key,value}对的新列表停放在一旁。

事实证明,逐行扫描DataTable非常缓慢。 DataTable是否适合此类目的?如果是,是否有更有效的方式来实现相同的目标?如果没有,请您告诉我哪些替代的.net编程功能可以解决问题,谢谢。

C#:
using System;
using System.Data;
using System.Collections.Generic;

namespace UpdateDataTable
{
    class Program
    {
        
        static void Main(string[] args)
        {

            DataTable table = GetTable();
            string[] CarNames = { "Volvo", "Tesla", null, null, "Ford" };
            DataTable returnedTable = UpdateTable(table, CarNames);

            for (int i = 0; i < returnedTable.Rows.Count; i++)
            {
                DataRow row = table.Rows[i];

                Console.WriteLine(row["Car"]);

            }
        }

        static DataTable GetTable()
        {
            DataTable table = new DataTable();

            table.Columns.Add("ID",   typeof(int));
            table.Columns.Add("Car",  typeof(string));
            table.Columns.Add("Name", typeof(string));
            table.Columns.Add("Date", typeof(DateTime));

            table.Rows.Add(25,  "Car A", "A", DateTime.Now);
            table.Rows.Add(50,  "Car B", "B", DateTime.Now);
            table.Rows.Add(10,  "Car C", "C", DateTime.Now);
            table.Rows.Add(21,  "Car D", "D", DateTime.Now);
            table.Rows.Add(100, "Car E", "E", DateTime.Now);
            
            return table;
        }
 
        static DataTable UpdateTable(DataTable table, string[] CarNames)
        {

            var list = new 清单<KeyValuePair<int, string>>();

            for (int i=0; i < table.Rows.Count; i ++)
            {
                DataRow row = table.Rows[i];
                if ( !string.IsNullOrEmpty (CarNames[i] ) )
                {
                    row["Car"] = CarNames[i];  //  Requirement-1 : to update datatable with non-nulls
                }
                else
                {
                    row["Car"] = "hello";      //Requirement-2:  Construct new value and pile them up in a list of pairs
                    list.Add(new KeyValuePair<int, string>(i, "hello"));
                }

            } // DataTable is updated row-by-row and is found very slow for large volumes

            return table;
        } // end of UpdateTable
     } // end of class
}
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
Welcome to O(log n) data access. The C# DataTable rows are stored as a tree. It's not an array so accessing a row by index needs to walk the tree. I don't know if the enumerator for the RowCollection is more efficient about walking the tree and remembering where it was last at.

现在我知道了乔!至少枚举器很聪明:
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
And it looks like the DataTable rows collection is a bit smarter now compared to the .NET Framework 1.1 days. Now it does a bunch of math to figure out where the node is by index instead of walking the tree the hard way:
 
Last edited:

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
再一次,我们真的会要求您发布您的真实代码,而不是您一直使用的伪代码。

In the past threads, I've questioned whether you really need a DataTable. My understanding is that you are going from .CSV, into multiple DataTables, and then from DataTables into Redis. Do you do anything with the DataTables afterwards? Or is what is really important that list from line 47 that you seem to be just discarding?

正如我以前提到的那样,如果新值的计算仅取决于正在处理的当前行,那么实际上就不需要DataTable,CSV中的每一行都可以在管道中进行处理,并完成它。自从您开始探索TPL DataFlow之后,您就知道可以并行化管道的各个部分。
 

etl2016

活跃的成员
已加入
2016年6月29日
留言内容
39
编程经验
3-5
谢谢。是的,这些单独的数据表将在写入文件系统之前进行合并。第47行中的列表将用作库异步方法的输入,该方法以性能批处理的方式更新Redis,在一分钟之内即可进行数十万次Redis调用。如果在管道中处理csv的每一行,会发现它非常慢,因为它必须使用单个的Get / Set库调用,每个调用都需要四分之一秒到一半的RTT。中间库方法提供的响应时间在整个方法中起着关键作用。将探讨您提到的有关TPL Dataflow中的并行化功能的选项,谢谢。
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
Each row of CSV , if processed in a pipeline, is noticed to be very slow, as it has to use singular Get/Set library calls, each of which is taking quarter to half a second RTT.
??您仍可以像处理数据表一样处理CSV的每一行并将新值放入列表中。
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
是的,这些单独的数据表将在写入文件系统之前进行合并。
合并还是仅将数据表的行连接在一起?如果合并,那么事物如何精确地合并在一起?如何确定CSV的哪几行去了哪个特定的DataTable?还是将CSV划分为DataTables只是从CSV读取的订单行的功能?
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
我无法入睡,所以这是我用笔记本电脑的电池运行一百万行时得到的结果:
C#:
DataTable index: 00:00:04.8185246
DataTable foreach: 00:00:04.6474218
RowByRow: 00:00:00.9127712

前两个使用数据表。他们从"csv"并填充数据表。它们都遍历数据表以进行处理。第一个通过索引遍历数据表,而第二个使用foreach获取枚举数。最后一个从"csv",进行处理,然后将结果附加到记录列表。所有这些都还创建了一个填充列表"created" key value pairs.

用于生成这些计时的代码:
C#:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;

struct CarRecord
{
    public int Id;
    public string Car;
    public string Name;
    public DateTime Date;
}

class Csv
{
    static DateTime _baseDate = DateTime.Now;

    int _count = 0;

    public IEnumerable<CarRecord> GetRecords()
    {
        while (true)
        {
            _count++;
            yield return new CarRecord()
            {
                Id = _count,
                Car = $"Car {_count:D8}",
                Name = $"{_count:D8}",
                Date = _baseDate.AddMilliseconds(_count)
            };
        }
    }
}

class NameGenerator
{
    static string[] _names =
    {
        "Volvo",
        "Tesla",
        "Ford",
    };

    Random _random = new Random(123456);

    public IEnumerable<string> GetNames()
    {
        while (true)
        {
            var index = _random.Next(_names.Length + 1);
            yield return index < _names.Length ? _names[index] : null;
        }
    }
}

class Program
{
    IEnumerable UseDataTableIndex(int rowCount, Csv CSV, NameGenerator generator)
    {
        var table = new DataTable();
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("Car", typeof(string));
        table.Columns.Add("Name", typeof(string));
        table.Columns.Add("Date", typeof(DateTime));

        int count = 0;
        foreach (var record in CSV.GetRecords())
        {
            table.Rows.Add(record.Id, record.Car, record.Name, record.Date);
            count++;
            if (count >= rowCount)
                break;
        }

        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        for(int index = 0; index < rowCount; index++)
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }
            var row = table.Rows[index];
            row["Name"] = name;
        }

        return table.Rows;
    }

    IEnumerable UseDataTableForEach(int rowCount, Csv CSV, NameGenerator generator)
    {
        var table = new DataTable();
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("Car", typeof(string));
        table.Columns.Add("Name", typeof(string));
        table.Columns.Add("Date", typeof(DateTime));

        int count = 0;
        foreach(var record in CSV.GetRecords())
        {
            table.Rows.Add(record.Id, record.Car, record.Name, record.Date);
            count++;
            if (count >= rowCount)
                break;
        }

        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        int index = 0;
        foreach (DataRow row in table.Rows)
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }
            row["Name"] = name;

            index++;
        }

        return table.Rows;
    }

    IEnumerable UseRowByRow(int rowCount, Csv CSV, NameGenerator generator)
    {
        var records = new 清单<CarRecord>(rowCount);
        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        int index = 0;
        foreach (var record in CSV.GetRecords())
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }

            records.Add(new CarRecord() { Id = record.Id, Car = record.Car, Name = name, Date = record.Date });

            index++;
            if (index >= rowCount)
                break;
        }

        return records;
    }

    const int RowCount = 1000000;

    void TimeIt(string caption, Func<int, Csv, NameGenerator, IEnumerable> func)
    {
        Console.Write($"{caption}: ");
        var stopwatch = new Stopwatch();

        var CSV = new Csv();
        var generator = new NameGenerator();
        stopwatch.Start();
        func(RowCount, CSV, generator);
        stopwatch.Stop();
        Console.WriteLine($"{stopwatch.Elapsed}");
    }

    void Run()
    {
        TimeIt("DataTable index", UseDataTableIndex);
        TimeIt("DataTable foreach", UseDataTableForEach);
        TimeIt("RowByRow", UseRowByRow);
    }

    static void Main()
    {
        new Program().Run();
    }
}
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
I was curious about how much of that 4 seconds for the first 2 is time used to load up the DataTable and how much was actually the cost of iterating and processing, so I go these results for processing:
C#:
DataTable index: 00:00:01.1930239
DataTable foreach: 00:00:00.5725378
RowByRow: 00:00:00.8157491

请注意,这不再是苹果与苹果的比较。前两个开始在时钟外填充其数据结构,仅是定时处理,而最后一个正在对其进行处理并在时钟上填充其数据结构。唯一一个最后一关的时间是最初构建的列表,其中可以容纳一百万个项目。

我使用以下代码:
C#:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;

struct CarRecord
{
    public int Id;
    public string Car;
    public string Name;
    public DateTime Date;
}

class Csv
{
    static DateTime _baseDate = DateTime.Now;

    int _count = 0;

    public IEnumerable<CarRecord> GetRecords()
    {
        while (true)
        {
            _count++;
            yield return new CarRecord()
            {
                Id = _count,
                Car = $"Car {_count:D8}",
                Name = $"{_count:D8}",
                Date = _baseDate.AddMilliseconds(_count)
            };
        }
    }
}

class NameGenerator
{
    static string[] _names =
    {
        "Volvo",
        "Tesla",
        "Ford",
    };

    Random _random = new Random(123456);

    public IEnumerable<string> GetNames()
    {
        while (true)
        {
            var index = _random.Next(_names.Length + 1);
            yield return index < _names.Length ? _names[index] : null;
        }
    }
}

class Program
{
    DataTable PrepareDataTable(int rowCount, Csv CSV)
    {
        var table = new DataTable();
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("Car", typeof(string));
        table.Columns.Add("Name", typeof(string));
        table.Columns.Add("Date", typeof(DateTime));

        int count = 0;
        foreach (var record in CSV.GetRecords())
        {
            table.Rows.Add(record.Id, record.Car, record.Name, record.Date);
            count++;
            if (count >= rowCount)
                break;
        }
        return table;
    }

    DataTable UseDataTableIndex(DataTable table, int rowCount, Csv CSV, NameGenerator generator)
    {
        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        for(int index = 0; index < rowCount; index++)
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }
            var row = table.Rows[index];
            row["Name"] = name;
        }

        return table;
    }

    DataTable UseDataTableForEach(DataTable table, int rowCount, Csv CSV, NameGenerator generator)
    {
        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        int index = 0;
        foreach (DataRow row in table.Rows)
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }
            row["Name"] = name;

            index++;
        }

        return table;
    }

    清单<CarRecord> UseRowByRow(List<CarRecord> records, int rowCount, Csv CSV, NameGenerator generator)
    {
        var list = new 清单<KeyValuePair<int, string>>();
        var enumerator = generator.GetNames().GetEnumerator();
        int index = 0;
        foreach (var record in CSV.GetRecords())
        {
            if (!enumerator.MoveNext())
                break;

            var name = enumerator.Current;
            if (name == null)
            {
                name = "hello";
                list.Add(new KeyValuePair<int, string>(index, name));
            }

            records.Add(new CarRecord() { Id = record.Id, Car = record.Car, Name = name, Date = record.Date });

            index++;
            if (index >= rowCount)
                break;
        }

        return records;
    }

    const int RowCount = 1000000;

    void TimeIt<T>(string caption, Func<int, Csv, T> prepare, Func<T, int, Csv, NameGenerator, T> process)
    {
        Console.Write($"{caption}: ");
        var stopwatch = new Stopwatch();

        var CSV = new Csv();
        var generator = new NameGenerator();

        T data = prepare(RowCount, CSV);

        stopwatch.Start();
        process(data, RowCount, CSV, generator);
        stopwatch.Stop();
        Console.WriteLine($"{stopwatch.Elapsed}");
    }

    void Run()
    {
        TimeIt("DataTable index", PrepareDataTable, UseDataTableIndex);
        TimeIt("DataTable foreach", PrepareDataTable, UseDataTableForEach);
        TimeIt("RowByRow", (count, CSV) => new 清单<CarRecord>(count), UseRowByRow);
    }

    static void Main()
    {
        new Program().Run();
    }
}
 

跳伞

工作人员
已加入
2019年4月6日
留言内容
2,500
地点
弗吉尼亚州切萨皮克
编程经验
10+
我要指出的是,我在#8和#9中使用的测试代码已经处理了1,000,000行。您在其他线程中说您只处理160,000(例如,每个10,000行的16个DataTable)。注意,即使有1,000,000行,使用索引也只花了1秒多一点的时间。我不知道您为什么要说访问数据表的每一行都很慢。
 

etl2016

活跃的成员
已加入
2016年6月29日
留言内容
39
编程经验
3-5
非常感谢跳伞运动员。抱歉,后续行动延迟,您正在尝试重新设计DataTable方法并将其替换为Lists等,其性能并没有更好。进一步挖掘,我可以将瓶颈定位到用户库方法,该方法用于在帖子#8的第90行中构造新值。为了检查影响,默认此步骤导致在一分钟内处理了几十万行,因此确认它是时间消耗者(每个呼叫最多半秒的周转时间)。因此,DataTable方法本身不会引起延迟,也不会对其进行迭代。再一次感谢你。
 
最佳 底部