Como calcular un acumulado de forma eficiente con TSQL

Standard

En algunas oportunidades necesitamos hacer alguna consulta que nos genere una acumulado de valores, por ejemplo si tenemos una tabla con transacciones poder ir viendo la evolución del saldo para un articulo transacción por transacción.

Este tipo de consultas suelen ser un tanto complejas de resolver y además por lo general poco eficientes.

Por suerte a partir de SQL Server 2012 tenemos la posibilidad de usar las Windows Function las cuales son muy poderosas.

Lo que haremos en este articulo es poder evaluar sobre nuestra base de datos AdventiureWorks2012 o 2014 las diferentes opciones que tenemos para poder calcular un acumulado comparándolos en performance a cada uno de ellos.

Básicamente lo que deseamos obtener es una consulta donde su resultado sea como la siguiente figura donde la columna balance muestra el acumulado de las cantidades para un producto dado transacción por transacción.

image

 Cursores:

Podríamos utilizar un curso para esta consulta el cual su código seria algo así como:

DECLARE @Result AS TABLE

(

  ProductID   INT,

  TransactionID  INT,

  Quantity     int,

  balance int

);

DECLARE

  @productid    AS INT,

  @prvactid AS INT,

  @tranid   AS INT,

  @quantity     AS int,

  @balance  AS int;

 

DECLARE C CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY FOR

  SELECT t.ProductID  , t.TransactionID , t.Quantity

  FROM [Production].[TransactionHistory] t

  ORDER BY t.ProductID , t.TransactionID ;

OPEN C

 

FETCH NEXT FROM C INTO @productid, @tranid, @quantity;

SELECT @prvactid = @productid, @balance = 0;

WHILE @@fetch_status = 0

BEGIN

  IF @productid  <> @prvactid

    SELECT @prvactid = @productid , @balance = 0;

       SET @balance = @balance + @quantity;

       INSERT INTO @Result VALUES(@productid , @tranid, @quantity, @balance);

  FETCH NEXT FROM C INTO @productid, @tranid, @quantity;

END

CLOSE C;

DEALLOCATE C;

 

SELECT * FROM @Result;

GO

 

Sub Consultas

Otra alternativa podría ser la utilización de subconsultas

SELECT T1.ProductID , T1.TransactionID , T1.Quantity ,

  SUM(T2.Quantity ) AS balance

FROM [Production].[TransactionHistory]  AS T1

  JOIN [Production].[TransactionHistory] AS T2

    ON T2.ProductID = T1.ProductID

   AND T2.TransactionID  <= T1.TransactionID

GROUP BY T1.ProductID , T1.TransactionID , T1.Quantity

ORDER BY T1.PRODUCTID,T1.TRANSACTIONID

 

Window Function

Como comente al principio estas funciones se las puede usar a partir de SQL 2012 y lo que resuelven básicamente es la utilización de OVER en las funciones de agregación como el SUM, MIN, MAX, etc.

Veamos como se haría la misma consulta pero usando estas nuevas opciones.

SELECT T.TransactionID,

       T.TransactionDate,

       T.ProductID,

       T.Quantity,

       SUM(T.QUANTITY) OVER (PARTITION BY PRODUCTID ORDER BY TransactionID

                                ROWS UNBOUNDED PRECEDING) AS balance 

   FROM

[Production].[TransactionHistory] T

ORDER BY ProductID,TransactionID 

 

Comparativas en performance y uso de recursos

Lo ultimo que nos quedaría hacer es poder medir los impactos en performance de cada solución y los recursos consumidos, para ello utilice profiler y exponer los resultados en los siguientes gráficos:

Solución

Tiempo ms

Reads

CPU

Sub consulta

24290

804644

69938

Cursores

5948

573391

4140

Window Function

978

876

451

 

image

image

image

 

Conclusiones

Los números hablan por si solos, la diferencia de usar Window Function es insuperable tanto en los tiempos de proceso como en los recursos consumidos.

PowerQuery sigue creciendo

Standard

Esta excelente herramienta de BI sigue creciendo día a día, en su versión de marzo 2015 incorpora

  • Conector para CRM Online.
  • Mejoras en performance.
  • Nuevas transformaciones.

