web-dev-qa-db-fra.com

Connaissez-vous un moyen facile de générer un enregistrement pour chaque heure des 12 dernières heures?

J'ai un rapport qui montre le nombre d'événements depuis 12 heures, regroupés à l'heure. Cela semble assez facile, mais ce que je me lance, c'est comment inclure des enregistrements qui couvrent les lacunes.

Voici un exemple de table:

Event
(
  EventTime datetime,
  EventType int
)

Les données ressemblent à ceci:

  '2012-03-08 08:00:04', 1
  '2012-03-08 09:10:00', 2
  '2012-03-08 09:11:04', 2
  '2012-03-08 09:10:09', 1
  '2012-03-08 10:00:17', 4
  '2012-03-08 11:00:04', 1

J'ai besoin de créer un ensemble de résultats qui a un enregistrement pour chaque heure des 12 dernières heures, peu importe d'événements pendant cette heure ou non.

En supposant que l'heure actuelle est '2012-03-08 11:00:00', le rapport montrerait (environ):

Hour  EventCount
----  ----------
23    0
0     0
1     0
2     0
3     0
4     0
5     0
6     0
7     0
8     1
9     3
10    1

J'ai proposé une solution qui utilise une table qui a un enregistrement pour chaque heure de la journée. J'ai réussi à obtenir les résultats que je cherchais à utiliser un syndicat et une logique de cas convoluée dans la clause WHERE, mais j'espérais que quelqu'un avait une solution plus élégante.

12
datagod

Pour SQL Server 2005+ Vous pouvez générer ces 12 enregistrements très facilement avec une boucle AR un CTE récursif. Voici un exemple de CTE récursif:

DECLARE @Date DATETIME
SELECT @Date = '20120308 11:00:00'

;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

Ensuite, vous venez de vous joindre à cela avec votre table d'événements.

21
Lamak

Les tables de comptabilisation peuvent être utilisées pour des choses comme celle-ci. Ils peuvent être très efficaces. Créez le tableau de base ci-dessous. J'ai créé la table Tally avec seulement 24 lignes pour votre exemple, mais vous pouvez le créer avec beaucoup de personnes que vous souhaitez convenir à d'autres fins.

SELECT TOP 24 
        IDENTITY(INT,1,1) AS N
   INTO dbo.Tally
   FROM Master.dbo.SysColumns sc1,
        Master.dbo.SysColumns sc2

--===== Add a Primary Key to maximize performance
  ALTER TABLE dbo.Tally
    ADD CONSTRAINT PK_Tally_N 
        PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100

J'ai supposé que votre table a été appelée dbo.Tblevents, exécutez la requête ci-dessous. Je crois que c'est ce que vous recherchez:

SELECT t.n, count(e.EventTime)
FROM dbo.Tally t
LEFT JOIN dbo.tblEvent e  on t.n = datepart(hh, e.EventTime)
GROUP BY t.n
ORDER BY t.n

Je crois que le crédit va aux liens suivants, je pense que c'est là que je suis tombé sur ce sujet:

http://www.sqlservercenter.com/articles/t-sql/62867/

http://www.sqlservercenter.com/articles/t-sql/74118/

10
Henry Lee

Premièrement, mes excuses pour le retard dans ma réponse depuis mes derniers commentaires.

Le sujet est arrivé dans les commentaires qui utilisent une CTE récursive (RCTE d'ici) fonctionne assez rapidement en raison du faible nombre de lignes. Bien que cela puisse paraître comme cela, rien ne pourrait être plus loin de la vérité.

Build Table Table et Fonction Tally

Avant de commencer les tests, nous devons construire une table de manœuvre physique avec l'indice en cluster à la cluster approprié et une fonction de style Itzik Ben-Gan. Nous ferons également tout cela dans TEMPDB afin de ne pas laisser tomber accidentellement les friandises de personne.

Voici le code pour construire la table de contact et ma version de production actuelle du magnifique code 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

À propos de ... Notez que la table construite d'un million et d'une rangée de rangée et a ajouté un index en regroupement dans environ une seconde environ. Essayez-le avec un RCTE et voyez combien de temps il faut! ;-)

