Grimpi IT Blog

marzo 3, 2011

Herramientas fundamentales para el desarrollo:

Filed under: .NET, Links — grimpi @ 12:01 pm

  • Notepad++. Es el excelente reemplazo al famoso UltraEdit, pero open source. Indispensable.
  • HeidiSQL: No se si es la mejor IDE de MySql gratuita que existe, pero le pega en palo. Lo mejor de todo, es que es un solo archivo .exe. No tiene dependencias ni dll ni requiere instalación.
  • .NET Reflector: De Red Gate. La mejor herramienta de reflector para .net. Hay que aprovechar a bajarla ahora, porque es gratis solo hasta el 30 de mayo del 2011.
  • Visual Studio Productivity Power Tools: Es un Add-In para Visual Studio con algunas boludeses que nos mejorar la calidad de vida. Recomendado.
  • ReSharper: No es gratis, pero para mi es fundamental. Se puede conseguir legalmente gratis si demostramos que participamos de algún proyecto open source.
  • Open DBDiff: Para comparar bases de datos SQL Server :)
  • NuGet: Es un Add-in para el Visual Studio. Ya no es necesario buscar manualmente librerías y Add-Ins. Con NuGet la vida es mas sencilla. Así de simple.
  • PDFSharp: La mejor librería open source para generar PDF en .NET. No busque otra.
  • 7-Zip: No es una herramienta de desarrollo, sino un compresor de archivos. Pero siendo open source y comprimiendo mas cualquier otro formato, no entiendo como todavía hay gente que usa el WinRar (y ni hablar de los que usan WinZip…)
  • FileZilla: El mejor cliente de FTP que existió, existe y existirá. Y encima gratis. El que no este de acuerdo con esta afirmación, que no lea mas este blog :)
  • Dependency Explorer: Nos informa que dll y componentes le esta faltando a una aplicación. Es una herramienta de los 90. Usada mucho por los desarrolladores de Visual Basic 6 y tecnologías COM. Sin embargo, mucha veces la necesitamos hoy en día.
  • Selenium: Para testing de paginas web. Imperdible.
  • Firebug: Si no usamos Chrome, el mejor Add-in de Firefox para trabajar con Javascript, CSS, Ajax, etc…
  • Regular Expression Designer: No se si es la mejor, pero es una excelente herramienta para trabajar y testear expresiones regulares. Y Gratis.
  • AnkhSVN: Cliente de SVN para Visual Studio. No puedo decir que es una excelente herramienta, porque a veces lo puteo un poco. Pero es la que mejor se integra con Visual Studio.
  • WinMerge: Excelente herramienta para comparar archivos y directorios.
  • Compumap: Nada que ver el desarrollo ni bases de datos. Simplemente quería elogiar esta excelente herramienta para buscar colectivos en Buenos Aires. Incluso funciona en Windows 3.1!!!!!

marzo 1, 2011

Filosofía del Software: De Karl Popper a TDD y Selenium

Filed under: Arquitectura, Test Driven Development, Testing — grimpi @ 8:59 pm

Karl Popper junto con Kuhn, fueron probablemente los dos epistemólogos mas importantes del siglo pasado. El aporte más significativo que le debemos a Popper, es el falsacionismo como método de constrastación de una teoría científica.
Que dice el falsacionismo? Básicamente que no es posible probar la verdad de una teoría, pero si podemos refutarla. Entonces, un enunciado científico estaría vigente siempre y cuando, no haya podido ser refutado. Por ejemplo, si mi enunciado dice “No existe vida extraterrestre”, no existe forma de comprobar si este enunciado es verdadero, pero lo aceptamos mientras no se descubra vida exterior. Pero si se descubriera, entonces estaríamos en condiciones de decir la teoría que afirmaba que no existía vida extraterrestre era falsa.
Popper insistía mucho que las teorías científicas debían intentar ser falseadas lo máximo posible. Una buena teoría, es aquella que pudo superar exitosamente muchos intentos de falsearla.
Bien, y que tiene que ver esto con el software?

