Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Dada la sugerencia de Sergio Tarrillo, en este artículo mediremos la lectura de datos de una base de datos con ADO.NET, incluyendo la carga de una lista genérica List<> de objetos de entidad.Este artículo es una continuación del artículo anterior Anti Prácticas .NET: Lectura de Datos con ADO.NET

Presentación del escenario

Este es el contexto en el que estoy haciendo las mediciones:Una aplicación Windows Forms, que utiliza 3 mecanismos para recuperar datos “de solo lectura” de la base de datos AdvertureWorks alojada en SQL Server 2005: 

  • DataReader cargado en una lista genérica de objetos de entidad
  • DataSet
  • DataTable Aquí subrayo “solo lectura” porque, justamente solo quiero recuperar los datos, y no hacer ninguna operación sobre ellos.

El Código

La versión completa del código podrás bajarla de aquí.  De todas formas démosle un vistazo:
Esta es la sentencia sql a ejecutar en la base de datos AdventureWorks:

Select
HumanResources.Employee.EmployeeID, Person.Contact.FirstName
       Person.Contact.MiddleName, Person.Contact.LastName,
       HumanResources.Employee.Title, HumanResources.Employee.BirthDate,
       Person.Address.AddressLine1, Person.Address.AddressLine2,
       Person.Address.City, Person.Address.PostalCode, Person.Contact.EmailAddress,
       Person.Contact.Phone, HumanResources.Employee.MaritalStatus, HumanResources.Employee.Gender
       FROM HumanResources.Employee
       INNER JOIN  Person.Contact
          ON HumanResources.Employee.ContactID = Person.Contact.ContactID
       INNER JOIN HumanResources.EmployeeAddress
          ON HumanResources.Employee.EmployeeID = HumanResources.EmployeeAddress.EmployeeID
       INNER JOIN Person.Address
          ON HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID
          AND  HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID

y la clase DataAccess:

namespace Walzer.Antipracticas{
    public class DataAccess
    {
        static readonly string _connString;
        static readonly string _sqlCmd;

        static DataAccess()
        {
            _connString = "Password=;User ID=;Initial Catalog=AdventureWorks;Data Source=WALZER3";
            //Obtengo la sentencia SQL que está en el archivo de texto Consulta.sql
            StreamReader sr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Walzer.Antipracticas.Consulta.sql"));
            _sqlCmd = sr.ReadToEnd();
        }

        static public DataSet TraerDataSet()
        {
            DataSet ds = null;
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    SqlDataAdapter da = new SqlDataAdapter(cmd);
                    ds = new DataSet();
                    da.Fill(ds);
                }
            }
            catch {}
            return ds;
        }

        static public List<Employee> TraerEmployees()
        {
            List<Employee> employees = new List<Employee>();
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    conn.Open();
                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Employee employee = new Employee();
                            employee.EmployeeID = Convert.ToInt32(dr["EmployeeId"]);
                            employee.FirstName = Convert.ToString(dr["FirstName"]);
                            //Por motivos de espacio obvié las lineas restantes…
                            employees.Add(employee);
                        }
                    }
                }
            }
            catch {}
            return employees;
        }

       
static public DataTable TraerDataTableOptimizado()
        {
            //Este método está optimizado para cargar un DataTable con datos de SOLO LECTURA
            DataTable dt = null;
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    SqlDataAdapter da = new SqlDataAdapter(cmd);
                    dt = new DataTable();
                    da.Fill(dt);
                }
            }
            catch {}
            return dt;
        }
     }
} 

Comparación de Técnicas

Lo que primero vamos a medir es el tiempo que consume cada uno de las técnicas de lectura de datos. Para ello recordemos que la consulta devuelve 290 regitros y que ejecuto 10 veces cada método.

Podemos observar entonces que no hay mayor diferencia entre las técnicas.  Claro que ya hemos optimizado en el artículo anterior la lectura del DataTable.

Optimización de la carga de una lista genérica de objetos de entidad con DataReader

De todas formas sería bueno revisar el método TraerEmployees, que carga una List<Employee> para ver si le cabe alguna optimización.

 Encontramos aquí que tenemos 446 ms. + 74 ms. en 34800 + 5800 invocaciones a GetOrdinal() debido a la forma en que recuperamos el valor de la columna dr["nombreCampo"]. GetOrdinal devuelve la posición de la columna para ese nombre campo, y esto es muy conveniente.  No es recomendable reemplazar el nombre de la columna por el número de la misma al recuperar el dato, ya que cualquier cambio en la consulta SQL invalidaría nuestro código.  Lo que debemos hacer entonces es minimizar la cantidad de llamadas. Quitemos la invocaciones a GetOrdinal() de adentro del bucle.

static public List<Employee> TraerEmployeesOptimizado1()
{
    List<Employee> employees = new List<Employee>();
    try
    {
        using (SqlConnection conn = new SqlConnection(_connString))
        {
            SqlCommand cmd = new SqlCommand();
            cmd.CommandText = _sqlCmd;
            cmd.Connection = conn;
            cmd.CommandType = CommandType.Text;
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader())
            {
                int colEmployeeId = dr.GetOrdinal("EmployeeId");
                int colFirstName = dr.GetOrdinal("FirstName");
                //Por motivos de espacio obvié las lineas restantes…
                while (dr.Read())
                {
                    Employee employee = new Employee();
                    employee.EmployeeID = Convert.ToInt32(dr[colEmployeeId]);
                    employee.FirstName = Convert.ToString(dr[colFirstName]);
                    //Por motivos de espacio obvié las lineas restantes…
                    employees.Add(employee);
                }
            }
        }
    }
    catch {}
    return employees;
}

