In this post I cover a way to import and query HTML table data in SQL Server using only T-SQL and no external tools.
Why?
For science!
But I also wanted to see if I can use T-SQL to parse some of the HTML report pages generated by PSBlitz.
In this case, I’m focusing on the index diagnosis page generated by running the non-stored procedure version of Brent Ozar’s sp_BlitzIndex that’s part of PSBlitz.
Getting things ready
There’s no initial setup part required here, all that’s needed is a SQL Server instance, and an HTML, containing at least one table, located in a directory where the SQL Server service account has read access.
Since I’m lazy efficient, I’ve just copied the BlitzIndex_4.html file, from one of my test runs of PSBlitz, to my instance’s backup directory.
If you want to do this from another directory, which I recommend for production and/or shared environments, you can use icacls to grant the service account read permissions on the file.
Don’t do silly stuff like adding the service account to the local Admins group or setting SQL Server to run as LocalSystem – see here why.
If you want to repeat the steps with the same data and file, you can find the same file I’ve used here.
For reference, this is what the first rows of the HTML file look like when viewed in the browser.

Importing and querying HTML
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | DECLARE @XMLContent XML; SELECT @XMLContent = CONVERT (XML, REPLACE(REPLACE(REPLACE(BulkColumn, '<a href="#top">Jump to top</a>', ''), '<br>', ''), '<td></td>', '<td>x</td>') , 2) FROM OPENROWSET (BULK 'E:\VSQL\Backup\BlitzIndex_4.html', SINGLE_BLOB) AS HTMLData; WITH XMLToTableCTE AS (SELECT xx.value('(./td/text())[1]', 'INT') AS [Priority], xx.value('(./td/text())[2]', 'NVARCHAR(200)') AS [Finding], xx.value('(./td/text())[3]', 'NVARCHAR(128)') AS [DatabaseName], xx.value('(./td/text())[4]', 'NVARCHAR(MAX)') AS [Details], xx.value('(./td/text())[5]', 'NVARCHAR(MAX)') AS [Definition], xx.value('(./td/text())[6]', 'NVARCHAR(MAX)') AS [SecretColumns], xx.value('(./td/text())[7]', 'NVARCHAR(MAX)') AS [Usage], xx.value('(./td/text())[8]', 'NVARCHAR(MAX)') AS [Size], xx.value('(./td/text())[9]', 'NVARCHAR(MAX)') AS [MoreInfo], xx.value('(./td/text())[10]', 'NVARCHAR(MAX)') AS [CreateTSQL] FROM (VALUES(@XMLContent)) t1(x) CROSS APPLY x.nodes('//table[1]/tr[position()>1]') t2(xx)) SELECT [Priority], [Finding], [DatabaseName], [Details], [Definition], [SecretColumns], [Usage], [Size], [MoreInfo], [CreateTSQL] FROM XMLToTableCTE ORDER BY [Priority] ASC; |
And this is how the result set looks like. Just like what you’d see after running sp_BlitzIndex from SSMS.

Breaking it down
I’ll skip the variable declaration since that’s self-explanatory.
The SELECT statement does the following:
- Uses OPENROWSET combined with BULK to read the
E:\VSQL\Backup\BlitzIndex_4.htmlfile, and SINGLE_BLOB to treat it as VARBINARY and avoid any potential encoding issues (as opposed to SIGLE_CLOB or SINGLE_NCLOB which can conflict with some Windows encodings and cause issues on file import) - Applies REPLACE on the BulkColumn column returned by OPENROWSET to
- And, finally, it converts the data to XML, but since HTML isn’t real XML, I use XML style 2 so that SQL Server does a bit of work to make it look like legit XML content.
| 1 2 3 4 | SELECT @XMLContent = CONVERT (XML, REPLACE(REPLACE(REPLACE(BulkColumn, '<a href="#top">Jump to top</a>', ''), '<br>', ''), '<td></td>', '<td>x</td>') , 2) FROM OPENROWSET (BULK 'E:\VSQL\Backup\BlitzIndex_4.html', SINGLE_BLOB) AS HTMLData; |
Now for the XML querying part
I’ll be honest with you, dear reader, I suck at XML querying and I have to google stuff the same as everyone else.
So, I’ve used Alan Burstein’s reply from this SQLServerCentral forum post as a starting point.
I’ll do my best to explain what’s happening here:
- Since the HTML table columns don’t really have names, but ordinal positions, I have to use their position in the table, so
./td/text())[1]represents column 1, in this case the Priority column - For now, xx is a dummy column name from the derived table t2
- The value() method performs an XQuery (XML query) for every column specified and returns the vale as the specified SQL data type – in the case of
xx.value('(./td/text())[1]', 'INT')it returns the text from the first<td>element as an INTEGER FROM (VALUES(@XMLContent)) AS t1(x)creates a derived table with one column (x) and one row containing the XML content from the @XMLContent variableCROSS APPLY x.nodes('//table[1]/tr[position()>1]') AS t2(xx))– This line uses theCROSS APPLYoperator along with the.nodes()method to parse the XML content and retrieve data from it.- The XMLToTableCTE common table expression that wraps around the XML query is there just to make filtering or any manipulation of the returned data easier
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | WITH [XMLToTableCTE] AS (SELECT xx.value('(./td/text())[1]', 'INT') AS [Priority], xx.value('(./td/text())[2]', 'NVARCHAR(200)') AS [Finding], xx.value('(./td/text())[3]', 'NVARCHAR(128)') AS [DatabaseName], xx.value('(./td/text())[4]', 'NVARCHAR(MAX)') AS [Details], xx.value('(./td/text())[5]', 'NVARCHAR(MAX)') AS [Definition], xx.value('(./td/text())[6]', 'NVARCHAR(MAX)') AS [SecretColumns], xx.value('(./td/text())[7]', 'NVARCHAR(MAX)') AS [Usage], xx.value('(./td/text())[8]', 'NVARCHAR(MAX)') AS [Size], xx.value('(./td/text())[10]', 'NVARCHAR(MAX)') AS [CreateTSQL] FROM (VALUES(@XMLContent)) AS t1(x) CROSS APPLY x.nodes('//table[1]/tr[position()>1]') AS t2(xx)) SELECT [Priority], [Finding], [DatabaseName], [Details], [Definition], [SecretColumns], [Usage], [Size], [CreateTSQL] FROM [XMLToTableCTE] ORDER BY [Priority] ASC; |
In summary, the query parses the provided XML content obtained from converting the HTML file, extracts data from the first table (excluding the header row), and presents the data in the SSMS result grid that we all know and love.
Conclusion
If T-SQL is your hammer of choice, then every file is a potential nail if it can be converted to usable XML.
In this case, it can easily import and query HTML files in SQL Server.
If you want to see how you can use PowerShell to generate a HTML table from a SQL Server result set, check out this post.