Primero, mis disculpas por el retraso en mi respuesta desde mis últimos comentarios.
El tema surgió en los comentarios de que el uso de un CTE recursivo (rCTE de aquí en adelante) se ejecuta lo suficientemente rápido debido al bajo número de filas. Si bien puede parecer así, nada podría estar más lejos de la verdad.
CONSTRUYE TALLY TABLE Y TALLY FUNCTION
Antes de comenzar a probar, necesitamos construir una tabla de conteo física con el índice agrupado apropiado y una función de conteo estilo Itzik Ben-Gan. También haremos todo esto en TempDB para no dejar caer accidentalmente las golosinas de nadie.
Aquí está el código para construir la tabla de conteo y mi versión de producción actual del maravilloso código de Itzik.
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Physical Tally Table
IF OBJECT_ID('dbo.Tally','U') IS NOT NULL
DROP TABLE dbo.Tally
;
-- Note that the ISNULL makes a NOT NULL column
SELECT TOP 1000001
N = ISNULL(ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1,0)
INTO dbo.Tally
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE dbo.Tally
ADD CONSTRAINT PK_Tally PRIMARY KEY CLUSTERED (N)
;
--===== Create/Recreate a Tally Function
IF OBJECT_ID('dbo.fnTally','IF') IS NOT NULL
DROP FUNCTION dbo.fnTally
;
GO
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Purpose:
Return a column of BIGINTs from @ZeroOrOne up to and including @MaxN with a max value of 1 Trillion.
As a performance note, it takes about 00:02:10 (hh:mm:ss) to generate 1 Billion numbers to a throw-away variable.
Usage:
--===== Syntax example (Returns BIGINT)
SELECT t.N
FROM dbo.fnTally(@ZeroOrOne,@MaxN) t
;
Notes:
1. Based on Itzik Ben-Gan's cascading CTE (cCTE) method for creating a "readless" Tally Table source of BIGINTs.
Refer to the following URLs for how it works and introduction for how it replaces certain loops.
http://www.sqlservercentral.com/articles/T-SQL/62867/
http://sqlmag.com/sql-server/virtual-auxiliary-table-numbers
2. To start a sequence at 0, @ZeroOrOne must be 0 or NULL. Any other value that's convertable to the BIT data-type
will cause the sequence to start at 1.
3. If @ZeroOrOne = 1 and @MaxN = 0, no rows will be returned.
5. If @MaxN is negative or NULL, a "TOP" error will be returned.
6. @MaxN must be a positive number from >= the value of @ZeroOrOne up to and including 1 Billion. If a larger
number is used, the function will silently truncate after 1 Billion. If you actually need a sequence with
that many values, you should consider using a different tool. ;-)
7. There will be a substantial reduction in performance if "N" is sorted in descending order. If a descending
sort is required, use code similar to the following. Performance will decrease by about 27% but it's still
very fast especially compared with just doing a simple descending sort on "N", which is about 20 times slower.
If @ZeroOrOne is a 0, in this case, remove the "+1" from the code.
DECLARE @MaxN BIGINT;
SELECT @MaxN = 1000;
SELECT DescendingN = @MaxN-N+1
FROM dbo.fnTally(1,@MaxN);
8. There is no performance penalty for sorting "N" in ascending order because the output is explicity sorted by
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
Revision History:
Rev 00 - Unknown - Jeff Moden
- Initial creation with error handling for @MaxN.
Rev 01 - 09 Feb 2013 - Jeff Moden
- Modified to start at 0 or 1.
Rev 02 - 16 May 2013 - Jeff Moden
- Removed error handling for @MaxN because of exceptional cases.
Rev 03 - 22 Apr 2015 - Jeff Moden
- Modify to handle 1 Trillion rows for experimental purposes.
**********************************************************************************************************************/
(@ZeroOrOne BIT, @MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
E1(N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1) --10E1 or 10 rows
, E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d) --10E4 or 10 Thousand rows
,E12(N) AS (SELECT 1 FROM E4 a, E4 b, E4 c) --10E12 or 1 Trillion rows
SELECT N = 0 WHERE ISNULL(@ZeroOrOne,0)= 0 --Conditionally start at 0.
UNION ALL
SELECT TOP(@MaxN) N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E12 -- Values from 1 to @MaxN
;
GO
Por cierto ... observe que construyó una tabla de conteo de un millón y una fila y le agregó un índice agrupado en aproximadamente un segundo más o menos. ¡Intente ESO con un rCTE y vea cuánto tarda! ;-)
CONSTRUIR ALGUNOS DATOS DE PRUEBA
También necesitamos algunos datos de prueba. Sí, estoy de acuerdo en que todas las funciones que vamos a probar, incluido el rCTE, se ejecutan en un milisegundo o menos durante solo 12 filas, pero esa es la trampa en la que muchas personas caen. Hablaremos más sobre esa trampa más tarde, pero, por ahora, simulemos llamar a cada función 40,000 veces, que es aproximadamente cuántas veces se llaman ciertas funciones en mi tienda en un día de 8 horas. Imagínense cuántas veces se podrían llamar tales funciones en un gran negocio minorista en línea.
Entonces, aquí está el código para construir 40,000 filas con fechas aleatorias, cada una con un Número de fila solo para fines de seguimiento. No me tomé el tiempo para hacer las horas enteras porque no importa aquí.
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Test Date table
IF OBJECT_ID('dbo.TestDate','U') IS NOT NULL
DROP TABLE dbo.TestDate
;
DECLARE @StartDate DATETIME
,@EndDate DATETIME
,@Rows INT
;
SELECT @StartDate = '2010' --Inclusive
,@EndDate = '2020' --Exclusive
,@Rows = 40000 --Enough to simulate an 8 hour day where I work
;
SELECT RowNum = IDENTITY(INT,1,1)
,SomeDateTime = RAND(CHECKSUM(NEWID()))*DATEDIFF(dd,@StartDate,@EndDate)+@StartDate
INTO dbo.TestDate
FROM dbo.fnTally(1,@Rows)
;
CONSTRUYA ALGUNAS FUNCIONES PARA HACER LA COSA DE 12 HORAS DE FILA
A continuación, convertí el código rCTE en una función y creé otras 3 funciones. Todos han sido creados como iTVF de alto rendimiento (funciones de valor de tabla en línea). Siempre se puede saber porque los iTVF nunca tienen un COMIENZO en ellos como Scalar o mTVF (funciones con valores de tabla de múltiples declaraciones).
Aquí está el código para construir esas 4 funciones ... Las nombré por el método que usan y no por lo que hacen solo para que sea más fácil identificarlas.
--===== CREATE THE iTVFs
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.OriginalrCTE','IF') IS NOT NULL
DROP FUNCTION dbo.OriginalrCTE
;
GO
CREATE FUNCTION dbo.OriginalrCTE
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH Dates AS
(
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,@Date)) [Hour],
DATEADD(HOUR,-1,@Date) [Date], 1 Num
UNION ALL
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,[Date])),
DATEADD(HOUR,-1,[Date]), Num+1
FROM Dates
WHERE Num <= 11
)
SELECT [Hour], [Date]
FROM Dates
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.MicroTally','IF') IS NOT NULL
DROP FUNCTION dbo.MicroTally
;
GO
CREATE FUNCTION dbo.MicroTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,t.N,@Date))
,[DATE] = DATEADD(HOUR,t.N,@Date)
FROM (VALUES (-1),(-2),(-3),(-4),(-5),(-6),(-7),(-8),(-9),(-10),(-11),(-12))t(N)
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.PhysicalTally','IF') IS NOT NULL
DROP FUNCTION dbo.PhysicalTally
;
GO
CREATE FUNCTION dbo.PhysicalTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.Tally t
WHERE N BETWEEN 1 AND 12
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.TallyFunction','IF') IS NOT NULL
DROP FUNCTION dbo.TallyFunction
;
GO
CREATE FUNCTION dbo.TallyFunction
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.fnTally(1,12) t
;
GO
CONSTRUIR EL ARNÉS DE PRUEBA PARA PROBAR LAS FUNCIONES
Por último, pero no menos importante, necesitamos un arnés de prueba. Hago una verificación de línea de base y luego pruebo cada función de manera idéntica.
Aquí está el código para el arnés de prueba ...
PRINT '--========== Baseline Select =================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = RowNum
,@Date = SomeDateTime
FROM dbo.TestDate
CROSS APPLY dbo.fnTally(1,12);
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Orginal Recursive CTE ===========================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.OriginalrCTE(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Dedicated Micro-Tally Table =====================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.MicroTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Physical Tally Table =============================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.PhysicalTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Tally Function ===================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.TallyFunction(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
Una cosa a tener en cuenta en el arnés de prueba anterior es que desvío toda la salida a variables "desechables". Eso es para tratar de mantener las mediciones de rendimiento tan puras como sea posible sin ninguna salida al disco o resultados de inclinación de la pantalla.
UNA PALABRA DE PRECAUCIÓN SOBRE LAS ESTADÍSTICAS FIJAS
Además, una advertencia para los posibles evaluadores ... NO DEBE usar SET STATISTICS cuando pruebe las funciones Scalar o mTVF. Solo se puede usar de forma segura en funciones iTVF como las de esta prueba. Se ha demostrado que SET STATISTICS hace que las funciones SCALAR se ejecuten cientos de veces más lento de lo que realmente lo hacen sin él. Sí, estoy tratando de inclinar otro molino de viento, pero eso sería una publicación más larga y no tengo tiempo para eso. Tengo un artículo en SQLServerCentral.com que habla sobre eso, pero no tiene sentido publicar el enlace aquí porque alguien se volverá loco al respecto.
LOS RESULTADOS DE LA PRUEBA
Entonces, aquí están los resultados de la prueba cuando ejecuto el arnés de prueba en mi pequeña computadora portátil i5 con 6GB de RAM.
--========== Baseline Select =================================
Table 'Worktable'. Scan count 1, logical reads 82309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 203 ms, elapsed time = 206 ms.
--========== Orginal Recursive CTE ===========================
Table 'Worktable'. Scan count 40001, logical reads 2960000, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4258 ms, elapsed time = 4415 ms.
--========== Dedicated Micro-Tally Table =====================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 234 ms, elapsed time = 235 ms.
--========== Physical Tally Table =============================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Tally'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 252 ms.
--========== Tally Function ===================================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 253 ms.
El "SELECCIÓN DE BASE", que solo selecciona datos (cada fila creada 12 veces para simular el mismo volumen de retorno), llegó a la derecha aproximadamente 1/5 de segundo. Todo lo demás llegó aproximadamente a un cuarto de segundo. Bueno, todo excepto esa maldita función rCTE. Tomó 4 y 1/4 segundos o 16 veces más (1,600% más lento).
Y mire las lecturas lógicas (memoria IO) ... El rCTE consumió la friolera de 2,960,000 (casi 3 MILLONES de lecturas) mientras que las otras funciones solo consumieron aproximadamente 82,100. Eso significa que el rCTE consumió más de 34.3 veces más memoria IO que cualquiera de las otras funciones.
PENSAMIENTOS DE CIERRE
Resumamos El método rCTE para hacer esta cosa "pequeña" de 12 filas usó 16 VECES (1,600%) más CPU (y duración) y 34,3 VECES (3,430%) más memoria IO que cualquiera de las otras funciones.
Je ... Sé lo que estás pensando. "¡Gran cosa! Es solo una función".
Sí, de acuerdo, pero ¿cuántas otras funciones tienes? ¿Cuántos otros lugares fuera de las funciones tiene? ¿Y tiene alguno de esos que funcionan con más de 12 filas cada ejecución? Y, ¿hay alguna posibilidad de que alguien en una sacudida por un método pueda copiar ese código rCTE para algo mucho más grande?
Ok, es hora de ser franco. No tiene ningún sentido que las personas justifiquen el código de rendimiento desafiado solo por el supuesto recuento o uso de filas limitadas. Excepto cuando compra una caja MPP por quizás millones de dólares (sin mencionar el costo de reescribir el código para que funcione en una máquina de este tipo), no puede comprar una máquina que ejecute su código 16 veces más rápido (SSD ganó tampoco lo hagas ... todo esto estaba en la memoria de alta velocidad cuando lo probamos). El rendimiento está en el código. El buen rendimiento está en buen código.
¿Te imaginas si todo tu código se ejecutó "solo" 16 veces más rápido?
Nunca justifique código malo o de rendimiento desafiado en recuentos bajos o incluso bajo uso. Si lo hace, es posible que tenga que pedir prestado uno de los molinos de viento que me acusaron de inclinar para mantener sus CPU y discos lo suficientemente frescos. ;-)
UNA PALABRA EN LA PALABRA "TALLY"
Si estoy de acuerdo. Hablando semánticamente, la tabla de conteo contiene números, no "recuentos". En mi artículo original sobre el tema (no era el artículo original sobre la técnica, pero fue el primero), lo llamé "Tally" no por lo que contiene, sino por lo que hace ... es solía "contar" en lugar de repetir y "contar" algo es "contar" algo. ;-) Llámalo como quieras ... Tabla de números, Tabla de conteo, Tabla de secuencia, lo que sea. No me importa Para mí, "Tally" tiene más significado completo y, al ser un buen DBA perezoso, contiene solo 5 letras (2 son idénticas) en lugar de 7 y es más fácil de decir para la mayoría de las personas. También es "singular", que sigue mi convención de nomenclatura para tablas. ;-) Eso' También se llama el artículo que contenía una página de un libro de los años 60. Siempre me referiré a ella como una "Tabla de conteo" y aún sabrá lo que yo u otra persona queremos decir. También evito la notación húngara como la peste, pero llamé a la función "fnTally" para poder decir "Bueno, si usaras la función de conteo ef-en que te mostré, no tendrías un problema de rendimiento" sin que realmente sea Violación de recursos humanos. ;-) sin que en realidad sea una violación de recursos humanos. ;-) sin que en realidad sea una violación de recursos humanos. ;-)
Lo que más me preocupa es que las personas aprendan a usarlo correctamente en lugar de recurrir a cosas como los rCTE con problemas de rendimiento y otras formas de RBAR oculto.