It is possible to change how row_number inserts the values? - sql-server

Currently I'm doing this:
select
ProductID = ProductID = ROW_NUMBER() OVER (PARTITION BY PRODUCTID ORDER BY PRODUCtID),
TransactionDate,
TransactionAmount
from ProductsSales
order by ProductID
The results are like this:
ProductID
TransactionDate
TransactionAmount
1
2022-11-06
30
2
2022-11-12
30
3
2022-11-28
30
2
2022-11-03
10
3
2022-11-10
10
4
2022-11-15
10
3
2022-11-02
50
The duplicated IDs are being inserted sequential, but what I need it to be like this:
ProductID
TransactionDate
TransactionAmount
1
2022-11-06
30
1.1
2022-11-12
30
1.2
2022-11-28
30
2
2022-11-03
10
2.1
2022-11-10
10
2.2
2022-11-15
10
3
2022-11-02
50
Is this possible?

Assuming your PRODUCTID field is numeric already, then this should work:
WITH _ProductIdSorted AS
(
SELECT
CONCAT
(
PRODUCTID,
'.',
ROW_NUMBER() OVER (PARTITION BY PRODUCTID ORDER BY TransactionDate) - 1
) AS ProductId,
TransactionDate,
TransactionAmount
FROM ProductsSales
)
SELECT
REPLACE(ProductId, '.0', '') AS ProductId,
TransactionDate,
TransactionAmount
FROM _ProductIdSorted;
By the way, just the same as the ORDER BY clause in your query, the one my answer uses is a nondeterminsitic sort. It seems, based on your Post, it doesn't matter to you the order which the rows are sorted within the partition though.

Related

Altering a QUALIFY with an additional criterion

In Snowflake I have this original query which, for a given consumer_ID, produces a list of unique store IDs.
SELECT
t.consumer_id
, t.business_id
, t.store_id
, t.campaign_id
FROM campaigns_mini AS t
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.store_id ORDER BY t.campaign_id) = 1
The original purpose was to provide a list that does not duplicate store_id for a given consumer_id. Suppose now I also need to ensure this list does not duplicate business_id as well for a given consumer_ID. Is there an easy way to modify the above?
SELECT
t.consumer_id
, t.business_id
, t.store_id
, t.campaign_id
FROM campaigns_mini AS t
QUALIFY ROW_NUMBER() OVER
(PARTITION BY t.consumer_id
,t.store_id
,t.business_id
ORDER BY t.campaign_id) = 1
The partition by clause forms windows by the combination of all the expressions in the clause.
This will deduplicate by the combination of consumer_id, store_id, and business_id. If this is not what you need, please update with sample input and output to clarify.
So if I make up some data:
WITH campaigns_mini(consumer_id, business_id, store_id, campaign_id) as (
select * from values
(1,10,100,1000),
(1,10,100,1001),
(1,10,101,1002),
(2,20,200,2000)
)
and use your exist SQL
SELECT
t.consumer_id
,t.business_id
,t.store_id
,t.campaign_id
FROM campaigns_mini AS t
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.store_id ORDER BY t.campaign_id) = 1
I get
CONSUMER_ID
BUSINESS_ID
STORE_ID
CAMPAIGN_ID
1
10
101
1002
1
10
100
1000
2
20
200
2000
we get the Store not repeated for the Consumer, but as you note you don't want the business repeated ether..
If we change to using business_id instead of store_id we see we get less rows:
SELECT
t.consumer_id
,t.business_id
,t.store_id
,t.campaign_id
FROM campaigns_mini AS t
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.business_id ORDER BY t.campaign_id) = 1
ORDER BY 1;
CONSUMER_ID
BUSINESS_ID
STORE_ID
CAMPAIGN_ID
1
10
100
1000
2
20
200
2000
So if we want "no repeating business_id AND no repeating stores" using the Qualify Greg's has proposed will not help, as we are keeping the first for the distinct set of consumer,business, & store:
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.business_id, t.store_id ORDER BY t.campaign_id) = 1
which gives:
CONSUMER_ID |BUSINESS_ID |STORE_ID |CAMPAIGN_ID
1 |10 |100 |1000
1 |10 |101 |1002
2 |20 |200 |2000
So the next thing is to think why not keep the only the first of the two sets:
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.store_id ORDER BY t.campaign_id) = 1
AND ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.business_id ORDER BY t.campaign_id) = 1
which for this data works!
CONSUMER_ID
BUSINESS_ID
STORE_ID
CAMPAIGN_ID
1
10
100
1000
2
20
200
2000
but then for this data:
WITH campaigns_mini(consumer_id, business_id, store_id, campaign_id) as (
select * from values
(1,10,100,1000),
(1,10,101,1001),
(1,20,101,1002)
)
there is only one row with business 20, for store 101, but the first 101 store is on campaign 1001, so both those rows are discarded.
CONSUMER_ID
BUSINESS_ID
STORE_ID
CAMPAIGN_ID
1
10
100
1000
So if we use two layers to do the prune, for this data:
select * from (
SELECT
t.consumer_id
,t.business_id
,t.store_id
,t.campaign_id
FROM campaigns_mini AS t
QUALIFY ROW_NUMBER() OVER (PARTITION BY t.consumer_id, t.business_id ORDER BY t.campaign_id) = 1
)
QUALIFY ROW_NUMBER() OVER (PARTITION BY consumer_id, store_id ORDER BY campaign_id) = 1
works:
CONSUMER_ID
BUSINESS_ID
STORE_ID
CAMPAIGN_ID
1
10
100
1000
1
20
101
1002
but if your flip those orders of QUALIFY you are back to just one row..
so as a general problem it cannot be safely solve for all data cases with this pattern...