Sin duda que esta herramienta esta día a día creciendo para mejorar el trabajo en BI de los usuarios, yo soy un usuario frecuente de la misma y la verdad que cada día me gusta mas.

Les dejo el link de descarga.

Power Query Update 2

Optimizando los tiempos de creación, ampliación y restore de Base de Datos.

Standard

SQL Server en su edición Enterprise y desde la versión 2005 dispone de la funcionalidad “Instant File Initialization” la cual permite optimizar de forma significativa las operaciones que se realizan sobre los archivos de datos (creación, ampliación y restore básicamente).

Esta funcionalidad no es algo que se deba activar en el SQL Server sino que al Sistema operativo hay que indicarle que le de permisos a la cuenta de SQL Server para poder usarlo, por defecto los administradores del equipo tienen permisos solo para ello, con lo cual si la cuenta de servicio no es administrador del equipo no usara la funcionalidad.

Habilitar o verificar que este el permiso en el SO para usar Instant File.

Paso 1: Desde el menú inicio haga clic en ejecutar (o desde la consola) y escriba: secpol.msc para abrir la Local Security Policy

Paso 2: Del menú de la izquierda busque : Local Policy – > User Rights Assignment

Paso 3: Busque la política : Perfmon volume Maintenance task

Paso 4: Agregue la cuenta de servicio a la política

Paso 5: Reiniciar el servicio de SQL Server

image

image

Pruebas de performance:

Lo que haremos son unas simples pruebas sin tener los permisos habilitados en el Sistema Operativo y luego habilitándolo para poder así usar dicha funcionalidad.

Test 1: Crear una base de datos vacía de 10GB

CREATE DATABASE [DEMOFILEINIT]

 ON  PRIMARY

( NAME = N’DEMOFILEINIT’, FILENAME = N’D:\TMP\DEMOFILEINIT.mdf’ , SIZE = 10240000KB , FILEGROWTH = 1024KB )

 LOG ON

( NAME = N’DEMOFILEINIT_log’, FILENAME = N’D:\TMP\DEMOFILEINIT_log.ldf’ , SIZE = 1024KB , FILEGROWTH = 10%)

GO

 

Test 2: Hacer un restore de la base de datos vacia:

 

RESTORE DATABASE [DEMOFILEINIT] FROM  DISK = N’D:\TMP\DBKP.BAK’ WITH  FILE = 1,  NOUNLOAD

 

Resultados de los test:

 

image

 

NewID() Vs NewsequentialID() en claves primarias

Standard

En algunas situaciones nos puede tocar utilizar alguna clave artificial del tipo GUID como PK (Primary Key). Si bien esto en principio no es ningún problema, lo que sucede es que por defecto (a menos que nosotros digamos otra cosa) toda PK es el índice Clustered de la tabla y aquí si ya empezamos a tener problemas con este tipo de datos.

Dependiendo de la función que usemos podemos obtener mejores en la performance general de la tabla o bien condenarla a que si la PK es el clustered la misma pierda performance en las operaciones de Insert y además quede fragmentada.

Del lado de SQL Server existen dos funciones para poder hacer GUID las cuales son:   NEWID() y NEWSEQUENTIALID()

La primera genera GUID de forma aleatoria con lo cual el problema que vamos a tener que no son secuenciales y si esto lo ponemos en un índice Clustered vamos a tener una tabla mas fragmentada y con perdida de performance.

Ahora haremos algunas pruebas con código para poder comparar ambas funciones:

Creamos tablas:

Vamos a crear dos tablas con el tipo de dato Uniqueidentifier donde usaremos en un caso NEWID() y en otro NEWSEQUENTIALID().

CREATE TABLE T_NEWSEQUENTIALID

(ID UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,

 FECHA DATETIME,

 C1 CHAR(200),

 C2 CHAR(200),

 C3 CHAR(200),

 C4 CHAR(200),

 NRO INT IDENTITY

)

GO

CREATE TABLE T_NEWID

(ID UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,

 FECHA DATETIME,

 C1 CHAR(200),

 C2 CHAR(200),

 C3 CHAR(200),

 C4 CHAR(200),

 NRO INT IDENTITY

)

GO

 