Todo. El software es excelente ejemplo de la corriente falsacionista popperiana. Jamás podremos decir que un software no tiene bugs. Pero no porque somos humanos y cometemos errores y la perfección es algo divino. No podemos decir que nuestro software no tiene bugs porque no hay forma de demostrarlo afirmativamente. Sin embargo, si podemos llegar a falsear esta afirmación si encontramos errores en el software que diseñamos.
Al igual que lo que afirmaba Popper, un buen software es aquel que ha pasado exitosamente muchos intentos por refutarlo (en nuestro caso, por hacerlo fallar).
El problema que testear un software, es una tarea compleja que debe ser llevada por distintas personas. Como ser un buen popperiano mientras testeamos?

  • Uso intensivo de pruebas unitarias: No importa si usamos TDD, BDD, TAD. No importa si usamos mocking o no. No importa si usamos xUnit, VSUnit, NUnit o no. Lo mas importante de todo, es que nuestro código tenga la mayor cantidad posible de pruebas unitarias (o de integración) posibles. Y esto no significa hacer un método de prueba por método de clase, sino tal vez, hacer varias pruebas unitarias por un solo método, si este es bastante complejo.
    Si nuestra clase esta expuesta para otros desarrollos, hay que probar también que pasa cuando se le ingresan valores no esperados (parámetros nulos, valores aleatorios totalmente fuera del rango esperado, etc.).
    Muchos programadores se sienten demasiado seguros cuando tienen muchos test unitarios en “green flag”. Sin embargo, este tipo de metodología lo que mejor garantiza es que una modificación de código no produzca que otro pedazo de código deje de funcionar. Pero no es suficiente.
    Las pruebas unitarias pueden no enfocarse solamente en probar el código base (sea C#, Java, Ruby o lo que fuese), sino también se podría hacer con Javascript y objetos de una base de datos.
  • Hacer stress testing: Muchas aplicaciones son desarrolladas en entornos incorrectos con un volumen de datos muy bajo en relación con el entorno que va a existir en producción. Esto es un gravísimo error. Porque después se sube la aplicación a producción, pensando que tiene una performance aceptable y nos encontramos al poco tiempo, con que las consultas a la base no estaban optimizadas, no hay estrategia de cacheo de datos, hay métodos que hacen uso intensivo del procesador que están mal programados y nos encontramos en producción con un montón de problemas de performance que no tuvimos en cuenta por desarrollar en un entorno muy distinto al real.
    Esto suele suceder por varias razones. En primer lugar, cuando desarrollamos una aplicación de cero, no tenemos un conjunto de datos reales de producción, entonces nuestras tablas están semivacías con los datos mínimos para poder desarrollarla. Todo funciona rapidísimo, nuestros reportes vuelan. Ahora cuando la tabla pasa de tener 20 registros a 5 millones en producción, recibimos un llamado diciendo “Houston, tenemos un problema…”
    Si no tenemos datos reales para desarrollar, debemos simularlos. Esto se puede hacer manualmente o con muchas herramientas que existen en el mercado. Visual Studio 2008/2010 tiene esa posibilidad. También Red Gate tiene un producto que se llama SQL Data Generator muy bueno.
    Es lo que se llama un test de carga de datos (Load testing).
    Otra situación que nos presente inconveniente es la concurrencia. Y esto es algo mucho más difícil de simular en desarrollo. Tener una base de datos grande es fácil. Ahora como hacemos para tener 10.000 usuarios concurrentes para desarrollo? Imposible. La mejor manera de hacer esto entonces es usar herramientas de stress testing que simulen un cuadro así. En Java existe una excelente herramienta llamada JMeter. Visual Studio ofrece una herramienta parecida (Web Application Stress). Existe este excelente portal con un compilado de distintas aplicaciones open source y gratuitas de testeo muy recomendable.
    Es lo que se llama un stress testing de concurrencia (Stress testing).
  • Test de funcionalidad: Bueno, este es el punto fuerte de QA. En una organización grande no puede faltar. Se debe verificar que el sistema haga lo que esta contemplado que haga. Se debe tener un buen documento de casos de usos y que resultados se esperan. En una empresa donde el testeo es mas “casero”, una de los principios básicos es que el que desarrollo la aplicación, no debe testearla.
    Una muy buena herramienta para automatizar test funcionales de aplicaciones web es Selenium.
  • Test de UI: Esta relacionado con el test de funcionalidad, pero se dedica a buscar errores en la UI (que son mucho mas difíciles de ser detectados en una prueba unitaria). Las validaciones son correctas? Que pasa si se deja campos en blanco o se ingresan valores extremos? Se pide confirmación cuando se van a realizar una operación de borrado o critica del negocio? Como funciona en distintas configuraciones regionales y en distintas resoluciones de pantalla? Como se muestran los errores de aplicación? Como funciona en los distintos navegadores? Que pasa si deshabilito la ejecución de javascript?
    También se deben probar elementos relacionados con la seguridad. Que pasa si modifico manualmente los valores de un querystring? Esta cubierta contra ataques de SQL Injection y XSS Injection? Que pasa si una aplicación externa intenta inyectar peticiones GET y POST a nuestra aplicación?
    WaitR y WaitN (lamentablemente discontinuado) son muy buenas herramientas para automatizar este tipo de test. También podemos usar Selenium.

Links Interesantes:
Performance Testing Guidance for Web Applications
Open Source Testing

febrero 24, 2011

Breve introducción de Read Uncommitted a Snapshot Isolation

Filed under: MVCC, SQL Server — grimpi @ 6:24 pm

Otras de las dudas muy habituales para los iniciados (y no tan iniciados) en SQL Server (y también en cualquier desarrollador de cualquier motor de base de datos) es que tipo de niveles de bloqueos existen (isolation level), para que sirven, en que caso usarlos y que desventajas tiene su uso.
Por lo tanto, decidí hacer un muy breve resumen de los distintos niveles existentes. Si bien, este post especifico esta enfocado en SQL Server, conceptualmente, puede servir para cualquier motor. Los niveles de aislamiento están especificados en el estandar ANSI.

Read Uncommitted: A veces llamado “lectura sucia” o “diry read”. Una transacción ejecutada en este nivel de aislamiento, puede leer datos que están siendo modificados por otra transacción concurrente. Ejemplo: Transacción T2 hace un update sobre el registro R1, sin embargo, la transacción T1 puede leer el registro R1 modificado, a pesar de que T2 potencialmente puede hacer un roll back sobre el mismo.

  • Ventajas: No se necesita hacer un lockeo para leer datos, aunque si para poder modificarlos.
  • Contras: No se puede garantizar la consistencia de los datos leídos. Puede darse el caso de que sea lea un dato que luego no exista más.
  • Uso habitual: En situaciones de alta concurrencia donde la potencial inconsistencia de datos sea tolerada.

Read Committed: Una transacción ejecutada en este nivel de aislamiento puede leer solamente datos commiteados. Por ejemplo, si una transacción concurrente T2 hace un update sobre el registro R1, la transacción T1 no podrá acceder al mismo y va a quedar bloqueada hasta que T2 haga un commit o un rollback sobre el registro.

  • Ventajas: Buen balance entre concurrencia y consistencia.
  • Contras: Mayor nivel de bloqueo.
  • Uso habitual: Es el isolation level mas común, de hecho, el que se usa por defecto en SQL Server.

Repeatable Read: Una transacción ejecutada en este nivel de aislamiento solo puede leer información comiteada con la garantía de que mientras dure la transacción, nadie va a poder leer esos datos. Esto significa que si vuelvo a leer esos datos dentro de la transacción, tengo total seguridad de que el resultado va a ser exactamente el mismo, ya que están bloqueados para la escritura en otra transacción.
Ejemplo: Asumamos que tenemos la tabla Cuentas que guarda el balance monetario de los clientes. Empezamos la transacción T1 con filtrando todos los balances cuyo monto sea mayor a 1000. Supongamos que nos devuelve 10 registros. Ahora empieza otra transacción T2, que inserta un nuevo registro cuyo balance es de 1020 y comitea. Si consultamos nuevamente en la transacción T1 todos los balances mayores a 1000, ahora nos va a devolver 11 registros. Esto se debe a que la transacción T1 bloqueó el conjunto de registros seleccionados en la consulta, pero no el predicado del filtro.
Ahora, si dentro de la transacción T2 intento eliminar todos los registros cuyo balance sea mayor a 1000, va a ocurrir un bloqueo, ya que están lockeados por la transacción T1.

  • Ventajas: Alta consistencia en los datos.
  • Contras: Mayor nivel de bloqueos y disminución del grado de concurrencia.
  • Uso habitual: En operaciones bancarias o financieras que requieran un grado de exactitud muy alto.

Serializable: Una transacción T1 ejecutada en este nivel ofrece el mayor grado de consistencia posible, eliminado los registros fantasmas. El motor puede llegar a bloquear incluso la tabla entera, para garantizar la máxima consistencia de datos.

  • Ventajas: Consistencia de datos absoluta.
  • Contras: Altísimo grado de bloqueo y muy bajo nivel de concurrencia, ya que las tablas pueden ser bloqueadas en su totalidad mientras se realiza una transacción.
  • Uso habitual: No es muy habitual. Casos muy raros cuando se requiera una gran consistencia de datos.

Adicionalmente, existen nuevos niveles de aislamiento a partir de SQL Server 2005.

Read-Committed-Snapshot : Este no es un nuevo nivel de aislamiento, sino una reimplementación del read comitted pero sin bloqueos.
SQL Server implementa versionado de registros mediante copias (snapshots) para evita el bloqueo.

  • Ventajas: Acceso no bloqueante a datos consistentes.
  • Contras: Mayor overhead para mantener el versionado de datos.
  • Uso habitual: Minimar los bloqueos lectura/escritura y el uso de el hint NOLOCK.

Snapshot Isolation: Ya habíamos hablado acá sobre este nivel de lockeo. Una transacción bajo este nivel de aislamiento, trabaja sobre una copia (o “snapshot”) de la base de datos. Cuando finaliza la transacción, hace un merge entre la copia y la base de datos.

  • Ventajas: Mayor nivel de consistencia incluso que un “repetable red”, con mayor nivel de concurrencia, ya que no realiza lockeos.
  • Contras: Mayor overhead para mantener las distintas versiones de un conjunto de datos, lo que impacta en la performance.
  • Uso Habitual: Cuando se necesita obtener información muy consistente y en entornos de alta concurrencia.

Recordemos que para activar especificar el nivel de aislamiento, se debe ejecutar SET TRANSACTION ISOLATION LEVEL <ISOLATION>

febrero 22, 2011

Link recomendado: Stairway to SQL Server

Filed under: Links, SQL Server — Etiquetas: , — grimpi @ 9:07 pm

Para quienes estén interesados en SQL Server, recomiendo y mucho este link del excelente portal SQL Server Central. La idea del “Stairway to SQL Server” es hacer una serie de tutoriales diseñados para pasar de conocimiento cero sobre un determinado tema de SQL Server, a un nivel de conocimiento profundo que permita empezar a utilizar esa característica en un entorno de producción.
Por el momento estos son los temas tratados (aunque se van a agregar nuevos):

  • SQL Server Agent
  • Server-side Tracing
  • SQL Server Indexes
  • Transactional and Merge Replication
  • Database Design
  • MDX
  • Integration Services
  • Reporting Services
  • StreamInsight

Realmente lo recomiendo.
Para poder entrar, hay que registrarse en el portal.

febrero 8, 2011

Control de concurrencia multiversión MVCC

Filed under: Engine, MVCC, PostgreSQL, SQL Server — grimpi @ 8:08 pm

Antes de explicar que es MVCC, hay que aclarar dos conceptos muy importantes: bloqueo pesimista y bloqueo optimista. El bloqueo optimista supone que no se va a hacer nada en el código de la aplicación que imponga explícitamente bloqueos de los recursos cuando se esté trabajando con ellos. Mientras que por otro lado, el bloqueo pesimista supone una intervención por parte de la aplicación como gestor del bloqueo.
Para ser más simples: El enfoque optimista delega en la base de datos el bloqueo y manejo de datos, mientras que el bloqueo pesimista, la aplicación es la encargada de gestionar la concurrencia.
MVCC (Multi version concurrency control) es una técnica de concurrencia optimista en donde ninguna tarea o hilo es bloqueado mientras se realiza una operación en la tabla, porque el otro hilo usa su propia copia (versión) del objeto dentro de una transacción.
Si bien obviamente la implementación interna de este algoritmo es distinta en cada motor de bases de datos, a grandes rasgos se podría decir que el MVCC internamente lo que hace es identificar cada transacción con un numero univoco y a cada registro de la tabla, con un numero de versión. Entonces, cada transacción trabaja con su copia y si se modifica un registro de una tabla, el contador de versión del registro se incrementa. Cuando se comitea, la copia del objeto reemplaza a la que existía en la base de datos. Si 2 transacciones modificaron la misma tabla, se hace un mergue de ambas tablas combinando las últimas versiones de cada registro. Si las 2 transacciones, modificaron exactamente el mismo registro, entonces en ese caso, cuando se commitee, el registro que finalmente queda, corresponde a la ultima transacción en realizar una modificación sobre ese registro.
De esta manera se logra que una lectura no bloquee una transacción de escritura y que una transacción de escritura tampoco bloquee una transacción de lectura. Para ser más claros, cuando hago un SELECT, puedo simultáneamente hacer un UPDATE y cuando hago un UPDATE puedo hacer simultáneamente un SELECT, algo que en el isolation habitual de las bases de datos, no es posible.
Al no existir ningún lockeo ni espera por parte del proceso, en determinadas situaciones concurrentes de fuerte escritura donde la información es modificada frecuentemente y de manera concurrente por muchos usuarios, se logran un gran mejoramiento de la performance del sistema. Es muy habitual usar este tipo modelo de concurrencia para evitar los famosos “deadlocks” que suelen ocurrir en bases de datos con mucha demanda.

Sin embargo, MVCC no debe ser usado en cualquier ocasión, ya que tiene un overhead muy importante. Todo el manejo de versionado tiene un alto costo, ya que las versiones de registros se copian y almacenan en tablas o estructuras físicas temporales que luego se descartan (por ejemplo, para guardar las copias, SQL Server usa la tempdb, mientras que Oracle usa el rollback segment). En SQL Server este overhead es un poco alto, por lo tanto solo en escenarios específicos conviene usar este modelo de concurrencia. A medida que las operaciones de escritura sobre la base de datos cobran relevancia sobre las operaciones de lectura, y por otro lado, nuestras lecturas son largas, los beneficios de usar MVCC aumentan, ya que reducimos los bloqueos.
Es fundamental la existencia de un alto grado de paralelismo en nuestra aplicación. MVCC es por definición, un modelo muy escalable y donde mejor se ven sus ventajas, es en escenarios de alta demanda.

Escenarios típicos:
• Aplicaciones que procesan transacciones en línea en gran escala.
• Aplicaciones mixtas que usan reportes contra datos sobre tablas en línea.
• Aplicaciones donde se ejecutan consultas largas mientras que simultaneamente se realizan operaciones sobre ella.

Escenarios donde no generalmente no debería usarse MVCC:
• Aplicaciones de datawarehousing que extraen información sobre tablas históricas donde no hay escritura.
• Aplicaciones que procesan datos en línea con poca concurrencia.
• Aplicaciones realtime, donde la velocidad es mas importante aun que la consistencia de información.

Todas las bases de datos modernas implementan actualmente este algoritmo de concurrencia optimista. En SQL Server se llama SNAPSHOT ISOLATION, en MySql se llama InnoDB Multi Version, en Firebird MGA y en Oracle y PostgreSQL, simplemente MVCC.

Links:
http://msdn.microsoft.com/en-us/library/ms345124(v=sql.90).aspx
http://www.codinghorror.com/blog/2008/08/deadlocked.html
http://www.rtcmagazine.com/articles/view/101612

febrero 2, 2011

Diferencias entre Unique Index vs Unique Constraint

Filed under: SQL Server — grimpi @ 3:30 pm

La otra vez tuve una discusión con un cliente que me inspiró a hacer este post. ¿Cual es la diferencia entre una constraint UNIQUE y un índice único? Pregunta bastante común para los iniciados en bases de datos.
Y la respuesta a esta pregunta, es que en el 90% de los casos, a efectos prácticos, es lo mismo. La diferencia es más conceptual que técnica.
Tanto una constraint de este tipo como un índice único, restringen los valores de una o varias columnas, impidiendo que se repitan valores. Cuando creamos una UNIQUE constraint, internamente se crea un índice sobre esa columna.
Veamos el siguiente ejemplo:

CREATE TABLE Emp (Id int, Name varchar(20), Email varchar(50))
GO

ALTER TABLE Emp ADD CONSTRAINT IX_Emp UNIQUE (Name)
GO

Ahora consultemos a las vistas de sistema para ver que pasó.

SELECT * FROM sys.indexes WHERE name = ‘IX_Emp’
GO
 
  

 

 

 

Vea que SQL Server crea automáticamente este índice nonclustered. Está marcado como unique constraint y se le da el mismo nombre que a nuestra constraint de la tabla.

El tema es que una constraint es vista como un elemento y requisito del negocio plasmado en la tabla, mientras que un índice único es una característica interna del motor de bases de datos, pero externa a las reglas de negocio de la aplicación. La diferencia es conceptual, a nivel técnico y de performance es lo mismo, pero hace mas clara la auto documentación de la bases de datos, definir una constraint por sobre un índice único.

Sin embargo, hay una diferencia teoría técnica fundamental. Una constraint de tipo UNIQUE sobre una o varias columnas, es una restricción que aplica a toda la tabla. En cambio, un índice único, la restricción aplica solamente sobre el conjunto de registros implicados en el índice. Como en la mayoría de los casos, siempre creamos índices sobre toda la tabla, no hay diferencias, pero hay varias situaciones particulares, donde no es lo mismo:

  • Foreign key: Puedo definir una constraint UNIQUE sobre una columna y usar esa columna como foreign key de otra tabla. Pero no se puede hacer lo mismo con un índice único.
  • Índices filtrados: A partir de SQL Server 2008, es posible definir un índice no por toda la tabla, sino por un conjunto restringido de registros. Si definimos a ese índice como único, esta restricción aplicaría entonces solamente al conjunto de registros implicados en el índice, no a toda la tabla.
    Esta opción nos podría ayudar a resolver la limitación de SQL Server de que solo podamos tener una clave NULL en una restricción de unicidad.
  • Vistas indexadas: Es una situación parecida a la del índice filtrado. Podría darse el caso de tener columnas cuyos valores se repiten, una está dentro de la vista y la otra no, por lo tanto, no se podría usar una constraint única.
    También podría darse el caso de que queremos unicidad en una columna dentro de un conjunto de varias tablas.
    Podríamos crear un índice único sobre una vista que agrupe a esas tablas. Con una constraint no podríamos hacerlo, porque la restriccion solo aplica a una sola tabla.
  • Performance: También hay cuestiones de performance muy finas que en situaciones particulares, hacen más conveniente usar un índice sobre una constraint.
    Por ejemplo, si bien, como ya dijimos antes, cuando creamos una UNIQUE, se crea un índice, este índice es interno, y las posibilidades de asignarle opciones del índice cuando es creado son mucho mas limitada. Podemos decirle con que FILLFACTOR debe ser creado ese índice pero por ejemplo, no podemos crear una constraint UNIQUE cuyo índice tenga la opción INCLUDE COLUMN habilitada. También cuando crea una restricción única, no hay forma de especificar que el índice creado para soportarlo sea ascendente o descendente. Existen algunas (extrañas) situaciones, donde esto podría interesarnos.

Ejemplo:
 
 CREATE TABLE Emp (Id int, Name varchar(20))
 GO

 ALTER TABLE Emp
  ADD CONSTRAINT IX_Emp UNIQUE (Name) WITH (FILLFACTOR = 20) INCLUDE (Email);
 GO 

Si corremos este script, nos va a tirar error. Ya que SQL Server no permite la opcion INCLUDE cuando creamos una UNIQUE constraint.

Resumiendo:
En la mayoría de los casos es lo mismo usar una u otra. Pero es preferible usar una constraint por sobre un índice, porque ayuda a documentar mejor las reglas de negocio de la base, pero por otro lado, usar un índice único nos permite hacer un tunning mas fino sobre la tabla.

enero 28, 2011

Que reglas usar en el Code Analysis de Visual Studio?

Filed under: .NET, Visual Studio — grimpi @ 12:46 pm

Cuando uno descubre FxCop, la excelente herramienta de analizador de código que viene integrada en el Visual Studio 2008 y 2010, enseguida tiende a creer que todo nuestro código para que sea optimo, no debe tener absolutamente ningún warning de FxCop.
Pero al poco tiempo nos damos cuenta que tal cosa es imposible y por varias razones. La primera es que la sugerencia no es valida, que el análisis del CodeAnalysis falló. Otras veces, que es realmente intranscendente y costoso hacer el cambio (especialmente las reglas agrupadas en la categoría “naming”).
Googleando un poco descubrí este excelente post de la gente de Microsoft, con un listado de todas las reglas que ellos usan para sus propios desarrollos y testeando, me pareció una muy buena selección de reglas.

Design
CA1008 EnumsShouldHaveZeroValue
CA1009 DeclareEventHandlersCorrectly
CA1011 ConsiderPassingBaseTypesAsParameters
CA1012 AbstractTypesShouldNotHaveConstructors
CA1014 MarkAssembliesWithClsCompliant
CA1017 MarkAssembliesWithComVisible
CA1018 MarkAttributesWithAttributeUsage
CA1019 DefineAccessorsForAttributeArguments
CA1023 IndexersShouldNotBeMultidimensional
CA1025 ReplaceRepetitiveArgumentsWithParamsArray
CA1026 DefaultParametersShouldNotBeUsed
CA1027 MarkEnumsWithFlags
CA1028 EnumStorageShouldBeInt32
CA1030 UseEventsWhereAppropriate
CA1032 ImplementStandardExceptionConstructors
CA1034 NestedTypesShouldNotBeVisible
CA1036 OverrideMethodsOnComparableTypes
CA1038 EnumeratorsShouldBeStronglyTyped
CA1039 ListsAreStronglyTyped
CA1040 AvoidEmptyInterfaces
CA1041 ProvideObsoleteAttributeMessage
CA1043 UseIntegralOrStringArgumentForIndexers
CA1044 PropertiesShouldNotBeWriteOnly
CA1045 DoNotPassTypesByReference
CA1046 DoNotOverloadOperatorEqualsOnReferenceTypes
CA1050 DeclareTypesInNamespaces
CA1051 DoNotDeclareVisibleInstanceFields
CA1052 StaticHolderTypesShouldBeSealed
CA1053 StaticHolderTypesShouldNotHaveConstructors
CA1054 UriParametersShouldNotBeStrings
CA1055 UriReturnValuesShouldNotBeStrings
CA1056 UriPropertiesShouldNotBeStrings
CA1057 StringUriOverloadsCallSystemUriOverloads
CA1058 TypesShouldNotExtendCertainBaseTypes
CA1059 MembersShouldNotExposeCertainConcreteTypes

Globalization
CA1300 SpecifyMessageBoxOptions
CA1301 AvoidDuplicateAccelerators
CA1304 SpecifyCultureInfo
CA1305 SpecifyIFormatProvider
CA1306 SetLocaleForDataTypes
CA1307 SpecifyStringComparison
CA1309 UseOrdinalStringComparison
CA2101 SpecifyMarshalingForPInvokeStringArguments

Interoperability
CA1401 PInvokesShouldNotBeVisible
CA1402 AvoidOverloadsInComVisibleInterfaces
CA1403 AutoLayoutTypesShouldNotBeComVisible
CA1404 CallGetLastErrorImmediatelyAfterPInvoke
CA1405 ComVisibleTypeBaseTypesShouldBeComVisible
CA1406 AvoidInt64ArgumentsForVB6Clients
CA1408 DoNotUseAutoDualClassInterfaceType
CA1413 AvoidNonpublicFieldsInComVisibleValueTypes

Naming
CA1700 DoNotNameEnumValuesReserved
CA1701 ResourceStringCompoundWordsShouldBeCasedCorrectly
CA1702 CompoundWordsShouldBeCasedCorrectly
CA1703 ResourceStringsShouldBeSpelledCorrectly
CA1704 IdentifiersShouldBeSpelledCorrectly
CA1707 IdentifiersShouldNotContainUnderscores
CA1708 IdentifiersShouldDifferByMoreThanCase
CA1709 IdentifiersShouldBeCasedCorrectly
CA1710 IdentifiersShouldHaveCorrectSuffix
CA1711 IdentifiersShouldNotHaveIncorrectSuffix
CA1712 DoNotPrefixEnumValuesWithTypeName
CA1713 EventsShouldNotHaveBeforeOrAfterPrefix
CA1714 FlagsEnumsShouldHavePluralNames
CA1715 IdentifiersShouldHaveCorrectPrefix
CA1716 IdentifiersShouldNotMatchKeywords
CA1719 ParameterNamesShouldNotMatchMemberNames
CA1720 IdentifiersShouldNotContainTypeNames
CA1721 PropertyNamesShouldNotMatchGetMethods
CA1722 IdentifiersShouldNotHaveIncorrectPrefix
CA1724 TypeNamesShouldNotMatchNamespaces

Performance
CA1811 AvoidUncalledPrivateCode
CA1812 AvoidUninstantiatedInternalClasses
CA1813 AvoidUnsealedAttributes
CA1815 OverrideEqualsAndOperatorEqualsOnValueTypes
CA1816 DisposeMethodsShouldCallSuppressFinalize
CA1819 PropertiesShouldNotReturnArrays

Portability
CA1900 ValueTypeFieldsShouldBePortable
CA1901 PInvokeDeclarationsShouldBePortable

Reliability>

CA2001 AvoidCallingProblematicMethods
CA2002 DoNotLockOnObjectsWithWeakIdentity
CA2004 RemoveCallsToGCKeepAlive
CA2006 UseSafeHandleToEncapsulateNativeResources>

Security
CA2102 CatchNonClsCompliantExceptionsInGeneralHandlers
CA2103 ReviewImperativeSecurity
CA2104 DoNotDeclareReadOnlyMutableReferenceTypes
CA2105 ArrayFieldsShouldNotBeReadOnly
CA2106 SecureAsserts
CA2107 ReviewDenyAndPermitOnlyUsage
CA2108 ReviewDeclarativeSecurityOnValueTypes
CA2109 ReviewVisibleEventHandlers
CA2111 PointersShouldNotBeVisible
CA2112 SecuredTypesShouldNotExposeFields
CA2114 MethodSecurityShouldBeASupersetOfType
CA2115 CallGCKeepAliveWhenUsingNativeResources
CA2116 AptcaMethodsShouldOnlyCallAptcaMethods
CA2117 AptcaTypesShouldOnlyExtendAptcaBaseTypes
CA2118 ReviewSuppressUnmanagedCodeSecurityUsage
CA2119 SealMethodsThatSatisfyPrivateInterfaces
CA2120 SecureSerializationConstructors
CA2121 StaticConstructorsShouldBePrivate
CA2122 DoNotIndirectlyExposeMethodsWithLinkDemands
CA2123 OverrideLinkDemandsShouldBeIdenticalToBase
CA2124 WrapVulnerableFinallyClausesInOuterTry
CA2126 TypeLinkDemandsRequireInheritanceDemands
CA2127 SecurityTransparentAssembliesShouldNotContainSecurityCriticalCode
CA2128 SecurityTransparentCodeShouldNotAssert
CA2129 SecurityTransparentCodeShouldNotReferenceNonpublicSecurityCriticalCode

Usage
CA1806 DoNotIgnoreMethodResults
CA2207 InitializeValueTypeStaticFieldsInline
CA2208 InstantiateArgumentExceptionsCorrectly
CA2209 AssembliesShouldDeclareMinimumSecurity
CA2211 NonConstantFieldsShouldNotBeVisible
CA2213 DisposableFieldsShouldBeDisposed
CA2214 DoNotCallOverridableMethodsInConstructors
CA2216 DisposableTypesShouldDeclareFinalizer
CA2217 DoNotMarkEnumsWithFlags
CA2218 OverrideGetHashCodeOnOverridingEquals
CA2220 FinalizersShouldCallBaseClassFinalizer
CA2221 FinalizersShouldBeProtected
CA2224 OverrideEqualsOnOverloadingOperatorEquals
CA2225 OperatorOverloadsHaveNamedAlternates
CA2227 CollectionPropertiesShouldBeReadOnly
CA2228 DoNotShipUnreleasedResourceFormats
CA2229 ImplementSerializationConstructors
CA2230 UseParamsForVariableArguments
CA2233 OperationsShouldNotOverflow
CA2234 PassSystemUriObjectsInsteadOfStrings
CA2235 MarkAllNonSerializableFields
CA2236 CallBaseClassMethodsOnISerializableTypes
CA2237 MarkISerializableTypesWithSerializable
CA2240 ImplementISerializableCorrectly

enero 25, 2011

Lo nuevo de SQL Server 2011: COLUMN STORAGE INDEX

Filed under: Engine, SQL Server, SQL Server 2011 — grimpi @ 4:28 pm

Una de las características más importantes que promete SQL Server 2011, son los índices por columna.

Introducción
A partir del 2007, salieron a la luz distintas bases de datos llamadas “Column-oriented DBMS”, que implementaron el concepto de índice por columna, el cual prometía revolucionar el mercado, especialmente el de datawharehousing. Uno de los motores mas conocidos es Vertica (y su versión gratis, C-Store), también MonetDB, que es open source. Incluso Sybase lanzó un producto llamado Sybase IQ. Por supuesto, hubo otras empresas que lanzaron productos similares.
El problema de estos productos es que son “Column-oriented DBMS” exclusivamente. O sea, no almacenan la información de la misma manera que la almacena una típica base de datos relacional tradicional como Oracle, MySql o SQL Server. Esto significa que el mercado de estos productos está muy limitado a escenarios específicos.

Por el contrario, SQL Server 2011 promete ser la primera de bases de datos relacional “tradicional” en incorporar como una feature mas a su motor, el índice por columna, pudiendo ser combinado su uso con la tradicional estructura “row-store”.
 

¿Ahora bien, que es un índice por columna?
En todos los motores relacionales, los índices se almacenan por filas (rows). Esto significa que la estructura del índice en su última instancia, va a tener un puntero a un registro de una tabla. Existen distintas maneras de construir un índice (bitmap, b-tree, etc), pero todos estos métodos en definitiva hacen lo mismo: acelerar el acceso a un registro.
El índice por columna por el contrario, invierte la filosofía. Ya no busca registros, busca columnas. En una tabla “normal”, los registros son almacenados en páginas físicas de disco. Cada página contiene una determinada cantidad de registros (que varia según el motor y los registros. En SQL Server cada página de datos tiene un tamaño fijo de 8K). Un índice por columna, es al revés. En cada página de datos, se guardan columnas, no filas.

¿Bárbaro, ahora que ventajas tiene esto?
La respuesta es simple. Performance. Y por 2 razones.

1) Al estar las páginas de disco ordenadas por columnas, cuando hago una consulta, el motor puede leer solo las columnas que necesitamos para nuestra query, lo cual evita lecturas de disco innecesarias.
2) Es muchísimo mas probable que exista redundancia de información entre los valores de una misma columna en distintos registros, que entre los valores de las distintas columnas en un mismo registro, por lo tanto, es mucho mas fácil y eficiente la compresión de datos, lo que significaría mas información en memoria y menos acceso al disco. Ejemplo:
Si yo tengo 4 registros de 4 columnas cada uno:

  Col1 Col2 Col3 Col4