Construire des données de test

Nous avons également besoin de données de test. Oui, je conviens que toutes les fonctions que nous allons tester, y compris le RCTE, courent dans un milliseconde ou moins pour seulement 12 rangées, mais c'est le piège que beaucoup de gens tombent dans. Nous parlerons plus de ce piège plus tard, mais pour l'instant, permettez-vous de simuler chaque fonction de 40 000 fois, ce qui concerne combien de fois certaines fonctions dans ma boutique sont appelées dans une journée de 8 heures. Imaginez simplement combien de fois ces fonctions pourraient être appelées dans une grande entreprise de vente au détail en ligne.

Ainsi, voici le code pour construire 40 000 lignes avec des dates aléatoires, chacun ayant un numéro de ligne juste à des fins de suivi. Je n'ai pas pris le temps de faire le temps ensemble d'heures car cela n'a pas d'importance ici.

--===== 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)
;

Construisez certaines fonctions pour faire le truc de 12 lignes

Ensuite, j'ai converti le code RCTE en une fonction et crée 3 autres fonctions. Ils ont tous été créés comme ITVFS hautes performances (fonctions de valeurs de table en ligne). Vous pouvez toujours raconter parce que ITVFS n'a jamais eu de commencement en eux comme scalaire ou MTVFS (fonctions de valeurs de table multi-instructions).

Voici le code pour construire ces 4 fonctions ... je les ai nommés après la méthode qu'ils utilisent et non ce qu'ils font pour faciliter la tâche de les identifier.

--=====  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

Créez le harnais de test pour tester les fonctions

Enfin, nous avons besoin d'un harnais de test. Je fais un chèque de base, puis testez chaque fonction de manière identique.

Voici le code pour le harnais de test ...

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

Une chose à remarquer dans le harnais de test ci-dessus est que je shunt Toutes la sortie dans des variables "jetables". Pour essayer de garder les mesures de performance aussi pures que possible sans aucune sortie sur les résultats de disque ou d'angoissons.

n mot de prudence sur les statistiques définies

En outre, un mot de prudence pour les testeurs potentiels ... Vous ne devez pas utiliser de statistiques définies lors du test des fonctions scalaire ou MTVF. Il ne peut être utilisé que sur des fonctions ITVF que celles de ce test. Définir des statistiques a été prouvée pour que les fonctions scalaires gèrent des centaines de fois plus lentement qu'ils ne le font en réalité. Oui, j'essaie d'incliner un autre moulin à moulin, mais ce serait un tout "Nuther Longueur de l'article et je n'ai pas le temps pour cela. J'ai un article sur sqlservercentral.com parlant tout à propos de cela, mais il n'a aucun sens dans la publication du lien ici parce que quelqu'un sera tout plié en forme à ce sujet.

les résultats du test

Donc, voici les résultats du test lorsque j'exécute le harnais de test sur mon petit ordinateur portable I5 avec 6 Go 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.

La "sélection de base", qui sélectionne uniquement les données (chaque ligne créée 12 fois pour simuler le même volume de retour), est venue à droite environ 1/5ème d'une seconde. Tout le reste est arrivé à environ un quart de seconde. Eh bien, tout sauf que la fonction sanglante RCTE. Il a fallu 4 et 1/4 secondes ou 16 fois plus longtemps (1 600% plus lent).

Et regardez les lectures logiques (mémoire Io) ... Le RCTE a consommé une réduction de 2 960 000 000 (près de 3 millions de lectures) alors que les autres fonctions n'ont consommé qu'environ 82 100. Cela signifie que le RCTE a consommé plus de 34,3 fois plus de mémoire IO que toutes les autres fonctions.

pensées de fermeture

