C#/SQLite 性能与 FoxPro 导入文件

C#/SQLite performance vs FoxPro for importing files

我 "inherited" 一些用于处理大型文本文件的遗留软件,目前是用 Visual Foxpro(特别是第 7 版)编写的。

我正在探索用更易于修改和维护的新程序替换这些程序的选项。但是,我无法接近 Foxpro 导入大型文本文件的性能。

例如,我有一个约 1gb 的文本文件,其中包含约 110,000 条记录,每条记录有 213 个字段(每行长 9247 个字符),我正在对其进行测试。

使用 Foxpro:

? SECONDS()
USE new
APPEND FROM test.dat TYPE sdf
USE
? SECONDS()

在我的电脑上导入数据库只用了不到 10 秒。

使用 C#/SQLite(使用 Filehelpers 和 System.Data.SQLite),我能够完成此导入的最快时间超过一分钟。我已尝试使用 this question 的建议(例如交易等)尽可能优化它。实际上,如果我不将其与 Foxpro 的 10 秒进行比较,导入 1gb 文件的一分钟似乎并不坏。

大约 1 分钟内花费的时间细分为:

Filehelpers reading file and parsing: 12 seconds.
Building SQL commands from Filehelpers object: 15 seconds.
Running individual ExecuteNonQuery()'s: 24 seconds.
Committing transactions (every 5000 records): 12 seconds.

与线程链接相比,我每秒的插入速度要慢得多,但我的记录有 213 个字段,而 7 个字段,所以这是预料之中的。如果我按 fields/second 将其分解,我大约有 360,000 与线程的 630,000。其他海报的兆字节插入率是 ~2.24 megabyte/s,我是 15.4 megatbyte/s。因此,我认为我的表现与其他海报相当,我可能没有更多的优化可以做。

为什么这比 Foxpro 导入慢很多(慢 5-6 倍)?这是否只是苹果对橙子,我应该接受较慢的速度来权衡我使用新技术获得的其他好处?

编辑:

这是我 运行 的一些测试代码,随机数据显示了相似的速度....我是否正确地执行了交易? ExecuteNonQuery() 花费了大量时间。我已经用简单的、非参数化的、很可能是低效的字符串操作实现了所有 SQL 命令,但在这一点上我并不过分关心(我将从文件中读取,这种方法要快得多比使用 entity framework).

public class Program
{
    public static void Main(string[] args)
    {
        TestSqlite(100, 20000);
    }