Row 1 01 Jose 01/01/2000 4
Row 2 02 Eduardo 02/02/2004 5
Row 3 03 Ramon 03/02/2004 5
Row 4 04 Susana 02/02/2004 5

Si yo comprimo esta información por columna, mi grado de compresión seria mucho mayor a que si yo comprimo por registro. Porque el grado de homogenización de valores dentro de una columna es mucho mayor. En cambio, en un registro, como tengo distintos tipos de datos y valores posibles entre ellos muy disímiles, la compresión no es tan efectiva. No es lo mismo comprimir 100 valores de un mismo tipo de datos, que comprimir 100 valores de distintos tipos de datos.
Por otro lado, existen diferentes técnicas de compresión que funcionan mejor con un tipo de datos que con otro (ejemplo: hay métodos que son más eficientes para números o binarios, y otros métodos que son más eficientes para texto). Por lo tanto, el motor elige una forma de compresión diferente dependiendo del tipo de dato de la columna, algo que es imposible de hacer en una compresión por row, lo que incrementa aun más el nivel de compresión.

Un gran ratio de compresión me permite tener más información en el cache de datos, incrementar fuertemente los aciertos del buffer y evitar el acceso a I/O, que es la operación más costosa en una base de datos.
En las pruebas hechas por los laboratorios de Microsoft sobre datos reales y haciendo la comparativa con SQL Server 2008 R2, en algunos casos se obtuvo un incremento de hasta 60 veces la performance de una consulta. Lo cual resulta un número bastante impresionante.

