marzo 24, 2006

Consideraciones en los WebServices: WebMethods

Sabemos que cuando a los métodos públicos de un Web Service le colocamos el atributo WebMethod, lo estamos exponiendo para llamadas remotas. Para esto también es necesario tener en cuenta algunas cosillas:

Parámetros con tipos nativos. Con esto minimizamos la serialización, dado que se hace una validación automática gracias al .NET Framework.

Buffering. Por defecto, la configuración de buffer (BufferResponse) está fijada en verdadero, para asegurar que la respuesta estará en el buffer completamente antes de pasar al cliente. Esto es bueno cuando se retorna pequeños datos, pero obviamente, en grandes cantidades, es preferible deshabilitar el buffering.

[WebMethod(BufferResponse=false)]
Public string GetTextFile()
{
...
}

Almacenar los datos en caché. Siempre y cuando los datos a retornar no sean muy volátiles. Se puede especificar el tiempo –en segundos- que la respuesta estará almacenada en caché. Pero ojo, dado que el caché consume memoria del servidor, no sería apropiado si el WebMethod devuelve mucha información.

[WebMethod(CacheDuration=60)]
Public String GetDetails()
{
...
}

febrero 23, 2006

Mejorando el rendimiento de los DataSets

Usualmente cuando necesitamos trabajar con datos de una manera desconectada, nos valemos de los DataSets, llenados por un DataAdapter. Podemos seguir las siguientes recomendaciones para mejorar el rendimiento o performance de un DataSet.

La serialización de los DataSet fue mucho mejor implementada en el .NET Framework 1.1; sin embargo, conduce a cuellos de botella. Para evitar esos inconvenientes, tenemos varias alternativas:

Utilizar alias de columnas. Los datos serializados contienen los nombres de las columnas de manera que lo podemos utilizar para minimizar la serialización. De igual manera, si no vamos a necesitar los datos que se vayan modificando, se puede llamar al método AcceptChanges antes de serializar el DataSet para limpiar el buffer interno. Esto debido a que el DataSet mantiene historia de los datos originales.

Para las búsquedas, utilicemos mejor las claves primarias y Rows.Find; en lugar de DataTable.Select. La sentencia DataTable.Select no utiliza índices. Ya si nos toca efectuar búsquedas repetitivas sobre campos no indexados, es preferible utilizar un dataview, ya que se crea un índice automáticamente, lo que ayudará a mejorar el rendimiento de la aplicación.

En el caso anterior, si nos vamos por dicha estrategia, debemos utilizar el constructor del DataView que recibe como parámetros el Sort, RowFilter y RowStateFilter; de manera que nos aseguramos que el índice se crea una sola vez. En caso que creemos el dataset y luego vamos seteando las propiedades, el índice se crea por lo menos dos veces.

febrero 16, 2006

Otros tips para manejo de índices en SQL Server

Por lo general abusamos de la creación de los índices en el SQL Server. Para poder tener mejor manejo de los índices, tenemos algunos tips adicionales.

Es mejor crear los índices basados en el uso que se le van a dar. Muchas veces resulta muy tentador crear una tabla y su índice a renglón seguido, por lo general con el secuencial de por medio. O tal vez al momento que vemos una consulta muy lenta, le creamos un índice y punto. NO. Los índices pueden afectar negativamente negativamente a las operaciones de escritura. Lo mejor siempre aquí es tener conocimiento del sistema que estamos trabajando, la carga que va a tener, las consultas más conflictivas. Crear índices no es un arte, es fruto de la experiencia con el sistema.

Los índices “clustered” deben mantenerse pequeños. Esto debido a que los índices “non-clustered” almacenan la clave del índice clustered para ubicar las filas.

En los índices “clustered”, consideremos los rangos de datos. Si frecuentemente utilizamos comparativos de rangos en nuestras consultas, mediante operadores between o < ó >, sería bueno tener un índice clustered sobre el campo consultado. Es más, podríamos decir que todas las tablas deberían tener su índice clustered a menos que hayamos demostrado que afecta al performance.

Índices compuestos. Cuando creamos un índice compuesto –es decir, con varias columnas-, únicamente la primera almacena las estadísticas. Esto quiere decir que dicha columna deberá ser la más restrictiva. Si a pesar de ello el índice no es completamente selectivo, el motor no lo utilizará. Si alguna sentencia WHERE no utiliza todas las columnas del índice compuesto, es posible que el motor no utilice el índice.

Eliminar índices no utilizados. Como ya indiqué, las operaciones de escritura son afectadas por los índices. Tener índices de más, ocasionará que el sistema no responda de manera efectiva.

Index Tuning Wizard. Es una buena manera de analizar los índices, pero a mi en lo particular me gusta más basarme en la experiencia propia.

febrero 15, 2006

Common Table Expressions

Una de las ventajas que nos trae SQL Server 2005 es el uso de expresiones de tablas comunes, o common table expressions. Tratando de interpretarlo, es como un conjunto de datos resultante temporal. Es muy útil en momento de reemplazar subqueries o tablas derivadas.