    public static void TestSqlite(int numColumns, int numRows)
    {
        Console.WriteLine("Starting Import.... columns: " + numColumns.ToString() + " rows: " + numRows.ToString());

        var conn = new SQLiteConnection(@"Data Source=C:\vsdev\SqliteTest\src\SqliteTest\ddic.db;Version=3");
        conn.Open();
        var cmd = new SQLiteCommand(conn);
        cmd.CommandText = "DROP TABLE IF EXISTS test";
        cmd.ExecuteNonQuery();

        string createCmd = "CREATE TABLE 'test'('testId' INTEGER PRIMARY KEY  AUTOINCREMENT  NOT NULL, ";

        for (var i = 0; i < numColumns; i++)
        {
            createCmd += "'test" + i.ToString() + "' TEXT, ";
        }

        createCmd = createCmd.Substring(0, createCmd.Length - 2);
        createCmd += ")";
        cmd.CommandText = createCmd;
        cmd.ExecuteNonQuery();

        Stopwatch stopWatch = new Stopwatch();
        stopWatch.Start();

        var transaction = conn.BeginTransaction();


        int lineCount = 0;
        long startTime;
        long endTime;
        long totalStringTime = 0;
        long totalAddTime = 0;
        long totalSaveTime = 0;
        string command;
        for (var l = 0; l < numRows; l++)
        {


            startTime = stopWatch.ElapsedMilliseconds;
            command = CreateRandomInsert("test", numColumns);
            endTime = stopWatch.ElapsedMilliseconds;
            totalStringTime += endTime - startTime;


            ///Execute Query
            startTime = stopWatch.ElapsedMilliseconds;
            cmd.CommandText = command;
            cmd.ExecuteNonQuery();
            endTime = stopWatch.ElapsedMilliseconds;
            totalAddTime += endTime - startTime;


            if (lineCount > 5000)
            {
                lineCount = 0;
                startTime = stopWatch.ElapsedMilliseconds;

                transaction.Commit();
                transaction.Dispose();
                transaction = conn.BeginTransaction();
                cmd = new SQLiteCommand(conn);
                endTime = stopWatch.ElapsedMilliseconds;
                totalSaveTime += endTime - startTime;
                Console.Write('.');
            }
            lineCount += 1;

        }

        startTime = stopWatch.ElapsedMilliseconds;
        transaction.Commit();
        transaction.Dispose();
        endTime = stopWatch.ElapsedMilliseconds;
        totalSaveTime += endTime - startTime;


        Console.WriteLine('.');
        Console.WriteLine("String time: " + totalStringTime.ToString());
        Console.WriteLine("ExecuteNonQuery time: " + totalAddTime.ToString() + ", per 1000 records: " + (totalAddTime / (numRows / 1000)).ToString());
        Console.WriteLine("Commit time: " + totalSaveTime.ToString() + ", per 1000 records: " + (totalSaveTime / (numRows / 1000)).ToString());


        stopWatch.Stop();
        TimeSpan ts = stopWatch.Elapsed;
        string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
            ts.Hours, ts.Minutes, ts.Seconds,
            ts.Milliseconds / 10);
        Console.WriteLine(" in " + elapsedTime);

        conn.Close();

    }


    public static string CreateRandomInsert(string TableName, int numColumns)
    {

        List<string> nameList = new List<string>();
        List<string> valueList = new List<string>();

        for (var i = 0; i < numColumns; i++)
        {
            nameList.Add("test" + i.ToString());
            valueList.Add(Guid.NewGuid().ToString());
        }
        return CreateSql(TableName, nameList, valueList);
    }
    public static string CreateSql(string TableName, List<string> names, List<string> values)
    {
        string textCommand = "";

        textCommand += "INSERT INTO " + TableName + " (";

        foreach (var nameVal in names)
        {
            textCommand += nameVal + ", ";
        }
        textCommand = textCommand.Substring(0, textCommand.Length - 2);
        textCommand += ") VALUES (";
        foreach (var val in values)
        {
            textCommand += "'" + val + "', ";
        }
        textCommand = textCommand.Substring(0, textCommand.Length - 2);
        textCommand += ");";

        return textCommand;
    }
}

当是固定宽度的数据时,VFP比其他数据库更有优势。低级别,它几乎与 VFP 存储其数据的格式相同。例如,如果您没有任何与 Foxpro 2.x 不兼容的字段,那么导入该数据将仅意味着在每行前面添加一个 0x20 字节(如果尚未写入文件头,则写入文件头) - 并更新索引(如果有的话),这将是真正耗时的部分。如果您需要解析行,那么与 C# 相比,VFP 在字符串操作上非常慢。

(我不知道 FileHelpers,感谢指点)

9247 字节的固定数据和 213 个字段很有趣。我想测试插入 SQLite、postgreSQL、SQL 服务器和一些 NoSQL 数据库。您能否共享一个空的 dbf(或者只是作为代码的结构或 xml - 可能包含来自文件的 1-2 行样本数据)?我之前没有尝试过 200 多个字段,否则 110K 行的 1 分钟听起来很慢。

VFP table 只能附加此类数据 2 次,您需要创建另一个 table。你说的是 "manipulation" 文本文件,但你没有评论那是什么类型的操作。也许不附加到 table 的直接 "manipulation" 更高效、更快并且整个过程完成得更快?只是一个想法。