¿Como se usa un índice por columna?
Si bien todavía la documentación oficial en SQL Server 2011 no está disponible porque el producto no fue liberado, si podemos ver la sintaxis si entramos a la web del sitio.
Para crear un índice por columna simplemente deberemos anteponer la palabra clave COLUMNSTORE.
Ejemplo:

 
CREATE COLUMNSTORE INDEX NuevoIndice ON Personas
(
FechaDeVenta,
FechaDeEntrega,
Producto,
Precio,
Cantidad
)
 

¿Siempre va a convenir entonces usar este tipo de índices?
La respuesta es un rotundo no. Los índices por columna es un índice más que incrementa notablemente la performance en bases de datos muy grandes y en determinado tipo de consultas. Pero son índices pensados para escenarios de datawharehousing, especialmente para consultas del tipo star join. No en todos los casos es mas óptimo este tipo de índice ni tampoco están pensados para las típicas bases de datos transaccionales OLTP. Para una query normal, sigue siendo mas útil usar los índices b-tree clásicos. El motor de SQL Server en función de sus estadísticas, sabrá elegir cuando es más conveniente usar un índice por columna o un índice común.
Por otra parte, este tipo de índices en SQL Server tienen por el momento varias restricciones. No todos los tipos de datos son soportados y más importante aún, no se pueden hacer operaciones de INSERT, UPDATE, DELETE o MERGE sobre tablas con este tipo de índices. La manera de llenar una tabla entonces es deshabilitar el índice temporalmente, llenar la tabla y volver a habilitar el índice. Otra opción es crear una vista indexada con columnstore index sobre la tabla y dejar a esta tabla sin este tipo de índice.
Por ultimo, otro dato a tener en cuenta, que la reconstrucción de un column index lleva entre 2 y 3 veces mas tiempo, que un índice b-tree clásico.

