Sending an HTML Table by Email With sp_send_dbmail

An HTML table turns a wall of email text into a report people actually read. Build the rows with FOR XML PATH so odd characters in your data cannot break the markup, then hand the finished body to Database Mail.

A cannoli forming tube beside a filled pastry shell and an empty matching shell

Why plain text reports get ignored

Picture the daily file size report. It lands in your inbox at 7 AM as forty lines of text that wrap badly. You glance at it, close it, and tell yourself you will read it later.

Later never comes. Then one morning a log file has grown ten times and nobody noticed. A small table with a header row and aligned columns gets read. It costs about ten lines of T-SQL.

The demo below uses a temp table with three made-up files. Two of them have names that cause trouble in HTML, and one has no size at all. Real data does this to you eventually.

The obvious way breaks quietly

Most people start by gluing strings together. Look at the output. The ampersand and the angle brackets go into the markup untouched. A browser would treat <Log> as a tag, so that cell would look empty. The third row is NULL, because one NULL size wipes out the whole concatenated string.

DROP TABLE IF EXISTS #DbFiles;
CREATE TABLE #DbFiles (
    FileId   int PRIMARY KEY,
    FileName nvarchar(100) NOT NULL,
    SizeMb   decimal(10,1) NULL);

INSERT #DbFiles VALUES
    (1, N'Data & Archive', 12.5),
    (2, N'<Log>', 2.0),
    (3, N'Temp', NULL);

SELECT N'<tr><td>' + FileName + N'</td><td>'
       + CONVERT(nvarchar(20), SizeMb) + N'</td></tr>' AS NaiveRow
FROM #DbFiles
ORDER BY FileId;

That is how a report goes wrong without any error. Nothing fails. The email just looks odd, and you find out from a confused reader.

Let FOR XML PATH escape the text

FOR XML PATH knows how to escape text. Name a column td and it writes a td element. Name the query PATH('tr') and every row becomes a tr. Look at the second result below. The ampersand turns into &amp; and the angle brackets into &lt; and &gt;. The browser shows them as the characters you meant.

The first result shows a trap. Two columns with the same name get merged into one cell, so “Data & Archive” and 12.5 end up glued together. The empty [text()] column in the middle keeps them apart. It looks like a hack because it is one, but everyone uses it.

-- Same name twice: the cells merge
SELECT FileName AS td, SizeMb AS td
FROM #DbFiles
ORDER BY FileId
FOR XML PATH('tr'), TYPE;

-- An empty text node in between keeps them apart
SELECT FileName AS td, N'' AS [text()], SizeMb AS td
FROM #DbFiles
ORDER BY FileId
FOR XML PATH('tr'), TYPE;

One more thing to notice. Row 3 has no second cell at all. A NULL column simply disappears from the XML, so that row has one cell while the others have two. The table would look ragged. The next block fixes it by turning NULL into the text n/a.

Build the full body

Now wrap the rows in a table with a header. The ISNULL on the size keeps every row at two cells. Convert the XML to nvarchar(max) so it becomes plain text that Database Mail can send.

DECLARE @Rows nvarchar(max), @Body nvarchar(max);

SET @Rows = CONVERT(nvarchar(max),
    (SELECT FileName AS td,
            N'' AS [text()],
            ISNULL(CONVERT(nvarchar(20), SizeMb), N'n/a') AS td
     FROM #DbFiles
     ORDER BY FileId
     FOR XML PATH('tr'), TYPE));

SET @Body = N'<table><tr><th>File</th><th>Size MB</th></tr>'
          + @Rows + N'</table>';

SELECT @Body AS EmailBody;

The result is a complete table. Row 1 shows Data &amp; Archive, row 2 shows &lt;Log&gt;, and row 3 ends with n/a.

Four checks for an HTML email body

Plan for the empty report

Some days there is nothing to report. A query that returns no rows gives you NULL from FOR XML, and a NULL joined to a string is NULL. Your email body would be empty. Send a friendly line instead. I filter on sizes above 100 MB here, and no file qualifies.

DECLARE @Rows nvarchar(max), @Body nvarchar(max);

SET @Rows = CONVERT(nvarchar(max),
    (SELECT FileName AS td, N'' AS [text()], SizeMb AS td
     FROM #DbFiles
     WHERE SizeMb > 100
     ORDER BY FileId
     FOR XML PATH('tr'), TYPE));

SET @Body = N'<table><tr><th>File</th><th>Size MB</th></tr>'
          + ISNULL(@Rows, N'<tr><td colspan="2">Nothing to report today.</td></tr>')
          + N'</table>';

SELECT @Body AS EmailBody;

Send it, then check what happened

The send call is below, commented out, because it needs a Database Mail profile and a real address. Replace both and set body_format to HTML. Without that setting your reader sees raw tags.

/*
EXEC msdb.dbo.sp_send_dbmail
    @profile_name = N'YourMailProfile',
    @recipients   = N'you@example.com',
    @subject      = N'File size report',
    @body         = @Body,
    @body_format  = 'HTML';
*/

SELECT TOP (5) mailitem_id, sent_status, subject
FROM msdb.dbo.sysmail_allitems
ORDER BY mailitem_id DESC;

SELECT TOP (5) event_type, description
FROM msdb.dbo.sysmail_event_log
ORDER BY log_id DESC;

DROP TABLE IF EXISTS #DbFiles;

A successful call only means the mail was queued. It does not mean it arrived. The first query shows each request and its status. The second one holds the error text when a mail fails. On my test server both lists are empty, because I never sent anything. Send yourself a test first and open it in the email client your readers use. Clients render tables differently.

Send yourself a test, look at it in your real inbox, and only then add the schedule.

A good email report is not every row, it is the few rows that matter.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Database Mail, HTML, SQL XML
Previous Post
The Outbox Table: Publishing Changes Reliably From a Transaction
Next Post
SQL SERVER – Retrieve Maximum Length of Object Name with sp_server_info

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.