Résumons. La méthode RCTE pour faire cette "petite" chose 12 lignes utilisée 16 fois (1 600%) plus de processeur (et de durée) et 34,3 fois (3 430%) plus mémoire IO que toutes les autres fonctions .

Heh ... Je sais ce que tu penses. (( "Big Deal! Ce n'est qu'une seule fonction."

Ouais, accepté, mais combien d'autres fonctions avez-vous? Combien d'autres endroits en dehors des fonctions avez-vous? Et avez-vous un de ceux qui fonctionnent avec plus de 12 rangées à chaque course? Et, est-ce qu'il y a une chance que quelqu'un dans une méthode d'une méthode puisse copier ce code RCTE pour quelque chose de beaucoup plus grand?

Ok, le temps d'être émoussé. Cela ne convient absolument aucun sens pour que les personnes justifient la performance du code remis en cause à cause des comptes de rangs ou une utilisation limitée supposés. Sauf lorsque vous achetez une case MPP pour peut-être des millions de dollars (sans oublier les dépenses de la réécriture du code pour que cela fonctionne pour fonctionner sur une telle machine), vous ne pouvez pas acheter une machine qui exécute votre code 16 fois plus rapide (SSD Won 't le faire soit ... Tout ce genre de choses était en mémoire à grande vitesse lorsque nous l'avons testé). La performance est dans le code. Bonne performance est en bon code.

Pouvez-vous imaginer si tout votre code a été "juste" 16 fois plus vite?

Ne justifiez jamais de mauvais ou de la performance du code mis au défi sur des encombrements faibles ou même une utilisation basse. Si vous le faites, vous devrez peut-être emprunter l'un des moulins à vent que j'étais accusé d'inclinaison pour garder vos processeurs et vos disques suffisamment cool. ;-)

n mot sur le mot "taly"

Oui je suis d'accord. Sémantiquement parlant, la table de manœuvre contient des chiffres, pas des "compteurs". Dans mon article original sur le sujet (ce n'était pas l'article original sur la technique, mais c'était mon premier dessus), je l'ai appelé "Tally" non pas à cause de ce qu'il contient, mais à cause de ce qu'il fait ... c'est utilisé pour "compter" au lieu de boucler et à "Tally" quelque chose est de "compter" quelque chose. ;-) Appelez-le ce que vous voulez ... Table de numéros, Table de notification, Table de séquence, peu importe. Je m'en fiche. Pour moi, "Tally" est plus de sens plein et, étant un bon dba paresseux, contient seulement 5 lettres (2 sont identiques) au lieu de 7 et il est plus facile de dire pour la plupart des gens. C'est aussi "singulier", qui suit ma convention de dénomination pour les tables. ;-) C'est aussi ce que l'article contenait une page à partir d'un livre à partir des années 60. Je vais toujours y réfléchir comme une "table de contact" et tu sauras toujours ce que je ou quelqu'un d'autre signifie. J'évite également la notation hongroise comme la peste, mais j'ai appelé la fonction "Fntalement" afin que je puisse dire "Eh bien, si vous avez utilisé la fonction EFF-EN Tally, je vous ai montré, vous n'auriez pas un problème de performance" sans que cela soit réellement Violation des ressources humaines. ;-)

Ce que je suis plus préoccupé, c'est que les gens apprennent à l'utiliser correctement au lieu de recourir à des choses telles que la performance remise en cause des cordes et d'autres formes de RBAR cachée.

7
Jeff Moden

Vous aurez besoin de RIGHT JOIN Vos données avec une requête renvoyant un enregistrement pour chaque heure dont vous avez besoin.

Voir - this pour quelques façons d'obtenir des numéros de ligne que vous pourriez ensuite soustraire à partir de l'heure actuelle.

Dans Oracle une requête hiérarchique sur Dual générera des rangées:

SELECT to_char(sysdate-level/24,'HH24') FROM dual CONNECT BY Level <=24;
2
Leigh Riffel