En el siguiente ejemplo, vemos una CTE llamada mid, que calcula la mediana de un conjunto de datos.

WITH mid AS
(SELECT ((MAX(value) - MIN(value)) / 2)
AS midval FROM invoices)
SELECT
CASE WHEN value > mid.midval THEN 0
ELSE 1 END AS half, invoices.*
FROM invoices, mid ORDER BY half

Aquí podremos ver entonces de qué se componen los CTE. Empiezan con una cláusula WITH, y la expresión que vamos a tener la colocamos entre paréntesis. En caso que tengamos múltiples CTE's, las separamos por comas. Luego colocamos la sentencia SELECT.

La gran ventaja de esto es que si es utilizada más de una vez, nos provee muchos menos lecturas que un subquery. Por ejemplo:

WITH low AS (SELECT ((max(amount)) / 3)
AS v FROM invoices),
high AS (SELECT (2 * max(amount) / 3) AS v FROM invoices)
select id, amount, amount - low.v
FROM invoices, low, high
WHERE invoices.amount > low.v
AND invoices.amount <= high.v

SELECT id, amount,
amount - (SELECT (max(amount) / 3) FROM invoices)
FROM invoices where
amount > (SELECT (max(amount) / 3) FROM invoices) and
amount < (SELECT (2 * max(amount) / 3) FROM invoices)

Si comparamos los planes de ejecución, notaremos una mejora gracias a la CTE.

De la misma manera, nos ahorran grandes procedimientos. Por ejemplo:

WITH low AS (SELECT ((MAX(amount)) / 3)
AS v FROM invoices),
high AS (SELECT (2 * MAX(amount) / 3)
AS v FROM invoices)
SELECT id, amount, amount - low.v
FROM invoices, low, high
WHERE invoices.amount > low.v
AND invoices.amount <= high.v

En lugar de

declare @high int
declare @low int
SELECT @high = max(amount) FROM invoices
SELECT @low = min(amount) FROM invoices
SELECT id, amount, amount - @low FROM invoices

El resultado final será el mismo, pero la desventaja es que no puede ser utilizada en una vista; cosa que el CTE si puede hacer. En palabras algo más técnicas, la CTE se vuelve declarativa, mientras que el query equivalente es un procedimiento.

febrero 01, 2006

Densidad de los índices

Usualmente cuando creamos índices, pensamos siempre en aquellos que satisfacen las condiciones de la cláusula WHERE en las consultas más utilizadas. Esto no es una manera incorrecta de pensar, de hecho, es lo mejor que se puede hacer. Pero no es todo.

A parte de crear el índice de esa manera, debemos de pensar en un índice que sea altamente selectivo. ¿Qué es eso? Un índice altamente selectivo tiene gran cantidad de valores distintos. Por ejemplo, un índice en una tabla de clientes, sobre un campo “flag”, tendrá solamente 2 valores distintos (sí/no). Sin embargo, un índice por el número de identificación, puede tener muchísimos. En el primer caso, es muy probable que el motor del SQL Server no lo utilice, dado que no es muy selectivo.

Afortunadamente, se tiene el comando DBCC SHOW_STATISTICS que permitirá entender la selectividad de un índice. Como salida de este comando, tenemos una columna Density, la misma que se calcula dividiendo 1 para el número de valores distintos. De esta manera, un índice único tendrá una densidad de 1/número de filas. Así, una tabla de 1000 filas tendrá una densidad de 0.001. En el caso anterior, el índice sobre la columna “flag”, tendrá una densidad de 0.5. Entonces, mientras más pequeño sea el número, mayor será la selectividad.

El mejor número será el que devuelve la columna All Density.

Más información en: Statistics Used by the Query Optimizer in SQL Server 2000.

enero 27, 2006

Transacciones: todo un dolor de cabeza

Este artículo aplica a SQL Server 2000.

Si de verdad te interesa mejorar enormemente el rendimiento de tu aplicación, es importante que te enfoques en manejar eficientemente las transacciones. Las transacciones bloquean recursos de manera que pueden bloquear otras transacciones. Algunos trucos efectivos al momento de manejar transacciones los voy a listar a continuación, esperando que les sean lo más útil posible.

Evitar transacciones de larga duración. Dado que las transacciones efectúan bloqueos, lo más barato es mantener las transacciones lo más cortas posibles. Igual, se puede iniciar transacciones en la capa de aplicación. Una técnica bastante aceptada –y que me encanta utilizar- es realizar todas las validaciones previas, antes de iniciar la transacción. Quizá debas volverlas a verificar durante la transacción, pero la ventaja es que se puede evitar que por una de esas condiciones tengas que hacer un rollback en media transacción.

Nunca, jamás, utilizar transacciones que requieran de ingresos del usuario. Nada más que agregar.

