如何在C#中高效地从SQL数据阅读器写入文件?

13
我有一个在C#中的远程sql连接需要执行查询并将结果保存到用户本地硬盘。这个东西可以返回相当大量的数据,因此需要考虑一种有效的存储方式。我之前读过,先将整个结果放入内存,然后再写入不是一个好主意,所以如果有人能帮忙,那就太好了!
我目前将sql结果数据存储到DataTable中,虽然我认为在while(myReader.Read(){...}中做些什么可能会更好。以下是获取结果的代码:
          DataTable t = new DataTable();
            string myQuery = QueryLoader.ReadQueryFromFileWithBdateEdate(@"Resources\qrs\qryssysblo.q", newdate, newdate);
            using (SqlDataAdapter a = new SqlDataAdapter(myQuery, sqlconn.myConnection))
            {
                a.Fill(t);
            }

            var result = string.Empty;
    for(int i = 0; i < t.Rows.Count; i++)
    {
        for (int j = 0; j < t.Columns.Count; j++)
        {
            result += t.Rows[i][j] + ",";
        }


        result += "\r\n";
    }

所以现在我有一个巨大的结果字符串。而且我有数据表。肯定有更好的方法做到这一点吧?
谢谢。

可能是 https://dev59.com/R3E95IYBdhLWcg3wlu-z 的重复问题。 - Dennis Traub
你是只写入未格式化的平面文件,还是将数据放入列中,例如.csv电子表格? - DOK
8个回答

22

你已经朝着正确的方向走了。使用一个循环,使用while(myReader.Read(){...},在循环内部将每个记录写入文本文件。.NET框架和操作系统会以高效的方式自动将缓冲区刷新到磁盘上。

using(SqlConnection conn = new SqlConnection(connectionString))
using(SqlCommand cmd = conn.CreateCommand())
{
  conn.Open();
  cmd.CommandText = QueryLoader.ReadQueryFromFileWithBdateEdate(
    @"Resources\qrs\qryssysblo.q", newdate, newdate);

  using(SqlDataReader reader = cmd.ExecuteReader())
  using(StreamWriter writer = new StreamWriter("c:\temp\file.txt"))
  {
    while(reader.Read())
    {
      // Using Name and Phone as example columns.
      writer.WriteLine("Name: {0}, Phone : {1}", 
        reader["Name"], reader["Phone"]);
    }
  }
}

谢谢。我应该使用哪种写入方法?StreamWriter?IO?一个例子会非常有帮助。谢谢。 - Sam
如果您想要将内容写入文件中,可以使用 StreamWriter。请查看我的更新,并参考示例。 - Anders Abel
谢谢。对于所有行,我使用以下代码:StringBuilder row = new StringBuilder(); for(int i = 0; i < myReader.FieldCount; i++){ row.Append(myReader[i]); if(i != myReader.FieldCount - 1) row.Append("\t"); } writer.WriteLine(row); - Sam
第9行缺少一个闭括号。尝试编辑,但字符数少于6个。 - StevenWhite
@StevenWhite 谢谢。已修复。 - Anders Abel

7
我想到了一个比其他答案更好的CSV编写器,内容如下:

我提出了这个,它是比其他答案更好的CSV编写器:

public static class DataReaderExtension
{
    public static void ToCsv(this IDataReader dataReader, string fileName, bool includeHeaderAsFirstRow)
    {

        const string Separator = ",";

        StreamWriter streamWriter = new StreamWriter(fileName);

        StringBuilder sb = null;

        if (includeHeaderAsFirstRow)
        {
            sb = new StringBuilder();
            for (int index = 0; index < dataReader.FieldCount; index++)
            {
                if (dataReader.GetName(index) != null)
                    sb.Append(dataReader.GetName(index));

                if (index < dataReader.FieldCount - 1)
                    sb.Append(Separator);
            }
            streamWriter.WriteLine(sb.ToString());
        }

        while (dataReader.Read())
        {
            sb = new StringBuilder();
            for (int index = 0; index < dataReader.FieldCount; index++)
            {
                if (!dataReader.IsDBNull(index))
                {
                    string value = dataReader.GetValue(index).ToString();
                    if (dataReader.GetFieldType(index) == typeof(String))
                    {
                        if (value.IndexOf("\"") >= 0)
                            value = value.Replace("\"", "\"\"");

                        if (value.IndexOf(Separator) >= 0)
                            value = "\"" + value + "\"";
                    }
                    sb.Append(value);
                }

                if (index < dataReader.FieldCount - 1)
                    sb.Append(Separator);
            }

            if (!dataReader.IsDBNull(dataReader.FieldCount - 1))
                sb.Append(dataReader.GetValue(dataReader.FieldCount - 1).ToString().Replace(Separator, " "));

            streamWriter.WriteLine(sb.ToString());
        }
        dataReader.Close();
        streamWriter.Close();
    }
}

使用方法:mydataReader.ToCsv(“myfile.csv”,true)

1
你不需要在 for (int index = 0; index < dataReader.FieldCount - 1; index++) 循环中加上 - 1。循环必须在小于 count 的情况下运行。如果 count 只有一个,那么如果加上了 - 1,它会立即结束,因为 0(index)不小于0。 - vapcguy
Rob,我非常欣赏你使用通用导出所有列和行的方法。但是,由于以下代码,我最终会得到列表中最后一个字段的双重导出: if (!dataReader.IsDBNull(dataReader.FieldCount - 1)) sb.Append(dataReader.GetValue(dataReader.FieldCount - 1).ToString().Replace(Separator, " "));能否帮助我理解这些代码的目的?否则,我打算将其从我的代码中省略掉... - laughsloudly

4

Rob Sedgwick的回答更接近实际情况,但还可以进一步改进和简化。这是我做法:

string separator = ";";
string fieldDelimiter = "";
bool useHeaders = true;

string connectionString = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

using (SqlConnection conn = new SqlConnection(connectionString))
{
     using (SqlCommand cmd = conn.CreateCommand())
     {
          conn.Open();
          string query = @"SELECT whatever";

          cmd.CommandText = query;

          using (SqlDataReader reader = cmd.ExecuteReader())
          {
                if (!reader.Read())
                {
                     return;
                }

                List<string> columnNames = GetColumnNames(reader);

                // Write headers if required
                if (useHeaders)
                {
                     first = true;
                     foreach (string columnName in columnNames)
                     {
                          response.Write(first ? string.Empty : separator);
                          line = string.Format("{0}{1}{2}", fieldDelimiter, columnName, fieldDelimiter);
                          response.Write(line);
                          first = false;
                     }

                     response.Write("\n");
                }

                // Write all records
                do
                {
                     first = true;
                     foreach (string columnName in columnNames)
                     {
                          response.Write(first ? string.Empty : separator);
                          string value = reader[columnName] == null ? string.Empty : reader[columnName].ToString();
                          line = string.Format("{0}{1}{2}", fieldDelimiter, value, fieldDelimiter);
                          response.Write(line);
                          first = false;
                     }

                     response.Write("\n");
                }
                while (reader.Read());
          }
     }
}

你需要拥有一个名为GetColumnNames的函数:

List<string> GetColumnNames(IDataReader reader)
{
    List<string> columnNames = new List<string>();
    for (int i = 0; i < reader.FieldCount; i++)
    {
         columnNames.Add(reader.GetName(i));
    }

    return columnNames;
}

2

我认为你最好使用SqlDataReader。示例如下:

StreamWriter YourWriter = new StreamWriter(@"c:\testfile.txt");
SqlCommand YourCommand = new SqlCommand();
SqlConnection YourConnection = new SqlConnection(YourConnectionString);
YourCommand.Connection = YourConnection;
YourCommand.CommandText = myQuery;

YourConnection.Open();

using (YourConnection)
{
    using (SqlDataReader sdr = YourCommand.ExecuteReader())
        using (YourWriter)
        {
            while (sdr.Read())
                YourWriter.WriteLine(sdr[0].ToString() + sdr[1].ToString() + ",");

        }
}

需要注意的是,在 while 循环中,您可以使用来自 SqlDataReader 的列数据以任何格式将该行写入文本文件。


2
保持你的原始方法,这里有一个快速的解决方案:
使用StringBuilder而不是String作为临时缓冲区。这将允许您使用函数.append(String)进行连接,而不是使用运算符+=
运算符+=特别低效,所以如果你把它放在一个循环中并且重复(潜在地)数百万次,性能会受到影响。 .append(String)方法不会破坏原始对象,因此更快。

1
我在项目中使用DataReader数据库中导出数据,使用.CSV格式。我手动读取数据并创建了一个.CSV文件。循环读取数据,将每一行的单元格值追加到结果字符串中。对于分隔列,我使用“,”,对于分隔行,我使用“\n”。最后,我将结果字符串保存为result.csv
我建议使用高性能扩展。我已经测试过它,可以快速导出600,000行的.CSV文件。

1
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。- 来自审查 - Bhavesh Odedra
谢谢您的评论。我已经完成了我的回答。 - Nigje

1
在没有使用response.Close()的情况下,使用响应对象至少会在某些情况下使写出数据的页面的HTML被写入文件。如果使用Response.Close(),连接可能会过早关闭并导致生成文件时发生错误。
建议使用HttpApplication.CompleteRequest(),但这似乎总是导致HTML被写入文件末尾。
我已经尝试过流与响应对象配合使用,在开发环境中取得了成功。但我还没有在生产环境中尝试过。

0

我使用:

private void SaveData(string path)
{
    DataTable tblResult = new DataTable();
    using(SqlCommand cm = new SqlCommand("select something", objConnect))
    {
        tblResult.Load(cm.ExecuteLoad());
    }
    if (tblResult != null)
    {
        using(FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
        {
            BinaryFormatter bin = new BinaryFormatter();
            bin.Serialize(fs, tblResult);
        }
    }
}

易于使用,易于加载,具有:
private DataTable LoadData(string path)
{
    DataTable t = new DataTable();
    using(FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
    {
        BinaryFormatter bin = new BinaryFormatter();
        t = (DataTable)bin.Deserialize(fs);
    }

    return t;
}

你也可以使用这种方法来保存 DataSet。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接