Insertamos 300K registros a cada tabla:

 

Luego de hacer un INSERT de 300.000 registros a cada tabla haremos la siguiente consulta en cada tabla para observar que sucedió con el orden de las inserciones.

 

SELECT TOP 5 ID,NRO AS ORDENINSERT FROM T_NEWSEQUENTIALID

ORDER BY ID

SELECT TOP 5 ID,NRO AS ORDENINSERT FROM T_NEWID

ORDER BY ID

 

image

 

Aquí podemos observar que NEWID() (el segundo cuadro) fueron insertados de forma totalmente aleatoria impactando en la performance de esta tabla ya que sus PK son clustered.

 

Tiempos de inserción:

 

El siguiente grafico muestra el tiempo insumido en hacer los 300k Insert en cada caso.

 

image

 

Análisis de fragmentación:

Lo que analizaremos ahora es en que nivel de fragmentación han quedado cada una de las tablas y cual seria su impacto.

Para ello usaremos el siguiente query:

SELECT OBJECT_NAME(OBJECT_ID) AS TABLA ,

INDEX_TYPE_DESC as INDICE,

AVG_FRAGMENTATION_IN_PERCENT AS [FRAG %]

FROM

SYS.DM_DB_INDEX_PHYSICAL_STATS(DB_ID(‘TESTID’), NULL, NULL, NULL , NULL);

 

image

Aquí podemos observar que la tabla que ha usado NEWID tiene un 99% de fragmentación Triste.

Como podemos medir que impacto podría tener esto? para ello vamos a correr estas dos consultas y ver su consumo de reads.

SET STATISTICS IO ON

SELECT TOP 20000 * FROM T_NEWSEQUENTIALID

SELECT TOP 20000 * FROM T_NEWID

SET STATISTICS IO OFF

 

Table ‘T_NEWSEQUENTIALID’. Scan count 1, logical reads 2266, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table ‘T_NEWID’. Scan count 1, logical reads 3342, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

Resumen:

Si necesitamos utilizar los campos GUID como PK lo ideal seria que no sean del tipo Clustered pero de serlo es ideal usar la función NEWSEQUENTIALID().

Debemos además mencionar que esta función tiene algoritmos mas performantes para poder calcular el GUID comparada contra  NEWID().

SELECT INTO y paralelismo en SQL 2014

Standard

Muchas veces utilizamos la instrucción SELECT INTO para poder insertar registros en alguna tabla.

A partir de SQL 2014 usando su modo de compatibilidad de base de datos 120, esta instrucción de forma automática hace paralelismo utilizando mas cores y así pudiendo mejorar los tiempos de respuesta en todas estas operaciones.

Es muy importante destacar que la base de datos debe estar en modo 120 (SQL 2014) de lo contrario el SELECT INTO funcionara como lo venía haciendo hasta el momento (sin paralelismo).

Ahora vamos a hacer algunas pruebas de performance y comparar los distintos planes de ejecución.

Usando paralelismo de SELECT INTO:

Lo que haremos primero es poner la base en modo 120 si es que no lo está:

ALTER DATABASE [AdventureWorksDW2014] SET COMPATIBILITY_LEVEL = 120

Ahora haremos un SELCT INTO y observaremos el plan de ejecución:

SELECT [ProductKey]
,[DateKey]
,[MovementDate]
,[UnitCost]
,[UnitsIn]
,[UnitsOut]
,[UnitsBalance]
INTO #TEMP1
FROM DBO.INSERTPARALELO



Aquí podemos observar la aparición de “Parallelism” en el INSERT.

Sin usar paralelismo de SELECT INTO:

La siguiente prueba es ejecutar el mismo SELECT INTO pero que no use paralelismo (versión de base de datos menor que 120)
ALTER DATABASE [AdventureWorksDW2014] SET COMPATIBILITY_LEVEL = 110

SELECT [ProductKey]
,[DateKey]
,[MovementDate]
,[UnitCost]
,[UnitsIn]
,[UnitsOut]
,[UnitsBalance]
INTO #TEMP1
FROM DBO.INSERTPARALELO


Aquí ya podemos observar que no aparece la operación de paralelismo como en el caso anterior.

Comparativas de performance:

En el siguiente cuadro se ilustran algunos números relacionados a la performance cuando se ejecuta de una u otra manera

Tipo Insert Costo QP Duración ms Cpu Time
Paralelismo (SQL 2014)

326

8094

22172

Normal (Modo 110)

576

9510

9484

Como se puede observar en la tabla de resultados es mas performante utilizar el modo de SQL 2014 a costa de mas consumo de CPU y en donde los costos de Query Plan han bajado de forma considerable al igual que los tiempos

Conclusiones:

SQL 2014 en su modo 120 de base de datos tiene entre sus tantas mejoras de performance la posibilidad de utilizar SELECT INTO con algoritmos de paralelismo mejorando así los tiempos de respuesta en estos procesos.

 

Como determinar cuál nodo del clúster es el activo

Standard

Muchas veces sufrimos un failover de un clúster de SQL Server o simplemente necesitamos determinar cuál de los nodos es el activo.

A partir de SQL Server 2012 se agregó a la DMV de sistema SYS.DM_OS_CLUSTER_NODES una nueva columna llamada Is_Current_Owner la cual es un bit e indica de todos los nodos cual está activo (o sea es el principal).Entonces si deseamos saber cuál esta como owner simplemente debemos ejecutar una consulta como la siguiente:

SELECT FROM SYS.DM_OS_CLUSTER_NODES WHERE is_current_owner = 1

Cursores de SQL Server y performance

Standard

Como ya todos sabemos el uso de esta técnica de programación tienen impactos negativos en la performance. Pero hay veces que no tenemos otra opción y si necesitamos usar algún que otro cursor Sad smile

En este post veremos como optimizar a los cursores ya que hay diferentes formas de definirlos y según como lo hagamos podemos tener distintos tiempos de respuesta.

Para nuestras pruebas usaremos la base de datos AdventurweWorks2012 en donde haremos distintos cursores probando los tiempos de respuesta, básicamente la diferencia entre ellos es el tipo de cursor pero no así la consulta o proceso que hagan internamente el cual siempre es el mismo.

Cursor Default:

La primer prueba es usar los cursores por defecto o sea sin indicarle ningún atributo.

DECLARE c CURSOR

FOR

  SELECT O.[object_id]

    FROM sys.objects AS O

    CROSS JOIN (SELECT TOP 1000 name FROM sys.objects) AS O2

    ORDER BY O.[object_id];

OPEN c;

FETCH c INTO @i;

WHILE (@@FETCH_STATUS = 0)

BEGIN

  SET @i += 1;

  FETCH c INTO @i;

END

CLOSE c;

DEALLOCATE c;

Cursor Local:

La segunda prueba es definir al cursor como local y correr el mismo proceso

DECLARE c CURSOR

LOCAL

FOR

  SELECT O.[object_id]

    FROM sys.objects AS O

    CROSS JOIN (SELECT TOP 1000 name FROM sys.objects) AS O2

    ORDER BY O.[object_id];

OPEN c;

FETCH c INTO @i;

WHILE (@@FETCH_STATUS = 0)

BEGIN

  SET @i += 1;

  FETCH c INTO @i;

END

CLOSE c;

DEALLOCATE c;

Cursor Local y estático:

Ahora haremos la prueba definiéndolo como local y a su vez estático

DECLARE c CURSOR

LOCAL STATIC

FOR

  SELECT O.[object_id]

    FROM sys.objects AS O

    CROSS JOIN (SELECT TOP 1000 name FROM sys.objects) AS O2

    ORDER BY O.[object_id];

OPEN c;

FETCH c INTO @i;

WHILE (@@FETCH_STATUS = 0)

BEGIN

  SET @i += 1;

  FETCH c INTO @i;

END

CLOSE c;

DEALLOCATE c;

Cursor Local y FAST_FORWARD:

La siguiente prueba es configurarlo como Local y FAST_FORWARD (Rápido y solo hacia adelante)

DECLARE c CURSOR

LOCAL FAST_FORWARD

FOR

  SELECT O.[object_id]

    FROM sys.objects AS O

    CROSS JOIN (SELECT TOP 1000 name FROM sys.objects) AS O2

    ORDER BY O.[object_id];

OPEN c;