Los datos más utilizados deberán ser accedidos al final de la transacción. Es mejor que todas las operaciones de lectura, sean colocadas al inicio de la transacción; las escrituras al final; y los recursos más accedidos al último. ¿Adivinaste? La idea es que los bloqueos son más cortos en los recursos más accedidos. Adiós a los bloqueos largos.

Acceder a los recursos en el mismo orden. Los deadlocks son las cosas mas molestas en la base de datos. Para evitarlos, debemos de tratar de utilizar los recursos en el mismo orden, en todo nuestro sistema. Si no lo hacemos corremos alto riesgo de enfrentarnos a escenarios de deadlock bastante frecuentes. Conseguir un sistema sin deadlocks es una tarea casi imposible; pero el objetivo deberá ser tratar de llevarlos al mínimo, reduciendo el tiempo en el que los bloqueos son efectuados. Esto incluso podríamos tratarlo en el futuro. Hay bastante que aprender acá.

Utilizar los hints de aislamiento (isolation). Si la lógica del negocio lo permite, podríamos llevar el nivel de aislamiento más bajo. Lo más común es utilizar el hint WITH NOLOCK en las sentencias select.

Las transacciones explícitas deberán de hacer commit o rollback. Se debe tratar que exista el manejo de errores adecuado, de manera que se hagan el commit o el rollback necesario. Sino jamás se cerrará la transacción. Si se tienen sospechas de alguna transacción abierta, se puede utilizar la sentencia DBCC OPENTRAN, que devolverá las transacciones abiertas.

enero 23, 2006

Sobre la administración de excepciones (2)

¿Sabías que puedes monitorear la cantidad de excepciones que tu aplicación está lanzando? Se puede hacer de dos maneras sencillas:

- Monitoreando periódicamente el log de excepciones, siempre que se haya implementado código para manejo de excepciones (similar a lo indicado en la entrada anterior).
- Con el Performance Monitor de Windows, se puede ver el valor del contador # of Exceps Thrown / sec que se encuentra en el objeto .NET CLR Exceptions. Digamos que para indicar un manejo adecuado, este valor deberá ser menor que el 5% de tus solicitudes por segundo.

Otra manera de evitar que las excepciones afecten al rendimiento de una aplicación, es hacerse cargo de aquellos recursos que son tomados y que al ocurrir una excepción podrían eventualmente quedar en memoria. Para esto pues utilizamos la cláusula finally. Con esto no aseguraremos que los recursos son liberados una vez ocurrida la excepción. De la siguiente manera:

try
{
conn.Open();

}
finally
{
If (null !=conn) conn.Close();
}

Y lo más importante, escribir código que evite las excepciones. Por ejemplo.

Verificar valores nulos. En caso de ser posible que un objeto tenga el valor null, habría que asegurarse que no es nulo, en lugar de lanzar una excepción. Esto comúnmente sucede cuando se tratan de consultar ítems del session state, view state, application state, u objetos del cache. Por ejemplo, NO conviene utilizar el siguiente código para acceder información a un objeto de sesión:

try{
loginid = Session[“loginid”].ToString();
}
catch (Exception ex){
Response.Redirect(“login.aspx”,false);
}


En su lugar, bien podríamos utilizar

if (Session[“loginid”]!=null)
loginid = Session[“loginid”].ToString();
else
Response.Redirect(“login.aspx”,false);


No utilizar las excepciones para controlar lógica. Las excepciones son eso – excepciones. Una falla en la conexión a una base de datos es una excepción. Un usuario que se equivoca en digitar el password es una mera condición que necesita ser manejada. Por ejemplo, el siguiente código:

public void Login(string UserName, string Password) {}

El siguiente código se utiliza para llamar al login.

try
{
Login(userName, password);
}
catch (InvalidUserNameException ex)
{…}
catch (InvalidPasswordException ex)
{…}


Es mucho mejor crear un enumerado de los posibles valores y cambiar el método login para que retorne la enumeración; algo así:

public enum LoginResult
{
Success, InvalidUserName, InvalidPassword, AccountLockedOut
}
public LoginResult Login(string UserName, string Password) {}


Y utilizar el siguiente código para llamar a Login:

LoginResult result = Login(userName, password);
switch(result)
{
case Success: …
case InvalidUserName: …
case InvalidPassword: …
}


Suprimir las llamadas internas a Response.End. Los métodos Server.Transfer, Response.Redirect y Response.End, levantan excepciones. Cada uno de ellos llama internamente al método Response.End. La llamada a Response.End, provoca una excepción ThreadAbortException. Si utilizas Response.Redirect, mejor utiliza el método recargado y pasa false al segundo parámetro para evitar la llamada interna a Response.End. Más información en: PRB: ThreadAbortException Occurs If You Use Response.End, Response.Redirect, or Server.Transfer.

No atrapar excepciones que no estamos manejando. Si el código no puede manejar una excepción, utiliza un bloque try/finally para asegurar que se cierran los recursos, aún si la excepción ocurre o no. No atrapes la excepción si no se intenta recuperar; antes bien, permítele propagarse para un manejador adecuado que pueda “lidiar” con la excepción encontrada.