Find each employee Project Start Date and End Date (i.e., Start Date and End Date should be continuous without any break in Days/Months/Year)

ID
EmployeeId
ProjectId
StartDate
EndDate
1
1
100
01-04-2019
30-04-2019
2
1
100
01-05-2019
31-05-2019
3
1
100
01-12-2019
31-12-2019
4
1
100
01-01-2020
31-01-2020
5
2
200
01-01-2019
31-01-2019
6
2
200
01-02-2019
28-02-2019
7
2
200
01-04-2019
28-04-2019
8
2
200
01-05-2019
31-05-2019
9
2
200
01-06-2019
30-06-2019
10
3
100
01-08-2019
31-08-2019
11
3
100
01-09-2019
30-09-2019
12
3
200
01-10-2019
31-10-2019
13
3
200
01-11-2019
30-11-2019
14
3
300
01-12-2019
31-12-2019
15
3
300
01-01-2020
31-01-2020
16
3
300
01-02-2020
29-02-2020
expected Output
EmployeeId
ProjectId
StartDate
EndDate
1
100
01-04-2019
31-05-2019
1
100
01-12-2019
31-01-2020
2
200
01-01-2019
28-02-2019
2
200
01-04-2019
28-04-2019
2
200
01-05-2019
30-06-2019
3
100
01-08-2019
30-09-2019
3
200
01-10-2019
30-11-2019
3
300
01-12-2019
29-02-2020
I have tried to find the enddate of the currentrow is enddate+1 is startdate of the next row,if it is continious without any gaps then need to select startdate of the previous row and enddate of current row.
;with MyCTE as
(
select mt.EmployeeId, mt.StartDate, mt.EndDate, ROW_NUMBER() over (order by ID) as RowNum
from #Employees mt
)
select c1.employeeId, case when c2.employeeId is null then c1.StartDate else dateadd(dd,1, c2.EndDate) end as StartDate,
c1.EndDate
from MyCTE c1
left join MyCTE c2
on C1.employeeId=c2.employeeId and
--and dateadd(dd,1,c1.startdate)
c1.RowNum = c2.RowNum +1
This is a classic gaps-and-islands problem.
There are many solutions. A typical simple (if not very efficient) solution, is as follows:
Use LAG to identify rows which start a group/island (partitioning as necessary)
Use a windowed COUNT to assign a group ID to each of those
Group by that ID, and take the MIN/MAX of the values
WITH PrevValues AS (
SELECT *,
IsStart = CASE WHEN DATEADD(day, -1, StartDate) <=
LAG(EndDate) OVER (PARTITION BY EmployeeId, ProjectId ORDER BY StartDate)
THEN NULL ELSE 1 END
FROM Employees e
),
Groups AS (
SELECT *,
GroupId = COUNT(IsStart) OVER (PARTITION BY EmployeeId, ProjectId ORDER BY StartDate ROWS UNBOUNDED PRECEDING)
FROM PrevValues pv
)
SELECT
g.EmployeeId,
g.ProjectId,
StartDate = MIN(StartDate),
EndDate = MAX(EndDate)
FROM Groups g
GROUP BY
g.EmployeeId,
g.ProjectId,
g.GroupId;
db<>fiddle

How to Sum (MAX values) from different value groups in same column SQL Server

I have a table like this:
Date
Consec_Days
2015-01-01
1
2015-01-03
1
2015-01-06
1
2015-01-07
2
2015-01-09
1
2015-01-12
1
2015-01-13
2
2015-01-14
3
2015-01-17
1
I need to Sum the max value (days) for each of the consecutive groupings where Consec_Days are > 1. So the correct result would be 5 days.
This is a type of gaps-and-islands problem.
There are many solutions, here is one simple one
Get the start points of each group using LAG
Calculate a grouping ID using a windowed conditional count
Group by that ID and take the highest sum
WITH StartPoints AS (
SELECT *,
IsStart = CASE WHEN LAG(Consec_Days) OVER (ORDER BY Date) = 1 THEN 1 END
FROM YourTable t
),
Groupings AS (
SELECT *,
GroupId = COUNT(IsStart) OVER (ORDER BY Date)
FROM StartPoints
WHERE Consec_Days > 1
)
SELECT TOP (1)
SUM(Consec_Days)
FROM Groupings
GROUP BY
GroupId
ORDER BY
SUM(Consec_Days) DESC;
db<>fiddle
with cte as (
select Consec_Days,
coalesce(lead(Consec_Days) over (order by Date), 1) as next
from YourTable
)
select sum(Consec_Days)
from cte
where Consec_Days <> 1 and next = 1
db<>fiddle

How to select specific records of groups based on criteria

I'm trying to group a set of data and for some of the fields I need to select a specific value based on the ttype, for example I have the following rows:
caseid age iss gcs ttype
00170 64 25 17 Transfer Out
00170 64 27 15 Transfer In
00201 24 14 40 Transfer In
If a caseID has ttype 'Transfer Out' I want to use the ISS and GCS values from this row, otherwise use the values from the 'Transfer In' row.
My desired output based on the above example would be:
caseid age iss gcs
00170 64 25 17
00201 24 14 40
My current select statement is:
select caseid, max(age), max(iss), max(gcs)
from Table1
group by caseid
Which I know is incorrect but how do I specify the values for ISS and GCS from a specific row?
Thanks
Edit - I will not always need to select from Row1, table below with expanded data:
caseid age iss gcs los ttype disdate
170 64 25 17 5 Transfer Out 2014-01-02 00:00:00.000
170 64 27 15 1 Transfer In 2014-01-04 00:00:00.000
201 24 14 40 4 Transfer In 2014-01-04 00:00:00.000
In this case, I want the max age and the ISS and GCS figure for row1 as before but I need to sum the LOS and select the disdate for row 2 (ie the latest date), so my output would be:
caseid age iss gcs los disdate
170 64 25 17 6 2014-01-04
201 24 14 40 4 2014-01-04
Is this possible?
You can use a CTE and ROW_NUMBER + Over-clause (edited acc. to your updated question):
WITH CTE AS
(
SELECT caseid, age, iss, gcs, los, ttype, disdate,
SumLos = SUM(los) OVER (PARTITION BY caseid),
LatestDisDate = MAX(disdate) OVER (PARTITION BY caseid),
rn = ROW_NUMBER() OVER (PARTITION BY caseid
ORDER BY CASE WHEN ttype = 'Transfer Out'
THEN 0 ELSE 1 END ASC, disdate ASC)
FROM dbo.Table1
)
SELECT caseid, age, iss, gcs, los = SumLos, disdate = LatestDisDate
FROM CTE
WHERE rn = 1
Demo
I think this is what you need -
;WITH CTE AS
(
SELECT case_id, age,iss,gcs, ROW_NUMBER () over (PARTITION BY ttype order by gcs DESC) Rn
from YOUR_TABLE_NAME
)
SELECT case_id,age,iss,gcs
from CTE where Rn =1

SQL How to create output with sub totals

I'm new to T-SQL and need help converting an excel report to a run on SQL. I have a SQL table that records all the daily inventory transactions (in/out) from each stockroom. I need to create a report that list the current inventory levels for each product in each location and the qty in each place as follows. In other words, the current inventory levels of each place.
I also need help on how to insert the Preferred Out Report (below) into SQL Server as a view so I can run this each month over and over again.
Thanks in Advance!
Inventory Log table:
PubID QTY LocationID Transaction
1 10 1 Add
1 20 2 Add
1 30 3 Add
1 5 1 Sold
1 10 2 Sold
1 5 3 Sold
2 10 1 Add
2 10 2 Add
2 5 2 Sold
2 8 2 Sold
1 20 1 Add
1 20 2 Add
2 2 2 Sold
Preferred Output Table:
PubID Local_1 Local_2 Local_3 Total
1 25 30 25 80
2 5 0 0 5
Total 30 30 25 85
I see a lot of close examples here but most just add the value while I need to subtract the Sold inventory from the Added stock to get my totals in each column.
The row totals and column totals on the right and bottom are pluses but not needed if it's easier without.
THANKS!
If this was about aggregation without pivoting, you could use a CASE expression, like this:
SELECT
...
Local_1 = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END),
...
FROM ...
GROUP BY ...
However, in the PIVOT clause, the argument of the aggregate function must be just a column reference, not an expression. You can work around that by transforming the original dataset so that QTY is either positive or negative, depending on Transaction:
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
The above query will give you a result set like this:
PubID QTY LocationID
----- --- ----------
1 10 1
1 20 2
1 30 3
1 -5 1
1 -10 2
1 -5 3
2 10 1
2 10 2
2 -5 2
2 -8 2
1 20 1
1 20 2
2 -2 2
which is now easy to pivot:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
Note that you could actually prepare the names Local_1, Local_2, Local_3 beforehand and avoid renaming them in the main SELECT. Assuming they are formed by appending the LocationID value to the string Local_, here's an example of what I mean:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
Name = 'Local_' + CAST(LocationID AS varchar(10))
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1,
Local_2,
Local_3
FROM prepared
PIVOT
(
SUM(QTY)
FOR Name IN (Local_1, Local_2, Local_3)
) AS p
;
You will see, however, that in this solution renaming will be needed at some point anyway, so I'll use the previous version in my further explanation.
Now, adding the totals to the pivot results as in your desired output may seem a little tricky. Obviously, the column could be calculated simply as the sum of all the Local_* columns, which might actually not be too bad with a small number of locations:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID
FROM dbo.InventoryLog
)
SELECT
PubID,
Local_1 = [1],
Local_2 = [2],
Local_3 = [3]
Total = COALESCE([1], 0)
+ COALESCE([2], 0)
+ COALESCE([3], 0)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
(COALESCE is needed because some results may be NULL.)
But there's an alternative to that, where you don't have to list all the locations explicitly one extra time. You could return the totals per PubID alongside the details in the prepared dataset using SUM() OVER (...), like this:
WITH prepared AS (
SELECT
PubID,
QTY = CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END,
LocationID,
Total = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
OVER (PARTITION BY PubID)
FROM dbo.InventoryLog
)
…
or like this, if you wish to avoid repetition of the CASE expression:
WITH prepared AS (
SELECT
t.PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
…
Then you would just include the Total column into the main SELECT clause along with the pivoted results and PubID:
…
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
;
That would be the total column for you. As for the row, it is actually easy to add it when you are acquainted with the ROLLUP() grouping function:
…
SELECT
PubID,
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
The total row will have NULL in the PubID column, so you'll again need COALESCE to put the word Total instead (only if you want to return it in SQL; alternatively you could substitute it in the calling application):
…
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
…
And that would be all. To sum it up, here is a complete query:
WITH prepared AS (
SELECT
PubID,
QTY = x.AdjustedQTY,
t.LocationID,
Total = SUM(x.AdjustedQTY) OVER (PARTITION BY t.PubID)
FROM dbo.InventoryLog AS t
CROSS APPLY (
SELECT CASE t.[Transaction] WHEN 'Add' THEN t.QTY ELSE -t.QTY END
) AS x (AdjustedQTY)
)
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Local_1 = SUM([1]),
Local_2 = SUM([2]),
Local_3 = SUM([3]),
Total = SUM(Total)
FROM prepared
PIVOT
(
SUM(QTY)
FOR LocationID IN ([1], [2], [3])
) AS p
GROUP BY ROLLUP(PubID)
;
As a final touch to it, you may want to apply COALESCE to the SUMs as well, to avoid returning NULLs in your data (if that is necessary).
The query below does what you need. I might have had one extra group by that could be combined into 1 but you get the idea.
DECLARE #InventoryLog TABLE
(
PubId INT,
Qty INT,
LocationId INT,
[Transaction] Varchar(4)
)
DECLARE #LocationTable TABLE
(
Id INT,
Name VarChar(10)
)
INSERT INTO #LocationTable
VALUES
(1, 'LOC_1'),
(2, 'LOC_2'),
(3, 'LOC_3')
INSERT INTO #InventoryLog
VALUES
(1 , 10, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(1 , 30, 3 , 'Add'),
(1 , 5 , 1 , 'Sold'),
(1 , 10, 2 , 'Sold'),
(1 , 5 , 3 , 'Sold'),
(2 , 10, 1 , 'Add'),
(2 , 10, 2 , 'Add'),
(2 , 5 , 2 , 'Sold'),
(2 , 8 , 2 , 'Sold'),
(1 , 20, 1 , 'Add'),
(1 , 20, 2 , 'Add'),
(2 , 2 , 2 , 'Sold')
SELECT PubId,
lT.Name LocationName,
CASE
WHEN [Transaction] ='Add' Then Qty
WHEN [Transaction] ='Sold' Then -Qty
END as Quantity
INTO #TempInventoryTable
FROM #InventoryLog iL
INNER JOIN #LocationTable lT on iL.LocationId = lT.Id
SELECT * INTO #AlmostThere
FROM
(
SELECT PubId,
ISNULL(LOC_1,0) LOC_1,
ISNULL(LOC_2,0) LOC_2,
ISNULL(LOC_3,0) LOC_3,
SUM(ISNULL(LOC_1,0) + ISNULL(LOC_2,0) + ISNULL(LOC_3,0)) AS TOTAL
FROM #TempInventoryTable s
PIVOT
(
SUM(Quantity)
FOR LocationName in (LOC_1,LOC_2,LOC_3)
) as b
GROUP BY PubId, LOC_1, LOC_2, LOC_3
) b
SELECT CAST(PubId as VARCHAR(10))PubId,
LOC_1,
LOC_2,
LOC_3,
TOTAL
FROM #AlmostThere
UNION
SELECT ISNULL(CAST(PubId AS VARCHAR(10)),'TOTAL') PubId,
[LOC_1]= SUM(LOC_1),
[LOC_2]= SUM(LOC_2),
[LOC_3]= SUM(LOC_3),
[TOTAL]= SUM(TOTAL)
FROM #AlmostThere
GROUP BY ROLLUP(PubId)
DROP TABLE #TempInventoryTable
DROP TABLE #AlmostThere
PubId LOC_1 LOC_2 LOC_3 TOTAL
1 25 30 25 80
2 10 -5 0 5
TOTAL 35 25 25 85
Sql Fiddle
Here is another approach: aggregate the data before pivoting, then pivot the aggregated results.
Compared to my other suggestion, this method is much simpler syntactically, which may also make it easier to understand and maintain.
All the aggregation is done with the help of the CUBE() grouping function. The basic query would be this:
SELECT
PubID,
LocationID,
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
You can see the same CASE expression as in my other answer, only this time it can be directly used as the argument of SUM.
Using aggregation by CUBE gives us not only the totals by (PubID, LocationID), but also by PubID and LocationID separately, as well as the grand total. This is the result of the query for the example in your question:
PubID LocationID QTY
----- ---------- ---
1 1 35
2 1 10
NULL 1 45
1 2 50
2 2 25
NULL 2 75
1 3 35
NULL 3 35
NULL NULL 155
1 NULL 120
2 NULL 35
Rows with NULLs in LocationID are row totals in the final result set, and those with NULLs in PubID are column totals. The row with NULLs in both columns is the grand total.
Before we can proceed with the pivoting, we need to prepare column names for the pivoted results. If the names are supposed to be derived from the values of LocationID, the following declaration will replace LocationID in the original query's SELECT clause:
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total')
We can also substitute 'Total' for the NULLs in PubID at this same stage, so this will replace PubID in the SELECT clause:
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total')
Now the results will look like this:
PubID LocationID QTY
----- ---------- ---
1 Local_1 35
2 Local_1 10
Total Local_1 45
1 Local_2 50
2 Local_2 25
Total Local_2 75
1 Local_3 35
Total Local_3 35
Total Total 155
1 Total 120
2 Total 35
and at this point everything is ready to apply PIVOT. This query transforms the above result set according to the desired format:
WITH aggregated AS (
SELECT
PubID = COALESCE(CAST(PubID AS varchar(10)), 'Total'),
Location = COALESCE('Local_' + CAST(LocationID AS varchar(10)), 'Total'),
QTY = SUM(CASE [Transaction] WHEN 'Add' THEN QTY ELSE -QTY END)
FROM dbo.InventoryLog
GROUP BY CUBE(PubID, LocationID)
)
SELECT
PubID,
Local_1,
Local_2,
Local_3,
Total
FROM aggregated
PIVOT (
MAX(QTY)
FOR Location IN (Local_1, Local_2, Local_3, Total)
) AS p
;
This query will return NULLs for missing combinations of (PubID, LocationID). If you want to return 0 instead, apply COALESCE to the result of SUM in the definition of aggregated.

Resources