4 mar 2010

Usar FOREACH ó SELECT

Hay veces en las que no nos detenemos a pensar, cómo estamos haciendo alguna tarea, simplemente no nos cuestionamos, tal vez porque estamos ajustados con los tiempos de entrega o peor aún, porque siempre lo hemos realizado así y no nos interesa preguntarnos si esaes la mejor forma. Debemos mantener la sana costumbre de preguntarnos cómo funcionan las cosas y si lo que hacemos es la mejor manera de alcanzar el objetivo.

Pues luego de esta introducción la pregunta: Tengo un DataTable y necesito encontrar una o varias filas según un criterio dado ¿Qué es mejor: recorrerlo con un foreach o hacer un select que me devuelva un arreglo de DataRows?.
Antes de contestar nada mejor lo probamos y salimos de las dudas. La prueba debe constar de lo siguiente:

- Crear el DataTable y Llenarlo con 2 millones de registros.
- Ejecutar un SELECT del DataTable, midiendo el tiempo que tarda.
- Ejecutar un FOREACH con el mismo criterio. Midiendo el tiempo que tarda

Paso 1 Creando el datatable y llenándolo de datos


/* Creamos el datatable con dos columnas*/
DataTable dt = new DataTable();
DataColumn dc1 = new DataColumn("Col1", System.Type.GetType("System.Int32"));
DataColumn dc2 = new DataColumn("Col2", System.Type.GetType("System.String"));
dt.Columns.Add(dc1);
dt.Columns.Add(dc2);

/* Llenamos el datable con dos millones de registros*/
DataRow dr;
DateTime paso1 = DateTime.Now;
for (double i = 1; i < 2000001d; i++)
{
dr = dt.NewRow();
dr["Col1"] = i;
dr["Col2"] = "Datos de Prueba";
dt.Rows.Add(dr);
}
DateTime paso2 = DateTime.Now;
TimeSpan span = paso2.Subtract(paso1);
Console.WriteLine("total de Registros: " + dt.Rows.Count.ToString("N2"));
Console.WriteLine("Tiempo que dura la carga en milisegundos: " + span.TotalMilliseconds);


Paso 2 Setear la variable que vamos a usar la búsqueda y ejecutar el SELECT


/*Columna a encontrar*/
int var = 1234567;

/*Ejecutando el SELECT*/
paso1 = DateTime.Now;
DataRow[] arreglo = dt.Select("Col1 = " + var);
paso2 = DateTime.Now;
span = paso2.Subtract(paso1);
Console.WriteLine("Fin SELECT Total encontrados: " + arreglo.Length.ToString("N2"));
Console.WriteLine("Tiempo que duró el SELECT en Milisegundos: " + span.TotalMilliseconds);


Paso 3 Ejecutar el FOREACH


/* Ejecutando el FOREACH*/
ArrayList arraylist = new ArrayList();
paso1 = DateTime.Now;
foreach (DataRow dtr in dt.Rows)
{
if (Convert.ToDouble(dtr["Col1"]) == var)
{
arraylist.Add(dtr["Col1"]);
}
}
paso2 = DateTime.Now;
span = paso2.Subtract(paso1);
Console.WriteLine("Fin FOREACH Total encontrados: " + arraylist.Count.ToString("N2"));
Console.WriteLine("Tiempo que duró el FOREACH en milisegundos: " + span.TotalMilliseconds);


Esta es una prueba sencilla y hecha de forma apresurada, pero válida dentro de lo que necesitamos probar. El código está en una consola y está hecho en VS 2005 y en una maquina virtual con Windows 2003. El resultado de las pruebas fue el siguiente:


total de Registros: 2.000.000,00
Tiempo que dura la carga en milisegundos: 7000,0656
Fin SELECT Total encontrados: 1,00
Tiempo que duró el SELECT en milisegundos: 5638,1072
Fin FOREACH Total encontrados: 1,00
Tiempo que duró el FOREACH en milisegundos: 470,6768


Por lo que podemos ver la prueba arroja que la carga de dos millones de registros en la DataTable duro 7 segundos, ejecutar el SELECT 5 segundos y medio y el FOREACH menos de medio segundo. Se ejecutaron varias corridas y los resultados fueron muy similares. Triunfador sin discusión: el FOREACH.

Ahora, que pasa si al DataTable le ponemos una llave. Al poner una llave debemos tener en cuenta que el engine ADO.NET, debe indexar la tabla lo que se puede llevar a cabo durante la carga de información, cada vez que se añade una fila (si creamos la llave antes de carga la información) o al crear la llave si la creamos luego de la carga de datos. Para propósitos de la prueba lo coloqué después de la carga y antes de la variable de búsqueda para determinar claramente la penalización de tiempo por la indexación.


/*Creando LLAVE*/
paso1 = DateTime.Now;
DataColumn[] pk = new DataColumn[1];
pk[0] = dc1;
dt.PrimaryKey = pk;
paso2 = DateTime.Now;
span = paso2.Subtract(paso1);
Console.WriteLine("Creacion de llave en milisegundos: " + span.TotalMilliseconds);


Esta prueba arrojó el siguiente resultado


total de Registros: 2.000.000,00
Tiempo que dura la carga en milisegundos: 6789,7632
Creacion de llave en milisegundos: 5868,4384
Fin SELECT Total encontrados: 1,00
Tiempo que duró el SELECT en milisegundos: 10,0144
Fin FOREACH Total encontrados: 1,00
Tiempo que duró el FOREACH en milisegundos: 450,648


Después de varios ciclos de ejecución: el tiempo de carga se mantiene alrededor de los 7 segundos, la creación de la llave y por ende la indexación tarda entre 5.5 y 6 segundos. La ejecución del SELECT roza la instantaneidad y el FOREACH se mantiene en menos de medio segundo. Conclusión: el tiempo que tardaba la ejecución del SELECT se ha traslado a la indexación. Si hubiese más operaciones de búsqueda más adelante en el código sobre la misma datatable, puede que valiese la pena la creación de la llave pero de lo contrario prevalece la ejecución del FOREACH.

Finalmente hicimos unas pruebas utilizando un DataTableReader a partir del DataTable y recorriéndolo para obtener el mismo resultado, pero fue unas tres veces más lento que el FOREACH.

Muchas Gracias Jason y Pigo sama,