1

This question is the follow-up of this thread

Given that I've created two simple tables User and Item:

  • User (Id, Name, Email)
  • Item (Id, CreatedDate, EmailSent, UserId)

I am able to create an SQL Server Agent job that periodically runs the following script:

USE test
DECLARE @emails varchar(1000)
SET @emails = STUFF((SELECT ';' + u.Email FROM [user] u JOIN [item] i 
ON u.ID = i.UserId 
WHERE i.EmailSent = 0 AND DATEDIFF(day, i.CreatedDate, GETDATE()) >= 30 
FOR XML PATH('')),1,1,'')

/** send emails **/
EXEC msdb.dbo.sp_send_dbmail 
@profile_name='test',
@recipients=@emails,
@subject='Test message',
@body='This is the body of the test message.'

The purpose is to get any item that has been created for >= 30 days and then send a reminder email to its user. The EmailSent is checked first to exclude items already reminded.

I want to improve this script as follows:

  1. There's a separate query that does the SELECT part and stores the result into a variable so that it can be reused in the email sending query, and in an UPDATE query that sets selected items' EmailSent to True (done after emails sent).
  2. Customise the title and body of the message with user's Name and item's Id. Something like: Dear Name, the item Id has been created for 30 days or more.

Anybody has any idea of how these improvement could be done?

Community
  • 1
  • 1
tab87vn
  • 389
  • 5
  • 17
  • Here's some basic code to concatenate a string into a variable. `DECLARE @S VARCHAR(100); SET @S = 'Dear ' + @Name`. However you seem to be sending to multiple people so that won't make much sense – Nick.Mc Nov 30 '16 at 11:19
  • True, I think it's more efficient to send emails to multiple recipients at a time than to send one each time using a loop. This unfortunately makes the string concatenation more complicated. – tab87vn Nov 30 '16 at 11:55
  • The point is, when you send an email to 5 people, who goes in the subject line? – Nick.Mc Nov 30 '16 at 12:48
  • My bad, I was thinking about some kind of template, but that doesn't seem applicable here. Anyway, an email with general title and body might still work for the the reminder/notification purpose. Do you have any idea for the first improvement? – tab87vn Nov 30 '16 at 13:26
  • I don't quite understand your first request. Maybe you should post the query. If you want to mark emails as sent you should do it like this: 1. Identify a batch to be sent and update them from 0 to 1; 2. Send all the emails marked as 1; 3. If successful. update all the 1's to 2's. This way you don't get any data inconsistencies – Nick.Mc Nov 30 '16 at 13:49
  • @Nick.McDermaid yes, that's the idea. Basically I want to have a `SELECT` query, once, to get all eligible items first then apply the sending email and update operations on those items. The `SELECT` query would be something like: @items = `SELECT * FROM [user] u JOIN [item] i ON u.ID = i.UserId WHERE i.EmailSent = 0 AND DATEDIFF(day, i.CreatedDate, GETDATE()) >= 30` How would I then get the concatenated email string from this `items` and do the update on their `EmailSent`? – tab87vn Nov 30 '16 at 16:07
  • I am not aware of an easy way in SQL Server to parse out multiple rows from the SELECT so as to generate individual emails to the sp_send_dbmail procedure. Instead, your SELECT could be made to use the "TOP 1" qualifier so that it only returns at most 1 row. Then, you might use the SQL Server Agent feature of having 2 steps instead of just 1. The first step is always run based on your schedule. The second step reschedules an immediate run of this job IF the first step successfully found one row that had expired. So the job would continue to resubmit itself until no more expired rows are found. – JohnH Nov 30 '16 at 17:24
  • If I understand your query correctly you are _already_ getting a concatenated list of emails using `stuff` so Just reuse that approach to get a concatenated list of items. Now that you have that, how do you want this represented in an email? if you have *one* email sent to seven people with fourteen items, how do you want that represented in the email? It would help for you to explain the relationships of the objects. The good news is that all of this is totally achievable with cursors etc. but I don't understand the relationships and cardinality of email addresses, email messages and items – Nick.Mc Dec 01 '16 at 01:24
  • @Nick.McDermaid I wanted to just have a general notification email sent to everybody, regardless of how many items one has. But after having carefully considered the requirement, this won't make a lot of sense. So it seems like I'd need to send a personalised mail (with specific user's `Name` and item `Id`) to each of the people that have at least an expired item. Could I just put them into a `WHILE` loop in T-SQL? – tab87vn Dec 01 '16 at 09:48
  • Here's an example of using a cursor to send emails: http://stackoverflow.com/questions/21246266/send-email-from-sql-server-for-each-row-of-a-dataset. One enhancement I would suggest is that you first pre-identify the records you are processing with and update statement and set them to 1, then only process the ones, and update them to 2 as you go. Then 0=unprocessed, 1=in progress, 2=complete – Nick.Mc Dec 02 '16 at 03:17
  • @Nick.McDermaid I agree checking the sending status from the log would be more thorough as `sp_send_dbmail` would return `0` (success code) even when the mails are not delivered by the SMTP server. I would want to loop through all the selected items, sending one email per item and only when the status checking guarantees the sending success will the sending status be updated to `2` and the it is logged into my own email table (with the datetime the email was sent-- how, though?) – tab87vn Dec 02 '16 at 09:13
  • I suggest you try and get the basics working from my link. Then all you need to do is sprinkle in appropriate `update` statements in the right place – Nick.Mc Dec 02 '16 at 09:17
  • @Nick.McDermaid Please have a look at my current progress in a separate answer, maybe it's similar to the post you suggested. – tab87vn Dec 02 '16 at 10:07

1 Answers1

0

I've managed to have this so far

USE test
-- 1) Update selected items to state 1
UPDATE [PickingList]
SET ReminderState = 1
WHERE ReminderState = 0 AND DATEDIFF(day, CreatedDate, GETDATE()) >= 30