Revisemos el profiling de código del código optimizado:



Hemos bajado el tiempo de 2631 ms. a 2132 ms. y reducido las invocaciones a GetOrdinal() a las necesarias: 14 invocaciones -> 14 campos.La segunda reducción que podríamos hacer corresponde con get_MetaData(), la cual se invoca por registro y por columna. La intención de minimizar esta llamada es la siguiente: ¿Para qué leer la “metadata” una y otra vez si el esquema de los datos no varía en un set de resultados de solo lectura como es el DataReader? ¿Cómo podemos hacer para reducir la cantidad de invocaciones?. Bien, mi sugerencia es tratando de reducir las invocaciones a GetValue() que internamente llama a get_MetaData(), reemplazándola por GetValues(object[]), la cual lee los datos del registro de una vez y devuelve un vector con los resultados.

static public List<Employee> TraerEmployeesOptimizado2()
{
    List<Employee> employees = new List<Employee>();
    try 
  
{
        using (SqlConnection conn = new SqlConnection(_connString))
        {
            SqlCommand cmd = new SqlCommand();
            cmd.CommandText = _sqlCmd;
            cmd.Connection = conn;
            cmd.CommandType = CommandType.Text;
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader())
            {
                int colEmployeeId = dr.GetOrdinal("EmployeeId");
                int colFirstName = dr.GetOrdinal("FirstName");
                //Por motivos de espacio obvié las lineas restantes…
                int colCount = dr.FieldCount;
                object[] values = new object[colCount];
                while (dr.Read())
                {
                    Employee employee = new Employee();
                    dr.GetValues(values);
                    employee.EmployeeID = Convert.ToInt32(values[colEmployeeId]);
                    employee.FirstName = Convert.ToString(values[colFirstName]);
                    //Por motivos de espacio obvié las lineas restantes…
                    employees.Add(employee);
                }
            }
        }
    }
    catch {}
    return employees;
}

Si observamos ahora el profiler vemos que las invocaciones a get_MetaData() se hacen por registro y no por columna.



Con esta segunda optimización, hemos logrado reducir el tiempo 2631 ms. a 1883 ms., disminuyendo las invocaciones a get_MetaData() de 4060 a 290. Logramos entonces una reducción del 40%.

Conclusión

Hemos comprobado que el uso correcto de las técnicas de acceso a datos en ADO.NET nos permite lograr un mayor rendimiento en nuestras aplicaciones.  También hemos aprendido algo de cómo funciona internamente ADO.NET, ejercicio que nos va a servir para tomar buenas decisiones al momento de elegir nuestra estrategia de acceso a datos.

Queda para una tercera entrega ver el costo de acceder a los datos usando los Asistentes de Visual Studio 2005.  Allí haré un resumen de lo investigado, para que tengan en cuenta al momento de diseñar su estrategia de acceso a datos.

Continuación

Parte III

Published Tue, Oct 30 2007 8:21 by cwalzer

Comments

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Tuesday, November 06, 2007 8:16 PM by sergiotarrillo

Excelente artículo Carlos, esto también deberían enseñarte al "enseñarte" ADO.NET...

Estaremos atento a la tercera entrega :).

Saludos,

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Wednesday, November 14, 2007 4:36 PM by Excelente Articulo

Muchas Gracias por tan vital e interesante aporte, realmente es muy interesante y sobre todo muy efectiva la forma en que escribiste el articulo.

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Wednesday, November 14, 2007 4:36 PM by Carlos Lone

Muchas Gracias por tan vital e interesante aporte, realmente es muy interesante y sobre todo muy efectiva la forma en que escribiste el articulo.

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Friday, November 23, 2007 9:55 AM by Jesus Alvino

Muy buen Articulo, he visto muchas maneras de Materializar  Datos mediante DataReaders, esta a mi parecer y con pruebas :P parece ser la mas eficiente, muchas gracias por el articulo :)

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Tuesday, November 27, 2007 1:14 PM by Didier

Hola, y que incoveniente habría en mapear es reader con estos metodos :

reader.GetInt32(reader.GetOrdinal("NombreCampo"))

# Serie de articulos: Cazando mitos en ADO.NET

Wednesday, December 19, 2007 6:39 AM by dotnetplanet

Excelentes Artículos de Carlos Walzer: Anti Prácticas .NET: Lectura de Datos con ADO.NET. En esta entrada se resuelve el mito: "El DataReader es más rápido que un DataSet". Se muestra a detalle un versus del uso de Da

# Cazando Mitos&#8230; ADO.net &laquo; Alexander Jim??nez

Saturday, February 02, 2008 5:46 PM by Cazando Mitos… ADO.net « Alexander Jim??nez

Pingback from  Cazando Mitos&#8230; ADO.net &laquo; Alexander Jim??nez

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Tuesday, February 05, 2008 6:05 AM by Daniel Llanos

Excelente aporte!!!, la del metadata esta muy interesante. Te felicito.

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Wednesday, April 16, 2008 3:43 PM by cwalzer

He corregido los links al código fuente.

# re: Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Tuesday, May 20, 2008 1:30 PM by Yvan

bueno esta todo claro, solo queda agradecerte por tan excelente articulo.

xD

# [Ado.Net] Clase de conexion generica para cualquier motor de base datos, usando .Net Providers

Tuesday, December 02, 2008 6:36 PM by SergioTarrillo - RichWeblog

Problema : En internet hay mucha información sobre como trabajar con Ado.Net y SQL, pero cuando tenemos

Leave a Comment

(required) 
(required) 
(optional)
(required) 
Powered by Community Server (Commercial Edition), by Telligent Systems