sql >> Base de Datos >  >> RDS >> Database

FORMAT() está bien y todo eso, pero...

Cuando SQL Server 2012 aún estaba en versión beta, escribí en un blog sobre el nuevo FORMAT() función:SQL Server v.Next (Denali):Mejoras de CTP3 T-SQL:FORMAT().

En ese momento, estaba tan entusiasmado con la nueva funcionalidad que ni siquiera pensé en hacer ninguna prueba de rendimiento. Abordé esto en una publicación de blog más reciente, pero únicamente en el contexto de eliminar el tiempo de una fecha y hora:Recortar el tiempo de una fecha y hora:un seguimiento.

La semana pasada, mi buen amigo Jason Horner (blog | @jasonhorner) me bromeó con estos tuits:

Mi problema con esto es que FORMAT() parece conveniente, pero es extremadamente ineficiente en comparación con otros enfoques (ah, y eso AS VARCHAR la cosa también está mal). Si está haciendo este oney-twosy y para conjuntos de resultados pequeños, no me preocuparía demasiado por eso; pero a escala, puede ser bastante caro. Permítanme ilustrar con un ejemplo. Primero, creemos una pequeña tabla con 1000 fechas pseudoaleatorias:

SELECT TOP (1000) d = DATEADD(DAY, CHECKSUM(NEWID())%1000, o.create_date)
  INTO dbo.dtTest
  FROM sys.all_objects AS o
  ORDER BY NEWID();
GO
CREATE CLUSTERED INDEX d ON dbo.dtTest(d);

Ahora, preparemos el caché con los datos de esta tabla e ilustremos tres de las formas comunes en que las personas tienden a presentar la hora exacta:

SELECT d, 
  CONVERT(DATE, d), 
  CONVERT(CHAR(10), d, 120),
  FORMAT(d, 'yyyy-MM-dd')
FROM dbo.dtTest;

Ahora, realicemos consultas individuales que utilicen estas diferentes técnicas. Los ejecutaremos cada 5 veces y ejecutaremos las siguientes variaciones:

  1. Seleccionar las 1000 filas
  2. Seleccionar TOP (1) ordenado por la clave de índice agrupado
  3. Asignación a una variable (lo que fuerza un análisis completo, pero evita que la representación de SSMS interfiera con el rendimiento)

Aquí está el guión:

-- select all 1,000 rows
GO
SELECT d FROM dbo.dtTest;
GO 5
SELECT d = CONVERT(DATE, d) FROM dbo.dtTest;
GO 5
SELECT d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest;
GO 5
SELECT d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest;
GO 5
 
-- select top 1
GO
SELECT TOP (1) d FROM dbo.dtTest ORDER BY d;
GO 5
SELECT TOP (1) CONVERT(DATE, d) FROM dbo.dtTest ORDER BY d;
GO 5
SELECT TOP (1) CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ORDER BY d;
GO 5
SELECT TOP (1) FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest ORDER BY d;
GO 5
 
-- force scan but leave SSMS mostly out of it
GO
DECLARE @d DATE;
SELECT @d = d FROM dbo.dtTest;
GO 5
DECLARE @d DATE;
SELECT @d = CONVERT(DATE, d) FROM dbo.dtTest;
GO 5
DECLARE @d CHAR(10);
SELECT @d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest;
GO 5
DECLARE @d CHAR(10);
SELECT @d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest;
GO 5

Ahora, podemos medir el rendimiento con la siguiente consulta (mi sistema es bastante silencioso; en el suyo, es posible que deba realizar un filtrado más avanzado que solo execution_count ):

SELECT 
  [t] = CONVERT(CHAR(255), t.[text]), 
  s.total_elapsed_time, 
  avg_elapsed_time = CONVERT(DECIMAL(12,2),s.total_elapsed_time / 5.0),
  s.total_worker_time, 
  avg_worker_time = CONVERT(DECIMAL(12,2),s.total_worker_time / 5.0),
  s.total_clr_time
FROM sys.dm_exec_query_stats AS s 
CROSS APPLY sys.dm_exec_sql_text(s.[sql_handle]) AS t
WHERE s.execution_count = 5
  AND t.[text] LIKE N'%dbo.dtTest%'
ORDER BY s.last_execution_time;

Los resultados en mi caso fueron bastante consistentes:

Consulta (truncada) Duración (microsegundos)
total_transcurrido avg_elapsed total_clr
SELECCIONE 1000 filas SELECT d FROM dbo.dtTest ORDER BY d; 1,170 234.00 0
SELECT d = CONVERT(DATE, d) FROM dbo.dtTest ORDER BY d; 2,437 487.40 0
SELECT d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ORD ... 151,521 30,304.20 0
SELECT d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest ORDER ... 240,152 48,030.40 107,258
SELECT TOP (1) SELECT TOP (1) d FROM dbo.dtTest ORDER BY d; 251 50.20 0
SELECT TOP (1) CONVERT(DATE, d) FROM dbo.dtTest ORDER BY ... 440 88.00 0
SELECT TOP (1) CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ... 301 60.20 0
SELECT TOP (1) FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest O ... 1,094 218.80 589
Assign variable DECLARE @d DATE; SELECT @d = d FROM dbo.dtTest; 639 127.80 0
DECLARE @d DATE; SELECT @d = CONVERT(DATE, d) FROM dbo.d ... 644 128.80 0
DECLARE @d CHAR(10); SELECT @d = CONVERT(CHAR(10), d, 12 ... 1,972 394.40 0
DECLARE @d CHAR(10); SELECT @d = FORMAT(d, 'yyyy-MM-dd') ... 118,062 23,612.40 98,556

 

And to visualize the avg_elapsed_time salida (haga clic para ampliar):

FORMAT() es claramente el perdedor:avg_elapsed_time resultados (microsegundos)

Lo que podemos aprender de estos resultados (nuevamente):

  1. En primer lugar, FORMAT() es caro .
  2. FORMAT() puede, sin duda, proporcionar más flexibilidad y brindar métodos más intuitivos que sean consistentes con los de otros lenguajes como C#. Sin embargo, además de su sobrecarga, y mientras CONVERT() los números de estilo son crípticos y menos exhaustivos, es posible que deba usar el enfoque anterior de todos modos, ya que FORMAT() solo es válido en SQL Server 2012 y posteriores.
  3. Incluso el CONVERT() en espera El método puede ser drásticamente costoso (aunque solo gravemente en el caso de que SSMS tuviera que generar los resultados; claramente maneja las cadenas de forma diferente a los valores de fecha).
  4. Simplemente extraer el valor de fecha y hora directamente de la base de datos siempre fue más eficiente. Debe perfilar el tiempo adicional que le toma a su aplicación formatear la fecha según lo desee en el nivel de presentación; es muy probable que no desee que SQL Server se involucre en absoluto con el formato bonito (y de hecho muchos argumentarían que aquí es donde esa lógica siempre pertenece).

Aquí solo estamos hablando de microsegundos, pero también estamos hablando de 1000 filas. Escale eso a los tamaños reales de su tabla, y el impacto de elegir el enfoque de formato incorrecto podría ser devastador.

Si desea probar este experimento en su propia máquina, he subido un script de muestra:FormatIsNiceAndAllBut.sql_.zip