DECLARE @userId INT
DECLARE @email NVARCHAR(100)
DECLARE @plId INT
DECLARE @getId CURSOR

-- 2) Process those having state 1
SET @getId = CURSOR FOR
SELECT u.ID, u.Email, pl.ID
FROM [User] u JOIN [PickingList] pl
ON u.ID = pl.UserId
WHERE pl.ReminderState = 1

OPEN @getId
FETCH NEXT
FROM @getId INTO @userId, @email, @plId
WHILE @@FETCH_STATUS = 0
BEGIN
    /** send emails **/
    DECLARE @pId VARCHAR(1000)
    DECLARE @title VARCHAR(1000)
    DECLARE @body VARCHAR(8000)
    SET @pId = CAST(@plId AS VARCHAR(16))
    SET @title = @pId + ' was created more than 30 days ago'
    SET @body = 'The following picking list ' + @pId + ' blah blah'

    DECLARE @code INT
    SET @code = -1
    EXEC @code = msdb.dbo.sp_send_dbmail 
    @profile_name='test',
    @recipients=@email,
    @subject=@title,
    @body=@body

    -- 3) Log the email sent and update state to 2
    -- Below is what I want to do, but ONLY when the it can be sure that 
    -- the email has been delivered
    INSERT INTO [PickingListEmail] (UserId, PickingListId, SentOn) 
    VALUES (@userId, @plId, GETDATE())
    UPDATE [PickingList] SET ReminderState = 2 WHERE ReminderState = 1

    FETCH NEXT
    FROM @getId INTO @userId, @email, @plId
END
CLOSE @getId
DEALLOCATE @getId

In step (3), before saving the email sent and update the item to state 2 (processed), I would want to make sure that the email has been sent, based on the data fetched from sysmail_log, as sp_send_dbmail would only care about whether it can send the mail to the SMTP server, so will return the success code 0 even when the sending fails.

Something like this:

meaning of the values of sent_status on msdb.dbo.sysmail_mailitems

or

Check If sp_send_dbmail Was Successful

Community
  • 1
  • 1
tab87vn
  • 389
  • 5
  • 17
  • Looks like you need to include the `@mailitem_id` output parameter (example here https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9c724a9e-4eb0-413e-8ef1-52cd3395cd57/updating-a-table-with-an-output-of-stored-procedure?forum=transactsql). Then you get an id for each email. Then you can use that to find the entry in sysmail_mailitems Only problem is you may not know for 24 hours if the email failed. But at least you'll have an idea within 5 minutes whether it was successful – Nick.Mc Dec 02 '16 at 10:29
  • Looking up a `mailitem_id` within the `sysmail_faileditems` table allows us to know almost immediately which email fails to be sent by SqlServer, however, I find that the `sysmail_event_log` table records what might go wrong with the actual mail server (which might only be resolved after 24 hours as you suggested). – tab87vn Dec 02 '16 at 11:24
  • That's good. So you can probably pick up (and log) any late arriving error messages in your next execution of the SP – Nick.Mc Dec 02 '16 at 11:27