什么是从DbDataReader读取数据的最快方法?

24

在下面的代码中,command是一个已经设置好的DbCommand:

using( var dataReader = command.ExecuteReader() /*The actual execution of the query takes relatively little time.*/ ) {
                while( dataReader.Read() ) {
                    // These are what take all of the time. Replacing them all with reader.GetValues( myArray ) has no impact.
                    val0 = dataReader.GetValue( 0 );
                    val1 = dataReader.GetValue( 1 );
                    val2 = dataReader.GetValue( 2 );
                }
            }

我目前正在处理的查询中,大部分时间都花在了执行 GetValue 调用上。每次 GetValue 调用是否都会向数据库发出一次请求?看起来是这样,并且这似乎非常低效。正如代码注释所述,尝试使用 GetValues() 一次性完成并没有任何区别。是否有一种方法可以一次性获取整行数据呢?更好的方法是,是否有一种方法可以一次性获取整个结果集呢?

谢谢。


不写实现,IDataReader和/或DbDataReader以及“GetStrongScalarType(ordinalNumber)”是更快的。 GetString,GetInt32等和0、1、2或序数。在数据末尾,填充DataTable或DataSet或大多数ORM都使用IDataReader和/或DbDataReader,并使用序数。ORM可以使用(“byStringColumnName”),但它们通常(一次)将其映射到序数并缓存以进行重复调用。示例ORM:https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Driver/NHybridDataReader.cs - granadaCoder
1
不要使用“GetValue”。为了获得最佳性能,请使用具体的GetString、GetInt32等方法。 - granadaCoder
6个回答

43

我自己用不同的方法进行了一些基准测试:

public DataTable Read_using_DataTable_Load(string query)
{
    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = query;
        cmd.Connection.Open();
        var table = new DataTable();
        using (var r = cmd.ExecuteReader())
            table.Load(r);
        return table;
    }
}

public DataTable Read_using_DataSet_Fill<S>(string query) where S : IDbDataAdapter, IDisposable, new()
{
    using (var da = new S())
    {
        using (da.SelectCommand = conn.CreateCommand())
        {
            da.SelectCommand.CommandText = query;
            DataSet ds = new DataSet();
            da.Fill(ds);
            return ds.Tables[0];
        }
    }
}

public IEnumerable<S> Read_using_yield_selector<S>(string query, Func<IDataRecord, S> selector)
{
    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = query;
        cmd.Connection.Open();
        using (var r = cmd.ExecuteReader())
            while (r.Read())
                yield return selector(r);
    }
}

public S[] Read_using_selector_ToArray<S>(string query, Func<IDataRecord, S> selector)
{
    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = query;
        cmd.Connection.Open();
        using (var r = cmd.ExecuteReader())
            return ((DbDataReader)r).Cast<IDataRecord>().Select(selector).ToArray();
    }
}

public List<S> Read_using_selector_into_list<S>(string query, Func<IDataRecord, S> selector)
{
    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = query;
        cmd.Connection.Open(); 
        using (var r = cmd.ExecuteReader())
        {
            var items = new List<S>();
            while (r.Read())
                items.Add(selector(r));
            return items;
        }
    }
}

1和2返回DataTable,而其余的则返回强类型结果集,因此它们并不完全相同,但我会按照它们的实际情况进行计时。

只提取关键部分:

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 100; i++)
{
    Read_using_DataTable_Load(query); // ~8900 - 9200ms

    Read_using_DataTable_Load(query).Rows.Cast<DataRow>().Select(selector).ToArray(); // ~9000 - 9400ms

    Read_using_DataSet_Fill<MySqlDataAdapter>(query); // ~1750 - 2000ms

    Read_using_DataSet_Fill<MySqlDataAdapter>(query).Rows.Cast<DataRow>().Select(selector).ToArray(); // ~1850 - 2000ms

    Read_using_yield_selector(query, selector).ToArray(); // ~1550 - 1750ms

    Read_using_selector_ToArray(query, selector); // ~1550 - 1700ms

    Read_using_selector_into_list(query, selector); // ~1550 - 1650ms
}

sw.Stop();
MessageBox.Show(sw.Elapsed.TotalMilliseconds.ToString());

查询返回了大约1200行和5个字段(运行了100次)。除了Read_using_Table_Load外,所有的表现都很好。

在所有方法中,我更喜欢Read_using_yield_selector,它可以按需惰性地返回数据。如果您只需要枚举数据,则这对于内存非常有用。如果您希望将集合复制到内存中,则最好使用Read_using_selector_ToArrayRead_using_selector_into_list


1
+1 - 有趣,尤其是 DataTable.Load()IDataAdapter.Fill() 之间的比较。我原以为数据适配器与直接加载到 DataTable 相同或更慢,但看起来它快了4-5倍?有什么想法是为什么吗? - Tim M.
1
@TimMedora:你需要在 DataTable.Load() 周围加上 .BeginLoadData().EndLoadData() 才能达到与 DataSet 相同的速度。 - Nikola Bogdanović
大家好,有人能解释一下如何在上面的Read5<S>(string query, Func<IDataRecord, S> selector)中传递“Func<IDataRecord, S>”参数吗? - nativegrip
1
@WinodPatel 你可以这样做:var users = Read5("select * from User", r => new User { Id = r[0], Name = r[1] })。希望清楚明白。 - nawfal
是的,Nawfal,我确实想要它。 非常感谢。 - nativegrip
显示剩余3条评论

4

我会使用类似 dapper-dot-net 这样的工具将其加载到基本类型模型中;这是一个微型对象关系映射器(micro-ORM),因此您可以获得元编程的好处(如高效预生成的IL等) - 而不会像EF或DataTable一样带来额外的开销。


4
我认为这不会对他关于数据读取器的具体问题有所帮助。Dapper在内部使用DbDataReader.GetValue。 - William Gross

3
using (connection)
    {
        SqlCommand command = new SqlCommand(
          "SELECT CategoryID, CategoryName FROM dbo.Categories;" +
          "SELECT EmployeeID, LastName FROM dbo.Employees",
          connection);
        connection.Open();

        SqlDataReader reader = command.ExecuteReader();

        while (reader.HasRows)
        {
            Console.WriteLine("\t{0}\t{1}", reader.GetName(0),
                reader.GetName(1));

            while (reader.Read())
            {
                Console.WriteLine("\t{0}\t{1}", reader.GetInt32(0),
                    reader.GetString(1));
            }
            reader.NextResult();
        }
    }

8
这并不比我在原问题中发布的代码更快(速度大致相同),但我认为它是所有答案中最快的。至少现在我们知道没有一种我错过的更快的方法。 - Greg Smalter

1
你可以使用 DbDataAdapter 来获取所有结果并将它们存储在 DataTable 中。

1
        Dim adapter As New Data.SqlClient.SqlDataAdapter(sqlCommand)
        Dim DT As New DataTable
        adapter.Fill(DT)

-1
使用Untyped DataSet。就我所知,这是最快的。

为什么你认为它是“最快”的?你没有提供任何关于这个答案的推理。 - Kevin B Burns

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