Conclusión:
Lamentablemente la primera CTP de SQL Server 2011 que lanzó Microsoft todavía no incluye esta feature, por lo tanto, no la podemos probar y verificar si el aumento de su rendimiento es tan significativo como ellos dicen. Sin embargo, el índice por columna es una característica muy distintiva y que hasta el momento, ni Oracle ni DB2 pudieron incorporar nativamente en su producto relacional.
Seguramente con el correr de los tiempos y a medida que tengamos mas información, van a aparecer en blogs y en artículos, distintas comparaciones y escenarios en donde conviene utilizar índices por columnas o el clásico índice por filas.

Links interesantes:
ColumnStores vs. RowStores: How Different Are They Really?
Columnstore Indexes for Fast Data Warehouse Query Processing in SQL Server 11.0

enero 18, 2011

Lo nuevo en SQL Server 2011

Filed under: SQL Server, SQL Server 2011, T-SQL — grimpi @ 2:02 pm

Estuve investigando SQL Server 2011, llamada “Denali”. Actualmente está en estado beta y se puede bajar la CTP desde acá. Antes de bajarla, para poder instalar esta CTP se requiere como minimo Windows Vista SP2. No funciona ni en Windows XP ni en Windows 2003. De todas maneras parece bastante prometedora.
Que tiene de nuevo esta nueva versión? Obviamente muchas, pero hay 2 cosas que realmente me gustaron:

