I have the next two tables:
CREATE TABLE #SalesByStore (
Brand VARCHAR(10),
StoreName VARCHAR(50),
Sales DECIMAL(10,2)
)
CREATE TABLE #SalesByBrand (
Brand VARCHAR(10),
TotalSales DECIMAL(10,2)
)
I am trying to build an HTML table body using recursive string concatenation, and I need to show the sales by store ordered by brand, and after each group of stores from a same brand show the sales subtotals for that brand, like this:
I am doing it the following way:
DECLARE @tableBody NVARCHAR(MAX), @lastBrand VARCHAR(10);
SELECT @tableBody='';
SELECT
@tableBody
= @tableBody
+ CASE
WHEN @lastBrand IS NOT NULL AND @lastBrand<>SS.Brand
THEN '<tr><td colspan="2">Subtotal</td><td>'
+ (SELECT TOP 1 CAST(SB.TotalSales AS VARCHAR(15)) FROM #SalesByBrand SB WHERE SB.Brand=@lastBrand)
+ '</td></tr>'
ELSE '' END
+ '<tr><td>' + SS.Brand + '</td><td>'
+ SS.StoreName + '</td><td>' + CAST(SS.Sales AS VARCHAR(15)) + '</td></tr>',
@lastBrand = SS.Brand
FROM #SalesByStore SS
ORDER BY SS.Brand
The problem is that the sub-query that gets me the sub-total amount by brand always returns NULL because @lastBrand remains null for the sub-query (see this stack overflow question for an explain about why this happens: Why subquery inside recursive string concatenation always returns NULL?).
Can you suggest me another way to create the HTML table with subtotals in SQL Server 2005?
By the way, I need to build the HTML table in SQL Server in order to send it inside a db mail.
EDIT: I have moved the case from the ending to the beggining of the concatenation, because the subtotal row must be draw before the new brand group begins. Sorry for the mistake.
Luckily in this case, we can simply swap out your subquery for a join and select the value directly:
DECLARE @tableBody NVARCHAR(MAX), @lastBrand VARCHAR(10), @lastTotal decimal(10,2);
SELECT @tableBody='';
SELECT
@tableBody
= @tableBody
+ CASE
WHEN @lastBrand IS NOT NULL AND @lastBrand<>SS.Brand
THEN '<tr><td colspan="2">Subtotal</td><td>'
+ CAST(@lastTotal AS VARCHAR(15)) -- Add the last total
+ '</td></tr>'
ELSE '' END
+ '<tr><td>' + SS.Brand + '</td><td>'
+ SS.StoreName + '</td><td>' + CAST(SS.Sales AS VARCHAR(15)) + '</td></tr>',
@lastBrand = SS.Brand,
@lastTotal = SB.TotalSales -- Save the last total, too
FROM #SalesByStore SS
join #SalesByBrand SB on SS.Brand = SB.Brand -- Join to get brand totals
ORDER BY SS.Brand
-- Finally add the last total
SELECT
@tableBody
= @tableBody
+ '<tr><td colspan="2">Subtotal</td><td>'
+ CAST(@lastTotal AS VARCHAR(15))
+ '</td></tr>'
You've certainly come up with a clever method of creating this HTML table. Note that I've moved things around a little and hacked together a solution.
I tested with this set of data:
insert into #SalesByStore select 'A', 'Store 1', 1000
insert into #SalesByStore select 'A', 'Store 2', 2000
insert into #SalesByStore select 'B', 'Store 3', 1500
insert into #SalesByStore select 'B', 'Store 4', 2100
insert into #SalesByStore select 'B', 'Store 5', 3100
insert into #SalesByBrand select 'A', 3000
insert into #SalesByBrand select 'B', 6700
Alternative using GROUPING SETS and proper ordering. The strings are joined together using XML PATH.
Working SQL Fiddle
The top query in the fiddle shows the variable content after the query. The bottom query just shows you what GROUPING SETS produces.
declare @tablebody nvarchar(max) = '';
select @tablebody = (select
case
when storename is null then
'<tr><td colspan="2">Subtotal</td>
<td>'+cast(sum(sales) as varchar(15))+'</td></tr>'
else
'<tr><td>'+brand+'</td>
<td>'+storename+'</td>
<td>'+cast(sum(sales) as varchar(15))+'</td></tr>'
end
from salesbystore
group by grouping sets
((brand, storename),(brand))
order by brand, case when storename is null then 1 else 0 end
for xml path (''), root('a'), type
).value('(/a)[1]','nvarchar(max)');
select @tablebody;
While grouping is certainly a feature of SQL Server 2008 onwards and should replace all usage of WITH ROLLUP, this particular example can easily be switch to a WITH ROLLUP for SQL Server 2005 using 3 lines replacing the 2 shown
group by brand, storename -- group by grouping sets
with rollup -- ((brand, storename),(brand))
having brand is not null