3

Is it possible to use a cursor to create a dynamic table and then use those columns to aggregate data in SQL server 2008?

For example take the following table.

CREATE TABLE Billing (
 BillingId BIGINT IDENTITY,
 SubscriptionId BIGINT,
 ExternalServiceName VARCHAR(50),
 BillYear INT NOT NULL,
 BillMonth INT NOT NULL
);
INSERT INTO Billing (BillingId, SubscriptionId, ExternalServiceName,
 BillYear, BillMonth)
VALUES (1, 1, 'Dogs', 2018, 4),
 (2, 2, 'Cats', 2018, 4),
 (3, 1, 'Dogs', 2018, 5),
 (4, 2, 'Cats', 2018, 5);
CREATE TABLE BillingData (
 BillingDataId INT IDENTITY PRIMARY KEY,
 BillingId INT NOT NULL,
 Feature VARCHAR(50) NOT NULL,
 Usage INT NOT NULL,
 Measurement VARCHAR(50),
 Cost NUMERIC(18,2) NOT NULL
);
INSERT INTO BillingData(BillingId, Feature, Usage, Measurement, Cost)
VALUES (1, 'Walks', 25, 'walks', 200.32),
 (1, 'Baths', 5, 'baths', 251.32),
 (2, 'Litter change', 53, 'changes', 110.21),
 (2, 'Groom', 25, 'brushings', 123),
 (2, 'Scratching', 213, 'clipping', 123),
 (3, 'Pilling', 11, 'medicate', 10),
 (4, 'Groom', 5, 'brushings', 50),
 (4, 'Exercise', 1, 'run', 25.12),
 (1, 'Walks', 500, 'walks', 12351.31),
 (1, 'Baths', 53, 'baths', 1235),
 (2, 'Baths', 53, 'baths', 1235); 

What I'd like to be able to do is create a table with this format

 +-------------+---------+---------+-----------------+---------+--------------+---------+----------+
 | [BillingId] | [Walks] | [Baths] | [Litter change] | [Groom] | [Scratching] | [Usage] | [Cost] |
 +-------------+---------+---------+-----------------+---------+--------------+---------+----------+
 | 1 | 525 | 58 | 0 | 0 | 0 | 583 | 14037.95 |
 | 2 | 0 | 53 | 53 | 25 | 213 | 344 | 1591.21 |
 +-------------+---------+---------+-----------------+---------+--------------+---------+----------+

The only way I could think to accomplish this was to aggregate the vertical table.

By doing something like the following query

SELECT MAX(BillingId), MAX(Feature), SUM(Usage), MAX(Measurement), SUM(Cost) 
FROM BillingData;

But then I'd have to dynamically join those columns into the Billing table, especially since the BillingData may not be the same from month to month. For example:

SELECT DISTINCT Feature FROM BillingData WHERE BillYear=2018 AND BillMonth=5;

Is different from

SELECT DISTINCT Feature FROM BillingData WHERE BillYear=2018 and BillMonth=4;

So while the columns BillingId, Walks, Baths, Litter change, Groom, Scratching, Usage, Cost are appropriate for April the columns for May would be just BillingId, Pilling, Groom, Exercise, Usage and Cost.

I believe a pivot table may be what I need here but I suspect it may need to be dynamic as columns would need to be different for each month.

I'm not sure the best way to go about doing this. Some help would be greatly appreciated.

Paul White
95.4k30 gold badges440 silver badges689 bronze badges
asked Jun 5, 2018 at 16:54

1 Answer 1

7

This can be done with PIVOT and it can be done dynamically, but before you jump in trying to do this dynamically, you should try to get the result you want using a static or hard-coded version of the query, then convert it to dynamic sql.

Since you are using SQL Server 2008 and you want a total column for both Usage and Cost, I would first start by looking at sum(<your column) over(...). This will allow you to aggregate your data in a single step before pivoting it.

To get the static version, I'd first start out with query similar to this:

select 
 b.BillingId,
 bd.Feature,
 bd.Usage,
 TotalUsage = sum(bd.Usage) over(partition by bd.BillingId),
 TotalCost = sum(bd.Cost) over(partition by bd.BillingId)
from Billing b
inner join BillingData bd
 on b.BillingId = bd.BillingId
where b.BillYear = 2018 and b.BillMonth = 4

See SQL Fiddle. This query gets you the basic data that you want to pivot:

| BillingId | Feature | Usage | TotalUsage | TotalCost |
|-----------|---------------|-------|------------|-----------|
| 1 | Walks | 25 | 583 | 14037.95 |
| 1 | Baths | 5 | 583 | 14037.95 |
| 1 | Walks | 500 | 583 | 14037.95 |
| 1 | Baths | 53 | 583 | 14037.95 |
| 2 | Baths | 53 | 344 | 1591.21 |
| 2 | Litter change | 53 | 344 | 1591.21 |
| 2 | Groom | 25 | 344 | 1591.21 |
| 2 | Scratching | 213 | 344 | 1591.21 |

Including your BillingId, each of the Features you eventually want in a new column, then the Usage, TotalUsage, and TotalCost for each BillingId. The sum(<yourcolumn> over(partition by bd.BillingId) gives you the value for each account without having to use GROUP BY. Once you've got this data, you can apply the PIVOT function:

select 
 BillingId,
 Walks = IsNull(Walks, 0),
 Baths = IsNull(Baths, 0),
 [Litter Change] = IsNull([Litter Change], 0),
 Groom = IsNull(Groom, 0),
 Scratching = IsNull(Scratching, 0),
 Usage = TotalUsage,
 Cost = TotalCost
from
(
 select 
 b.BillingId,
 bd.Feature,
 bd.Usage,
 TotalUsage = sum(bd.Usage) over(partition by bd.BillingId),
 TotalCost = sum(bd.Cost) over(partition by bd.BillingId)
 from Billing b
 inner join BillingData bd
 on b.BillingId = bd.BillingId
 where b.BillYear = 2018 and b.BillMonth = 4
) x
pivot
(
 sum(Usage)
 for Feature in ([Walks], [Baths], [Litter Change], [Groom], [Scratching])
) piv;

See SQL Fiddle for Demo. This gives a result:

| BillingId | Walks | Baths | Litter Change | Groom | Scratching | Usage | Cost |
|-----------|-------|-------|---------------|-------|------------|-------|----------|
| 1 | 525 | 58 | 0 | 0 | 0 | 583 | 14037.95 |
| 2 | 0 | 53 | 53 | 25 | 213 | 344 | 1591.21 |

Now that you've gotten the final result you are looking for, you can start converting the query to dynamic SQL. In order you do this, you will need to get a list of the values you want to be column aka the Feature values. This is done by querying your tables with the BillYear and BillMonth you want, and concatenating the values into a string, then taking that list of columns and executing a full sql string. The full code could be similar to:

DECLARE 
 @BillYear int = 2018,
 @BillMonth int = 4,
 @colsNull AS NVARCHAR(MAX),
 @cols AS NVARCHAR(MAX),
 @query AS NVARCHAR(MAX)
select @cols = STUFF((SELECT ', ' + QUOTENAME(bd.Feature)
 from Billing b
 inner join BillingData bd
 on b.BillingId = bd.BillingId
 where b.BillYear = @BillYear 
 and b.BillMonth = @BillMonth
 group by bd.Feature
 FOR XML PATH(''), TYPE
 ).value('.', 'NVARCHAR(MAX)') 
 ,1,1,'')
select @colsNull = STUFF((SELECT ', IsNull(' + QUOTENAME(bd.Feature)+',0) as '+ QUOTENAME(bd.Feature)
 from Billing b
 inner join BillingData bd
 on b.BillingId = bd.BillingId
 where b.BillYear = @BillYear 
 and b.BillMonth = @BillMonth
 group by bd.Feature
 FOR XML PATH(''), TYPE
 ).value('.', 'NVARCHAR(MAX)') 
 ,1,1,'');
set @query = N'SELECT BillingId, ' + @colsNUll + N', TotalUsage, TotalCost 
 from 
 (
 select 
 b.BillingId,
 bd.Feature,
 bd.Usage,
 TotalUsage = sum(bd.Usage) over(partition by bd.BillingId),
 TotalCost = sum(bd.Cost) over(partition by bd.BillingId)
 from Billing b
 inner join BillingData bd
 on b.BillingId = bd.BillingId
 where b.BillYear = '+cast(@BillYear as nvarchar(4))+N' 
 and b.BillMonth = '+cast(@BillMonth as nvarchar(2))+N'
 ) x
 pivot 
 (
 sum(Usage)
 for Feature in (' + @cols + N')
 ) p '
exec sp_executesql @query;

See SQL Fiddle with Demo. You'll notice that there are two variables for the columns - one @cols this is used inside of the PIVOT function and then @colsNull this is similar to the first, but it replaces the nulls in the final select list with zero - you can exclude using this if you don't need it. If you execute this for BillingMonth = 4 you'll get the same result as the static version:

| BillingId | Baths | Groom | Litter change | Scratching | Walks | TotalUsage | TotalCost |
|-----------|-------|-------|---------------|------------|-------|------------|-----------|
| 1 | 58 | 0 | 0 | 0 | 525 | 583 | 14037.95 |
| 2 | 53 | 25 | 53 | 213 | 0 | 344 | 1591.21 |

Then if you change the BillingMonth = 5 you get the results without having to change the query (Demo):

| BillingId | Exercise | Groom | Pilling | TotalUsage | TotalCost |
|-----------|----------|-------|---------|------------|-----------|
| 3 | 0 | 0 | 11 | 11 | 10 |
| 4 | 1 | 5 | 0 | 6 | 75.12 |
answered Jun 5, 2018 at 20:49

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.