1) Objetos sequences: Quienes trabajen con Oracle o PostgreSQL, sabrán perfectamente que son. Para el que no lo sabe, un sequence es un objeto cuyo valor se autoincrementa cada vez que es consultado.
Es un excelente reemplazo de los identities, ya que es un objeto externo a la tabla.
Dentro de poco veremos las flamewars en blogs argumentando si es mejor o peor usar sequences o identities como campo PK de una tabla.
Sin embargo, la incorporación de este objeto facilitara enormemente la migración de Oracle y PostgreSQL a SQL Server.
Ejemplo:

CREATE SEQUENCE PrimerSequence
START WITH 1
INCREMENT BY 1;
GO

SELECT (NEXT VALUE FOR PrimerSequence)
GO

INSERT INTO Tabla (Id, Nombre)
SELECT NEXT VALUE FOR PrimerSequence, ‘Esteban’
GO

Link:
http://msdn.microsoft.com/en-us/library/ff878058(v=SQL.110).aspx

La única contra que le veo, es la forma en que SQL Server implementó la lectura de los sequences. Escribir NEXT VALUE FOR xxxx es demasiado texto para simplemente para leer un sequence.
En Oracle o en PostgreSQL resulta más legible y compacto hacer lo mismo.

2) Paginación: Ya no hay que inventar subqueries y cosas raras para paginar datos. Con SQL Server 2011 va a ser posible paginar nativamente una query declarando un OFFSET en la cláusula ORDER BY.
Habrá que investigar la performance de esto, pero parece prometedor.
Ejemplo:

SELECT Id, Campo1, Campo2 FROM Tabla
ORDER BY
OFFSET 0 ROWS
FETCH NEXT 10 ROWS ONLY
GO

Maravilloso. Lo único que no entiendo es porque decidieron poner en el ORDER BY la paginación y no en la instrucción TOP, que pareciera ser lo mas lógico. Después de todo se supone que el ORDER BY solo se limita a ordenar datos y no a limitar la cantidad de registros a devolver.
Además lo hicieron demasiado trabajoso. Demasiadas palabras claves para algo tan simple. Hay que escribir OFFSET, ROWS, FETCH, NEXT, ONLY, ROWS nuevamente solamente para limitar un resultado. MySql lo resuelve de manera mucho mas elegante y practica con su cláusula LIMIT (0,10). Simple, claro y elegante.

Link:
http://msdn.microsoft.com/en-us/library/ff878058(v=SQL.110).aspx

ACTUALIZACION: Me acabo de enterar que la manera en que SQL Server implementó la paginacion corresponde a la especificación ANSI SQL:2008.

Hay otras cosas interesantes en SQL Server 2011.  Se agregó un nuevo tipo de indice, llamado “columnstore”, pensado especialmente para escenarios de datawharehousing. Se mejoró notablemente el Full Text Search, la IDE cambió, se agregaron snippets, varias mejoras en BI y SSIS.
Seguramente con el correr de los meses, se irán incorporando nuevas características. Pero ya el hecho de tener paginación nativa y sequences, representa un gran avance para los desarrolladores y en mi opinion, el nuevo indice por columnas en algunos escenarios, va a ser imprescindible. Pero esto lo voy a explicar en otro post.

enero 14, 2011

Identity avanzado en SQL Server

Filed under: Identitys, SQL Server — grimpi @ 4:11 pm

Hace un tiempo escribí un post sobre como trabajar con las columnas identity. Bueno, acá viene la segunda parte con tips un poco mas avanzados.

