Monday, April 11, 2016

How can I run my custom function and query in a loop for different time frames?

Leave a Comment

I am writing a function to calculate the total number of seconds a user was online at my website. Afterwards, I convert the number of seconds to hh:mm:ss:

select * into #temp from [MyFunction](timestamp1, timestamp2);  select u.Name,         convert(varchar(8), t.Seconds / 3600) + ':'              + right('0', convert(varchar(2) t.Seconds % 3600/60), 2) + ':'              + right('0', convert(varchar(2) t.Seconds % 60), 2)     as [Total Time] from #temp t left join Users u     on t.UserID = u.UserID; 

Where an example timestamp is 2016-04-01 00:00:00.000. What I want now, is to see total time spent on my website, not on 1 range, but a sequence of ranges, for instance:

2016-01-01 to 2016-01-15 2016-01-16 to 2016-01-31 2016-02-01 to 2016-02-15 

Is it possible to put my code in a dynamic query to calculate all of these ranges by running the same code every time?

The output of my code above is:

Name    [Total Time] -------------------- Anton   6:34:55 Bert    5:22:14 

What I would like is an output such as

Name    [Period_1] [Period_2] [Period_3] [Period_4] --------------------------------------------------- Anton   6:34:55    5:00:22    null       10:44:32 Bert    5:22:14    null       null        9:22:53 

So each range, or loop over the code, should be a column.

I believe pivot() will help me here, but any help kickstarting me with the dynamic SQL (or any better solution) would be greatly appreciated.

3 Answers

Answers 1

Wrap your current code into a procedure with parameters, something like:

CREATE PROCEUDRE dbo.CalcTime   @Period       varchar(100)  --  Name of the period  ,@PeriodStart  datetime      --  Period starts  ,@PeriodEnd    datetime      --  Period ends 

and using appropriate datatypes.

Next, create a second procedure. Within this one, define another temporary table, like

CREATE TABLE #Results  (    Name       varchar(100)  not null  --  Or however long it might get   ,Period     varchar(100)  not null  --  Ditto   ,TotalTime  int           null      --  *  ) 

Loop over every period you wish to define data for. For each period, call the "CalcTime" stored procedure, and dump the results into the temp table. Two ways to do this, use

INSERT #Results  execute dbo.CalcTime  'Period', 'Jan 1, 2016', 'Jan 15, 2016' 

or, having defined the temp table in the calling procedure, you can reference it in the called procedure in a standard INSERT... SELECT... statement.

Also within the loop, build a comma-delimited string that lists all your period labels, e.g.

SET @AllPeriodLabels = isnull(@AllPeriodLabels + ',', '') + @ThisPeriodLabel 

or,

SET @AllPeriodLabels = isnull(@AllPeriodLabels + ',', '') + '[' + @ThisPeriodLabel + ']'  --  ** 

Use this to build the dynamic SQL pivot statement against the temp table, and you’re done. As mentioned in the comments, there are any number of SO posts on how to do that, and here are links to two: The first discusses building a dynamic pivot statement, and the second uses similar tactics for an unpivot statement.


* Avoid embedded spaces in object names, they will only give you pain.

** Ok, Sometimes you have to do it.

Answers 2

Two pseudo tables:

persons:   personId int   lastname nvarchar(50)  visits:   personid int   created datetime   duration int -- (store things like duration in seconds) 

First make a list of the columns, here I used a created column and converted it to a month period. So the result is something like: [201501],[201502],[201503],....

declare     @cols nvarchar(max) set         @cols = STUFF((select ',' + quotename(convert(VARCHAR(6), created, 112))                         from        visits                         group by    convert(VARCHAR(6), created, 112)                         order by    convert(VARCHAR(6), created, 112)                     for xml path(''), type).value('.', 'nvarchar(max)'), 1, 1, '') 

I need dynamic SQL to fill in the variable number of COLs, I suggest you start with NON dynamic SQL, make it dynamic should be the last step.

declare     @sql nvarchar(max) set         @sql = N'                     select      *                     -- lazy create a temp so you don't have to bother about the column definitions                     -- into #temp                      from (                         select      p.lastname, convert(VARCHAR(6), created, 112) as period                         -- this is optional to get a Grand Row total                         -- ,(select sum(duration) from visits v where v.personId = p.personId) as total                         from        visits v                                     inner join persons p on v.personId = p.personId                     ) src                     pivot (                         sum(duration) for period in (' + @cols + ')                     ) pvt;             ' 

Well you can print this for verification or run it ...

exec sp_executesql @sql 

You can make a twist by dumping the result in a temp table (created on the fly). That creates the opportunity to add extra columns for output, like an organization etc. etc..

alter table #temp add organization nvarchar(100) 

Good luck !

Answers 3

Here is a working test code. Adapt it as you see fit.

Setup:

-- create test tables CREATE TABLE Users    (       UserId   INT,       UserName NVARCHAR(max)    )   CREATE TABLE Access    (       UserId    INT,       StartTime DATETIME2,       EndTime   DATETIME2    )   CREATE TABLE Periods    (       NAME      NVARCHAR(max),       StartTime DATETIME2,       EndTime   DATETIME2    )   go   -- function to format the time CREATE FUNCTION ToTime(@SECONDS BIGINT)  returns NVARCHAR(max)  AS    BEGIN        RETURN CONVERT(VARCHAR(8), @SECONDS / 3600) + ':'               + RIGHT('00'+CONVERT(VARCHAR(2), @SECONDS % 3600/60), 2)               + ':'               + RIGHT('00'+CONVERT(VARCHAR(2), @SECONDS % 60), 2)    END   go   -- populate values INSERT INTO Users  VALUES     (1, 'Anton'),             (2,'Bert')   DECLARE @I INT=100  DECLARE @D1 DATETIME2  DECLARE @D2 DATETIME2   WHILE ( @I > 0 )    BEGIN        SET @D1=Dateadd(second, Rand() * 8640000, Getdate())        SET @D2=Dateadd(second, Rand() * 1000, @D1)         INSERT INTO Access        VALUES     (Floor(Rand() * 2 + 1), @D1, @D2);        SET @I=@I - 1    END   SET @I=1  SET @D1=Getdate()   WHILE ( @I < 6 )    BEGIN        SET @D2=Dateadd(day, 15, @D1)         INSERT INTO Periods        VALUES     (Concat('Period_', @I),                    @D1,                    @D2);         SET @D1=@D2        SET @I=@I + 1    END   go  

Working code:

-- Getting the values DECLARE @COLS NVARCHAR(max)   SET @COLS = Stuff((SELECT ',' + Quotename(NAME)                     FROM   Periods                     GROUP  BY NAME                     ORDER  BY NAME                     FOR xml path(''), type).value('.', 'nvarchar(max)'), 1, 1, ''              )   DECLARE @SQL NVARCHAR(max)   SET @SQL = N'SELECT * FROM (             SELECT     u.UserName,                        p.Name                                                  AS Period,                       dbo.Totime(Sum(Datediff(SECOND,a.StartTime,a.EndTime))) AS [Time]             FROM       Access a             INNER JOIN Users u             ON         a.UserId=u.UserId             INNER JOIN Periods p             ON         p.StartTime<=a.StartTime             AND        a.StartTime<p.EndTime             GROUP BY   u.UserName,                        p.Name ) x PIVOT ( Max([Time]) FOR Period IN (' + @COLS +')  ) p;'   --PRINT @SQL   EXECUTE(@SQL)  
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment