2017-02-19

TSQL Recipes: Send Dynamic Query Result through Email

How many emails do you need to trigger from the server every day? How many of them are just single row query result? Well, I have to send a lot html formatted, single row query result to clients. Not funny at all. So if you are doing the same thing, today's recipe could save you a bit.

You don't need to read this post line by line, to save your time, below is the definition. If you are an expert in dynamic query, you should know what I am doing fairly quick. In case my sp is too complicated, I put my notes after the procedure.

 
Create Procedure [dbo].[sp_SendVerticalHTMLEmail]
@Query nvarchar(max),
@CSSStyle nvarchar(max),
@EmailTo nvarchar(500),
@EmailCc nvarchar(500),
@EmailSubjct nvarchar(255),
@EmailAttachments nvarchar(max)
as
BEGIN
 SET NOCOUNT ON

 declare @id int = 0

 BEGIN TRY  
  declare @EmailBody nvarchar(max) = '', @cmd nvarchar(max) = '' 

  IF @CSSStyle = '' OR @CSSStyle is null
  SET @CSSStyle = 'table {border-collapse: collapse; font-size:90%}table, td, th {border: 1px solid black;}td.header {font-weight: bold}'
  
  select @cmd += 'IF object_id (''tempdb.dbo.#Tmp'') IS NOT NULL DROP Table tempdb.dbo.#Tmp;'+ CHAR(10) + char(13)

  select @cmd += 'select * INTO #Tmp FROM ( ' + @query + ') tmp;' + CHAR(10) + char(13)

  select @cmd +='declare @tmpSql nvarchar(max) = ''''
  select @tmpSql += 
  ''SELECT '''''' + name + '''''' as R1, (select CAST('' + QUOTENAME(CAST(name as varchar(255)), ''[]'') + '' as varchar(1000)) from #tmp) as R2   UNION ALL '' + CHAR(10) + char(13)
  from tempdb.sys.columns where object_id = object_id(''tempdb.dbo.#Tmp'');

  select @tmpSql = left(@tmpSql, len(@tmpSql) - 12);

  declare @var nvarchar(max) = ''''
  select @var =''SELECT @body = ('' + CAST(''SELECT td = R1, '''''''', td = R2 from ('' + @tmpSql + '') t FOR XML PATH(''''tr'''')'' as nvarchar(max)) + '')''

  exec sp_executesql @var, N''@body nvarchar(max) OUTPUT'', @body output' 

  --select @cmd

  exec sp_executesql @cmd, N'@body nvarchar(max) output', @EmailBody output

  select  @EmailBody = '' + @EmailBody + '
' exec msdb..sp_send_dbmail @recipients = @EmailTo, @copy_recipients = @EmailCc, @subject = @EmailSubjct, @body = @EmailBody, @body_format = 'HTML', @file_attachments = @EmailAttachments, @mailitem_id = @id output END TRY BEGIN CATCH select @id = -1 END CATCH RETURN @id END


So here is my notes, The basic concept is like this:

1. I want to have a sp which can accept a query statement, wrap my query result, and send through in the email body right away. I am targeting single row query result because to me, most multi row result sets need to be handled separately. But single row result is just like a short and quick notification.

2. Now I know I am looking at single row result set. But I have no idea how many columns a query can return. So the eaist way is turning the result set 90 degrees to become a vertical two column result set, the first column is the original column header, and the second column is the row contents.

3. To read the column titles from the result set, I load the query result into a temp table by calling a dynamic query:

 
select @cmd += 'select * INTO #Tmp FROM ( ' + @query + ') tmp;' + CHAR(10) + char(13)


4. Now think in this way, from the temp table, I can get a two-row result set: the first row is the column titles of the temp table, and the second row is the original data row. It is just a simple information schema query, and an union statement. So for the temp table, we can simple write down

 
select * from sys.columns where object_id = object_id('tempdb.dbo.#Tmp')
union all
select * from tempdb.dbo.#Tmp


5. Once we have the two row result set, we can then use a for XML clause to generate html email. Something just like:

 
SELECT @body = (SELECT td = R1, '', td = R2 from (select * from myTable) t FOR XML PATH('tr'))


6. Everything looks pretty simple, isnt it? Hold on for one second. Well, the difficult part of the procedure is that, we have to get the dynamic result set within a dynamic execution context. Get confused? Think about it, starts from step 4, we transform the resultset by referring #Tmp. But this temp table was generated by a dynamic statement in step 3. So to make temp table context available to the following execution steps, we have to wrap the procedure "sp_executesql" within the dynamic statement. This is the reason you can see two "sp_executesql" calls within my procedure.

7. the last point, you might have noticed, I named my procedure "sp_xxx", not a good practice as this pattern leads to bad practice. But in my case, I want to have this sp available to all my databases, so I registered this procedure as a system procedure by calling

 
sp_ms_marksystemobject 'sp_SendVerticalHTMLEmail'


So now we can have a test





and the 2nd test





Enjoy it :)

2017-01-26

Sending Email by Gmail Service

In my previous post, we had tried to send email through O365. Today in my first post in 2017, let's try to send an email through gmail service.

Firstly let's look at configuration part. First of the first, we need a google account :p, then we need to turn on the less secure apps setting for the google account. To do that, we can go to the setting page, and then select "turn on".



Pretty much it is the only configuration we need to do at google account level.Very simple, isn't it? Now let's see the coding part. Open a new console program project in Visual Studio, add system.net and system.net.mail into the using section, and then add codes below. Run the program, you should have the email in your mail box.

            //add your email account & password here
            string username = "your account name";
            string password = "your password";

            string emailbody = "some text";

            var client = new SmtpClient("smtp.gmail.com", 587)
            {
                Credentials = new NetworkCredential(username, password),
                EnableSsl = true
            };

            try
            {              
                MailMessage msg = new MailMessage();
                msg.From = new MailAddress(username);

                //add a to recipient here
                msg.To.Add("recipient@emaildomain.com.au");

                msg.Subject = "test html from google account";
                msg.Body = emailbody;
                msg.IsBodyHtml = true;

                Console.WriteLine(DateTime.Now.ToLongTimeString());
                client.Send(msg);
                Console.WriteLine("Sent");
                Console.WriteLine(DateTime.Now.ToLongTimeString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            
            Console.ReadLine();



Now, we can make the thing even easier if we want to send emails from SQL Server. What we need to do is just add the gmail into database's mail profile.

Find the Database Mail node in SQL Server Management Studio and select "configure database mail" option



Leave the option to the "Set up Database Mail by performing the following tasks" and press next button



on the next screen, input a profile name, then click add button to add a SMTP account



We need to select an account for the new profile. Since we have not created one for our gmail account, click the "New Account" button



Now we are on the window of "New Database Mail Account", give this account a name by filling in account name. Then add your gmail account information into the fields required, as demonstrated below:



Once all fields are completed, click the OK button will close current account setting window and bring you back to the profile setting window.



The next window will ask you to select a default profile. I don't want any change to the system default profile, so just level it as is and press next button.



Now the next screen asks us to adjust parameters for the profile, leave every to default and press next button



The final screen just shows us a summary of what SQL Server will do for us. Press finish button and SQL Server will set everything up for you.





We can then do a simple test. Right click the Database Mail node and select "Send Test E-mail"



From the dropdown box, select our newly created gmail profile, provide a to recipient, then press "Send Test E-mail".



Now check your mail box, you've got mail :D

2016-12-20

TSQL Recipes: Distributing Time by Interval

This should be the last post in 2016. Before the post Merry Christmas and Happy New Year 😊

In the previous post we tried to build a time range by merging time segments. Today let's look at another typical business scenario: distributing time to different slots.

As usual, to make the concept simple, let's look at screenshot below. What I have here is a two column result set, The first column is a date time column which indicates the action start date time, and the 2nd column shows total duration in seconds since the start date time.



Saving the data in such a format definitely is fine. But what if the end user wants to do an analysis by hour? Something like below?



Or even better, flexible output by time interval setting (this time the interval is 15 minutes)?



So this is the topic today. Firstly let's create a few variables

 declare @BeginDateTime datetime = '20161201 00:02:06' -- begin datetime
 declare @Duration int = 17894 -- seconds
 declare @Interval int = 10 --minutes
 declare @Result TABLE 
 (
 BeginDateTime datetime,
 Duration int,
 OutputTimeWindow datetime,
 SecondsLeft int,
 SecondsInWindow int
 )


Hopefully declarations made above make sense to you. If not, basically we need to take the @BeginDateTime as an anchor and then split duration by intervals, and output the result into the result table.

After the declaration, what we need to do is to get some transformed variable: logic is simple when we break down procedures step by step.

declare @BaseDate datetime = CAST(CAST(@BeginDatetime as date) as datetime)  --based date from @BeginDateTime
declare @MinutesToBaseDate  int = DATEDIFF(MINUTE, @BaseDate, @BeginDateTime) --minutes differences between @BeginDatetime and base date
declare @AmountOfIntervals int = @MinutesToBaseDate / @Interval --how many intervals we have


Starts from here, we can begin to build the initial time window

declare @DtWindow datetime = DATEADD(MINUTE, @Interval * @AmountOfIntervals, @BaseDate)
declare @NextDtWindow datetime = dateadd(MINUTE, @interval * (@AmountOfIntervals + 1), @BaseDate)


To make logic simple, I did an initial check to see if the initial time window is sufficient enough to hold the whole duration

IF DATEADD(SECOND, @Duration, @BeginDatetime) < @NextDtWindow 
BEGIN
  insert into @Result(BeginDateTime, Duration , OutputTimeWindow, SecondsLeft, SecondsInWindow)
  select @BeginDatetime, @Duration, DATEADD(MINUTE, @Interval * @AmountOfIntervals, @BaseDate), 0, @Duration
END


Now here is the important part, what if the initial time window is not big enough? What I did is creating a recursive cte and deduct duration by amount of intervals:

;with cte as 
(
select @BeginDatetime as BeginDatetime, @Duration as Duration
    , DATEADD(MINUTE, @Interval * (DATEDIFF(MINUTE, CAST(CAST(@BeginDateTime as date) as datetime), @BeginDateTime) / @Interval), @BaseDate) as DtWindow --base bucket
    , @Duration as SecondsLeft
    , DATEDIFF(SECOND, @BeginDatetime, @NextDtWindow) as SecondsInWindow
union all
select BeginDatetime, Duration
    , DATEADD(MINUTE, @Interval, DtWindow)
    , SecondsLeft - SecondsInWindow
    , IIF(SecondsLeft - SecondsInWindow > @Interval * 60, @Interval * 60, SecondsLeft - SecondsInWindow)
from cte
where SecondsLeft - SecondsInWindow > 0
)

insert into @Result(BeginDateTime, Duration , OutputTimeWindow, SecondsLeft, SecondsInWindow)
select BeginDatetime, Duration, DtWindow, SecondsLeft, SecondsInWindow
from cte


This is all we need. At the end of this post there is a table valued function I created by merging all steps listed above. You can call the function like below:

declare @t table (DT datetime, Duration int)
insert into @t(DT, Duration)
values
('2016-12-01 00:02:06', 17894),
('2016-12-01 05:00:20', 3482),
('2016-12-01 18:00:08', 920),
('2016-12-01 18:15:28', 23)

declare @interval int = 30 --half hour

select x.*
from @t t cross apply dbo.ufnDistributeTimeByInterval(@interval, t.DT, t.Duration) x




Do some tests by changing the @interval variable value. Enjoy it and see you next year.



CREATE function [dbo].[ufnDistributeTimeByInterval](@Interval int, @BeginDateTime datetime, @Duration int)
RETURNS @Result TABLE 
(
 BeginDateTime datetime,
 Duration int,
 OutputTimeWindow datetime,
 SecondsLeft int,
 SecondsInWindow int
)
AS
BEGIN

 declare @BaseDate datetime = CAST(CAST(@BeginDatetime as date) as datetime)  --based date from @BeginDateTime
 declare @MinutesToBaseDate  int = DATEDIFF(MINUTE, @BaseDate, @BeginDateTime) --minutes differences between @BeginDatetime and base date
 declare @AmountOfIntervals int = @MinutesToBaseDate / @Interval --how many intervals we have

 declare @DtWindow datetime = DATEADD(MINUTE, @Interval * @AmountOfIntervals, @BaseDate)
 declare @NextDtWindow datetime = dateadd(MINUTE, @interval * (@AmountOfIntervals + 1), @BaseDate)
 
 IF DATEADD(SECOND, @Duration, @BeginDatetime) < @NextDtWindow 
 BEGIN
  insert into @Result(BeginDateTime, Duration , OutputTimeWindow, SecondsLeft, SecondsInWindow)
  select @BeginDatetime, @Duration, DATEADD(MINUTE, @Interval * @AmountOfIntervals, @BaseDate), 0, @Duration
 END
 ELSE
 BEGIN
  ;with cte as 
  (
   select @BeginDatetime as BeginDatetime, @Duration as Duration
    , DATEADD(MINUTE, @Interval * (DATEDIFF(MINUTE, CAST(CAST(@BeginDateTime as date) as datetime), @BeginDateTime) / @Interval), @BaseDate) as DtWindow --base bucket
    , @Duration as SecondsLeft
    , DATEDIFF(SECOND, @BeginDatetime, @NextDtWindow) as SecondsInWindow
   union all
   select BeginDatetime, Duration
    , DATEADD(MINUTE, @Interval, DtWindow)
    , SecondsLeft - SecondsInWindow
    , IIF(SecondsLeft - SecondsInWindow > @Interval * 60, @Interval * 60, SecondsLeft - SecondsInWindow)
   from cte
   where SecondsLeft - SecondsInWindow > 0
  )

  insert into @Result(BeginDateTime, Duration , OutputTimeWindow, SecondsLeft, SecondsInWindow)
  select BeginDatetime, Duration, DtWindow, SecondsLeft, SecondsInWindow
  from cte

 END --end of if else

 RETURN

END --end of function