1) Modificar una columna para que sea identity:
No se puede. SQL no permite modificar la propiedad Identity de una columna, ya sea para habilitar o deshabilitar esa propiedad. No existe algo asi como un ALTER COLUMN Nombre_Campo SET IDENTITY ON (Como dato curioso, Sybase si tiene esta posibilidad). Pero muchos diran, ¿Como no puede ser posible, si yo lo hago desde el enterprise manager?. Bueno, lo que hace ese programa cuando se habilita (o deshabilita) la propiedad identity a una columna existente, es reconstruir toda la tabla. Esto significa borrar todas las dependencias de una tabla, crear una tabla auxiliar similar a la tabla original pero con la propiedad identity cambiada, copiar todo el contenido de la tabla original a la auxiliar, borrar la tabla original, renombrar la tabla auxiliar por el nombre que tenia la tabla original y reconstruir todas las dependencias.
En una tabla vacia, el costo de hacer esto es casi nulo, en una tabla con 20 millones de registros y 30 tablas asociadas a esta via llaves foraneas es casi suicida. Por tal motivo, y con muy buen criterio, a partir de SQL Server 2008, el Enterprise Manager deshabilita por defecto todas las operaciones sobre edicion de tablas y columnas, que signifiquen una reconstruccion de la tabla. Esto sirve para evitar que algun despistado sin conocimiento sobre lo que hace, reconstruya una tabla en un servidor de produccion.
De todas maneras, si necesitamos hacer esto independientemente del costo que nos representa, este este es un script de ejemplo:

/*Paso 1: Borro todos los objetos dependedientes de la tabla a modificar*/
ALTER TABLE TablaAsociada DROP CONSTRAINT TablaAsociada_Original_FK

/*Paso 2: Creo una tabla nueva exactamente similar a la original, pero agregando la propiedad Identity*/
CREATE TABLE TablaOriginalAuxiliar
(
ID int IDENTITY(1,1),
Campo1 varchar(200)
)

/*Paso 3: Habilitamos el insertado explicito de valores en la columna de tipo identity para mantener los valores antiguos del campo ID*/
SET IDENTITY_INSERT TablaOriginalAuxiliar ON

/*Paso 4: Copio el contenido de una tabla a la otra*/
INSERT INTO TablaOriginalAuxiliar (ID, Campo1) SELECT ID, Campo1 FROM TablaOriginal

/*Paso5: Deshabilitamos el insertado explicito de valores en la columna de tipo identity*/
SET IDENTITY_INSERT TablaOriginalAuxiliar OFF

/*Paso 6: Borro la tabla original*/
DROP TABLE TablaOriginal

/*Paso 7: Renombro la tabla auxiliar por la tabla original*/
sp_rename ‘TablaOriginalAuxiliar’,‘TablaOriginal’

/*Paso 8: Creo todos los objetos dependedientes de la tabla a modificada que habia borrado en el paso 1*/
ALTER TABLE TablaAsociada ADD CONSTRAINT TablaAsociada_Original_FK

Todo esto tambien aplica en el caso inverso, cuando tenemos una columna identity y queremos deshabilitarla.

2) Determinar el estado del IDENTITY_INSERT en una tabla:
En algunas ocasiones (no muchas) puede ser necesario verificar si una tabla que tiene una columna identity, tiene en estado ON o OFF la opcion IDENTITY_INSERT. Nuevamente, no es posible de manera directa hacer esto. No hay ninguna vista de sistema que nos indique el estado de la tabla con respecto a esta situación.

Sin embargo, existen métodos alternativos para hacer esto.
El método mas común es insertar un registro en tabla que queremos seteandole un valor explicito que sabemos que no existe en la columna (ejemplo: un 0, que no suele ser un valor habitual para un ID) en la columna identity. Si tira error, es porque la opción esta deshabilitada. Toda esta operación debe estar encapsulada dentro de una transacción y al finalizar, deberemos hacer un rollback.

Ejemplo:

BEGIN TRANSACTION
DECLARE @err int
INSERT INTO tabla1 (id) values (0)
SET @err=@@error
ROLLBACK TRANSACTION

IF @err =0
print ‘identity_insert = on’
else
print ‘identity_insert = off’

3) Consultar si una columna es identity

Para esto, debemos ver las vistas de sistema de SQL Server.
La forma mas práctica, es consultar la vista sys.column y verificar el valor del campo is_identity.
Ejemplo:

select name, is_identity from sys.columns where OBJECTNAME(object_id) = ‘NombreDeLaTabla’ and name = ‘NombreColumna’

Existe tambien una vista llamada sys.identity_columns que nos devuelve todas las columnas identity de todas las tablas de la base de datos.

Recordemos que estos ejemplos solo aplican a SQL Server 2005/2008 en adelante. No funciona en SQL Server 2000.

4) Modificar la propiedad NOT FOR REPLICATION de una columna identity sin reconstruir la tabla:
Por alguna razón desconocida, si deseamos modificar la propiedad NOT FOR REPLICATION de una columna identity desde el enterprise manager de SQL Server, este reconstruye la tabla (hace exactamente lo mismo que vimos en el punto 1 al principio del post).
Sin embargo, existe una manera de hacer esto muchísimo mas eficiente y es llamar al store procedure de sistema sys.sp_identitycolumnforreplication.
Internamente este store procedure llama a un proceso interno de SQL Server que modifica la tabla, sin necesidad de reconstruirla.

Ejemplo para habilitar la propiedad NOT FOR REPLICATION:

EXEC sys.sp_identitycolumnforreplication OBJECT_ID(“NombreDeLaTabla”), 1



Ejemplo para deshabilitar la propiedad NOT FOR REPLICATION:

EXEC sys.sp_identitycolumnforreplication OBJECT_ID(“NombreDeLaTabla”), 0

5) Buscar gaps o huecos dentro de una columna
Esta es una pregunta habitual, pero la realidad es que no se me ocurre un escenario real donde un hueco entre los valores de una columna identity pueda ser relevante para nosotros. Generalmente quienes consideran relevante esto, es porque estan haciendo un mal uso conceptual de los identities. Recordemos que un identity funciona perfecto como valor interno para una clave primaria. Pero le queremos dar otros usos (como por ejemplo, que sea el codigo numerico de una factura) estamos cometiendo un error suicida. Los identities no son transaccionales y no se puede confiar en una tabla no tenga huecos si se efectuan operaciones de DELETE sobre la misma.

De todos modos, quienes esten buscando gaps, les recomiendo entrar a este post del excelente blog de Pinal Dave, donde no solamente está disponible un script para realizar dicha consulta (para nada algo trivial), sino que ademas hay una muy buena polémica al respecto del uso de los identities.

« Newer PostsOlder Posts »

Blog de WordPress.com.