FETCH c INTO @i;

WHILE (@@FETCH_STATUS = 0)

BEGIN

  SET @i += 1;

  FETCH c INTO @i;

END

CLOSE c;

DEALLOCATE c;

Cursor Local , Static Read_Only y Forward_Only:

Nuestra ultima prueba es configurarlo con todas estas opciones.

DECLARE c CURSOR

LOCAL STATIC READ_ONLY FORWARD_ONLY

FOR

  SELECT O.[object_id]

    FROM sys.objects AS O

    CROSS JOIN (SELECT TOP 1000 name FROM sys.objects) AS O2

    ORDER BY O.[object_id];

OPEN c;

FETCH c INTO @i;

WHILE (@@FETCH_STATUS = 0)

BEGIN

  SET @i += 1;

  FETCH c INTO @i;

END

CLOSE c;

DEALLOCATE c;

Comparación de performance en tiempo:

Al correr todos los distintos procesos hemos tomado los tiempos donde arrojaron los siguientes resultados:

TEST TIEMPO (Seg)
CURSOR DEFAULT 22
LOCAL 20
LOCAL STATIC 5
LOCAL FAST_FORWARD 4
LOCAL STATIC READ_ONLY FORWARD_ONLY  4

image

 

Conclusiones:

Si bien el uso de los cursores es una técnica poco recomendada por performance, es bueno considerar si tenemos que usarlos cuales son las mejores opciones de configuración para poder obtener los mejores tiempos de respuesta dentro de un cursor. Como se puede observar en los test hay una diferencia significativa entre los Cursores Default o locales y el resto

Como leer el contenido del transaction log de SQL Server

Standard

En muchas oportunidades me preguntan como se puede leer el contenido del transaction log de SQL Server, en este simple post les mostrare el uso de la función no documentada:

sys.fn_dblog la cual nos permitirá leer el transaction log.

 

Primero debemos estar posicionados sobre la base de datos que deseamos analizar el Log, por ejemplo AdventureWorks2012 y luego podemos simplemente correr el siguiente query

 

SELECT * FROM sys.fn_dblog(NULL, NULL)

 

 

El siguiente ejemplo solo busca las operaciones de modificación

 

SELECT * FROM sys.fn_dblog(NULL, NULL)

WHERE Operation IN

   (‘LOP_INSERT_ROWS’,‘LOP_MODIFY_ROW’,

    ‘LOP_DELETE_ROWS’,‘LOP_BEGIN_XACT’,‘LOP_COMMIT_XACT’) 

 

Leerlog

Índices redundantes y el impacto en la performance

Standard

En muchos lugares se habla de los índices redundantes o similares, estos básicamente son índices que están cubiertos por otros dentro de la misma tabla.

Supongamos que tenemos una tabla con 5 campos (C1,C2,C3,C4,C5) y luego creamos índices con este estilo:

Índice 1 = C1

Índice 2 = C1,C2

Índice 3 = C1,C2,C3,C4

El índice 3 cubre a los otros 2 (recordar que se leen y son útiles de izquierda a derecha) ya que índice 3 va a servir para las consultas de C1=Valor y C1 and C2 = Valor.

Que sucede si tenemos estos 3 índices en una tabla? bueno estaríamos castigando las operaciones de modificación (INSERT , UPDATE, DELETE)  ya que tiene que actualizar 3 índices cuando si borro Indice1 e Indice2 seria menor el costo en estas operaciones.

En principio entonces estaríamos diciendo que deberíamos eliminar el índice 1 y 2 para solo quedarnos con el 3, pero en este post veremos que no es tan así la cosa y antes de hacer esto hay que analizar otras cuestiones Sonrisa

Vamos a ir por partes entonces, en primer lugar crearemos una tabla que luego la llenaremos con una cantidad de registros.

USE TEMPDB

GO

 

CREATE TABLE TR (C1 INT,

                          C2 DATETIME,

                              C3 CHAR(500),

                              C4 CHAR(100),

                              C5 CHAR(300),

                              C6 BIT

                              )

GO

 

WITH C

AS

(                    

SELECT TOP 10000000 ROW_NUMBER() OVER( ORDER BY  C1.OBJECT_ID) AS ID  FROM

SYS.COLUMNS C1,

SYS.COLUMNS C2

)

INSERT INTO TR (

                        C1,

                        C2,

                        C3,

                        C4,

                        C5,

                        C6

)

SELECT C.ID AS C1,

DATEADD(MI,C.ID,GETDATE()) AS C2,

 REPLICATE (‘A’, 500LEN(C.ID)) + CONVERT(VARCHAR(200),C.ID),

 REPLICATE (‘B’, 100LEN(C.ID)) + CONVERT(VARCHAR(200),C.ID),

 REPLICATE (‘C’, 300LEN(C.ID)) + CONVERT(VARCHAR(200),C.ID),

 0 AS C6

 FROM C

Ahora que tenemos la tabla creada y sus datos en ella haremos algunas pruebas.

 

Update sin índices:

 

Haremos un UPDATE sobre la tabla sin índices creados para medir el consumo de Reads, para ello usaremos el siguiente código:

 

SET STATISTICS IO ON

 UPDATE TR SET C6=1

SET STATISTICS IO OFF

Como resultado obtenemos las siguientes estadísticas

 

Table ‘TR’. Scan count 1, logical reads 246045, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Update con el primer índice:

Lo que haremos ahora es crear un primer índice para el campo C2 y volver a correr el UPDATE anterior para poder así obtener las estadísticas.

CREATE INDEX I1 ON TR (C2)

SET STATISTICS IO ON

 UPDATE TR SET C6=1

SET STATISTICS IO OFF

Como resultado obtenemos las siguientes estadísticas

 

Table ‘TR’. Scan count 1, logical reads 1002733, physical reads 0, read-ahead reads 16, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

 

Como se puede observar hemos pasado de 246045 lecturas a 1002733 debido a que ahora la operación es mas costosa por el índice.

 

 

Update con dos índices:

 

Lo que haremos ahora es crear un segundo índice por varios campos pero el primero será para C2 y volveremos a correr el UPDATE

 

 

CREATE INDEX I2 ON TR (C2,C3,C4) INCLUDE (C5,C6)

SET STATISTICS IO ON

 UPDATE TR SET C6=1

SET STATISTICS IO OFF

Como resultado obtenemos las siguientes estadísticas

 

Table ‘TR’. Scan count 1, logical reads 1135426, physical reads 0, read-ahead reads 411, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

Como se puede observar también ahora es mas caro hacer el UPDATE  porque tenemos mas índices.

 

Índices redundantes:

 

Observando hasta acá podríamos decir que el primer índice I1 por la columna C2 es redundante ya que el I2 lo contiene en su primera columna, por lo cual podría ser una buena idea eliminar el índice 1 I1 para que las operaciones de modificación sean mas eficientes.

Vamos a hacer algunas pruebas primero con consultas y observar que pasa.

 

Si vemos el query plan del siguiente código observaremos que usa el índice 1 (I1)

 

SELECT * FROM TR

WHERE C2 >= GETDATE()

AND C2 <= GETDATE() + 40

Redundante1

 

 

Con un costo de QP de 146

 

Rediundante2

 

 

Como el primer índice es redundante con I2 vamos a eliminarlo y volver a correr la misma consulta.

 

DROP INDEX I1 ON TR

 

SELECT * FROM TR

WHERE C2 >= GETDATE()

AND C2 <= GETDATE() + 40

Vamos a observar el Query plan ahora

 

Redundante3

Redundante4

 

Como podemos observar se sigue haciendo un Index Seek (ahora por I2) pero el costo es mayor, pasamos de 146 a 151, y esto se debe a que I2 al ser mas pesado que I1 cuesta mas, recordar que el Costo de un Query Plan es = Costo CPU + Costo I/O de una query, como aquí el I/o es superior la consulta pesa mas y tarda mas.

 

Conclusiones:

No es para nada bueno tener índices redundantes, muchas veces (por no decir la mayoría) son por errores en el diseño, pero hay que tener cuidado con consultas especificas como las que vimos,en nuestro ejemplo si borramos el índice redundante las operaciones de UPDATE serán mas rápidas pero en la del Select será mas lenta, dependiendo de cuan critica sea una y otra es como deberíamos actuar.