SAP B1 query using pivot for yearly comparison - sql-server

Requirement: I want to compare the years(2018) sales and gross profit over last year(2017).
Solution: I have tried using the query below and I get the expected results.
Month | prevSales | prevGP | currentSales | currGP
Jan | 1234567.00| 1234567.00| 1234567.00 | 1234567.00
Feb | 1234567.00| 1234567.00| 1234567.00 | 1234567.00
Problem: The query took so long to produce results, it's almost one minute to display results.
SELECT P.[monName],
ISNULL([2017],0) as [prev],
ISNULL(P.[prevGP],0) [prevGP],
ISNULL([2018],0) as [curr],
ISNULL(P.[currGP],0) [currGP]
FROM (
SELECT LEFT(DATENAME(MONTH,T1.DocDate),3) [monName],
MONTH(T1.DocDate) [monNum],
ROUND((T1.Doctotal-T1.VatSum-T1.TotalExpns),0) AS [BAL],
(SELECT Sum(A.GrosProfit)
FROM OINV A
WHERE A.CANCELED='N' AND A.DocStatus='C' AND RIGHT(A.NumAtCard,9)<>'CANCELLED'
AND YEAR(A.DocDate)=YEAR(GETDATE())-1 AND MONTH(A.DocDate)=MONTH(T1.DocDate) ) [prevGP],
(SELECT SUM(B.GrosProfit)
FROM OINV B
WHERE B.CANCELED='N' AND B.DocStatus='C' AND RIGHT(B.NumAtCard,9)<>'CANCELLED'
AND YEAR(B.DocDate)=YEAR(GETDATE()) AND MONTH(B.DocDate)=MONTH(T1.DocDate) ) [currGP],
year(T1.Docdate) as [year]
FROM dbo.OCRD T0
LEFT JOIN dbo.OINV T1 ON T1.CardCode = T0.CardCode
Where RIGHT(T1.Numatcard,9)<>'CANCELLED' AND T1.CANCELED='N'
AND T0.[CardType] ='C' AND T1.DocStatus='C'
) S
PIVOT ( SUM(S.[BAL]) FOR [year] IN ([2017],[2018])) P
What could I possibly do to make the query efficient. I believe there is something to do with the pivot.
Thank you.

The problem with your view are the subqueries. If you want to improve the performance of your query you need to avoid the use of those subqueries.
For example, you can create an sql view that returns the information you need. Then you join your current query with that view and you remove the subqueries.
You could do it like this.
create view InvoiceInfoByMonth
as
select sum(GrosProfit) as GrosProfit, year(docdate) as DocYear, month(docdate) as DocMonth
from OINV
WHERE CANCELED='N' AND DocStatus='C' AND RIGHT(NumAtCard,9)<>'CANCELLED'
group by year(DocDate), month(DocDate)
GO
SELECT P.[monName],
ISNULL([2017],0) as [prev],
ISNULL(P.[prevGP],0) [prevGP],
ISNULL([2018],0) as [curr],
ISNULL(P.[currGP],0) [currGP]
FROM (
SELECT LEFT(DATENAME(MONTH,T1.DocDate),3) [monName],
MONTH(T1.DocDate) [monNum],
ROUND((T1.Doctotal-T1.VatSum-T1.TotalExpns),0) AS [BAL],
T2.GrosProfit as prevGP, T3.GrosProfit as currGP,
year(T1.Docdate) as [year]
FROM dbo.OCRD T0
LEFT JOIN dbo.OINV T1 ON T1.CardCode = T0.CardCode
left join InvoiceInfoByMonth T2 ON T2.DocYear = year(getdate())-1 and month(T1.DocDate) = T2.DocMonth
left join InvoiceInfoByMonth T3 ON T3.DocYear = year(getdate()) and month(T1.DocDate) = T3.DocMonth
Where RIGHT(T1.Numatcard,9)<>'CANCELLED' AND T1.CANCELED='N'
AND T0.[CardType] ='C' AND T1.DocStatus='C'
) S
PIVOT ( SUM(S.[BAL]) FOR [year] IN ([2017],[2018])) P
In my system, the query execution time improved from 1 minute to 1 second.

Related

How to use a Left Join with a bunch of other joins

I am trying to join two tables based on EquipWorkOrderID. Tables(EquipWorkOrder and EquipWorkOrderHrs)
With the query I have below it duplicates the row based on ID if there is two UserNm's for the Same ID. I want the two UserNm's and the Hrs if the ID match's in the same role if possible.
example of what my results give me now
EquipWorkOrderID/Equip/Description/Resolution/UserNm/Hrs
---------------------------------------------------------
1 / ForkLift / Bad /Fixed/John Doe / 2
1 /Forklift / Bad /Fixed/Jane Doe /2
What I would Like to see
EquipWorkOrderID/Equip/Description/Resolution/UserNm1/Hrs1/UserNm2/Hrs2
---------------------------------------------------------
1 / ForkLift / Bad /Fixed/John Doe / 2 / Jane Doe / 2
Select * From
(
Select
a.EquipWorkOrderID,
c.UserNm,
b.Hrs
From
EquipWorkOrder a
Left Join EquipWorkOrderHrs b
On a.EquipWorkOrderID = b.EquipWorkOrderID
Left Join AppUser c
On c.UserID = b.UserID
) t
Pivot (
Count(Hrs)
For UserNm IN (
[Tech1],
[Tech2],
[Tech3],
[Tech4],
[Tech5])
) AS pivot_table
I have placed the result of your query in a table (selection) and retrieved the data from it in a common table expression (cte). Replace the content of the CTE with your query and add the two new columns I created (UsrNum and HrsNum).
My solution uses a double pivot (one for the UserNm column and one for the Hrs column) followed by a grouping. This may not be ideal, but it gets the job done.
Here is a fiddle to show how I built up the solution.
Sample data
This just recreates the results of your current query.
create table selection
(
EquipWorkOrderID int,
Equip nvarchar(10),
Description nvarchar(10),
Resolution nvarchar(10),
UserNm nvarchar(10),
Hrs int
);
insert into selection (EquipWorkOrderID,Equip,Description,Resolution,UserNm,Hrs) values
(1, 'ForkLift', 'Bad', 'Fixed', 'John Doe', 2),
(1, 'Forklift', 'Bad', 'Fixed', 'Jane Doe', 2);
Solution
Replace the first part of the CTE with your query and add the two new columns.
with cte as
(
select EquipWorkOrderID,Equip,Description,Resolution,UserNm,Hrs,
'Usr' + convert(nvarchar(10),row_number() over(partition by Equip order by UserNm)) as 'UsrNum',
'Hrs' + convert(nvarchar(10),row_number() over(partition by Equip order by UserNm)) as 'HrsNum'
from selection
)
select ph.EquipWorkOrderId, ph.Equip, ph.Description, ph.Resolution,
max(ph.Usr1) as 'UserNm1',
max(ph.Hrs1) as 'Hrs1',
max(ph.Usr2) as 'UserNm2',
max(ph.Hrs2) as 'Hrs2'
from cte c
pivot (max(c.UserNm) for c.UsrNum in ([Usr1], [Usr2])) pu
pivot (max(pu.Hrs) for pu.HrsNum in ([Hrs1], [Hrs2])) ph
group by ph.EquipWorkOrderId, ph.Equip, ph.Description, ph.Resolution;
Result
Outcome looks like this. Jane Doe is UserNm1 because that is how the new UserNum column was constructed (order by UserNm). Adjust the order by if you need John Doe to remain first.
EquipWorkOrderId Equip Description Resolution UserNm1 Hrs1 UserNm2 Hrs2
----------------- --------- ------------ ----------- --------- ----- -------- -----
1 ForkLift Bad Fixed Jane Doe 2 John Doe 2
Edit: solution merged with original query (untested)
with cte as
(
SELECT TOP 1000
--Start original selection field list
ewo.EquipWorkOrderID,
ewo.DateTm,
equ.Equip,
equ.AccountCode,
equ.Descr,
ewo.Description,
ewo.Resolution,
sta.Status,
au.UserNm,
ewoh.Hrs,
cat.Category,
ml.MaintLoc,
equt.EquipType,
cre.Crew,
ewo.MeterReading,
typ.Type,
--Added two new fields
'Usr' + convert(nvarchar(10),row_number() over(partition by Equip order by UserNm)) as 'UsrNum',
'Hrs' + convert(nvarchar(10),row_number() over(partition by Equip order by UserNm)) as 'HrsNum'
FROM EquipWorkOrder ewo
JOIN EquipWorkOrderHrs ewoh
ON ewo.EquipWorkOrderID = ewoh.EquipWorkOrderID
JOIN AppUser au
ON au.UserID = ewoh.UserID
JOIN Category cat
ON cat.CategoryID = ewo.CategoryID
JOIN Crew cre
ON cre.CrewID = ewo.CrewID
JOIN Equipment equ
ON equ.EquipmentID = ewo.EquipmentID
JOIN Status sta
ON sta.StatusID = ewo.StatusID
JOIN PlantLoc pll
ON pll.PlantLocID = ewo.PlantLocID
JOIN MaintLocation ml
ON ml.MaintLocationID = ewo.MaintLocationID
JOIN EquipType equt
ON equt.EquipTypeID = ewo.EquipTypeID
JOIN Type typ
ON typ.TypeID = equ.TypeID
ORDER BY ewo.DateTm DESC
)
select ph.EquipWorkOrderId, ph.Equip, ph.Description, ph.Resolution,
max(ph.Usr1) as 'UserNm1',
max(ph.Hrs1) as 'Hrs1',
max(ph.Usr2) as 'UserNm2',
max(ph.Hrs2) as 'Hrs2'
from cte c
pivot (max(c.UserNm) for c.UsrNum in ([Usr1], [Usr2])) pu
pivot (max(pu.Hrs) for pu.HrsNum in ([Hrs1], [Hrs2])) ph
group by ph.EquipWorkOrderId, ph.Equip, ph.Description, ph.Resolution;
Edit2: how to use pivot...
Select pivot_table.*
From
(
Select a.EquipWorkOrderID,
b.Hrs,
c.UserNm,
'Tech' + convert(nvarchar(10), row_number() over(order by c.UserNm)) -- construct _generic_ names
From EquipWorkOrder a
Left Join EquipWorkOrderHrs b
On a.EquipWorkOrderID = b.EquipWorkOrderID
Left Join AppUser c
On c.UserID = b.UserID
) t
/*
Pivot (Count(Hrs) For UserNm IN ([Tech1], [Tech2], [Tech3], [Tech4], [Tech5])) AS pivot_table -- UserNm does not contain values like "Tech1" or "Tech2"
*/
Pivot (Count(Hrs) For GenUserNm IN ([Tech1], [Tech2], [Tech3], [Tech4], [Tech5])) AS pivot_table -- pivot over the _generic_ names

T-SQL AVG of multiple columns in a row

I'm trying to select the average sales per person per territory out of the AdventureWorks database.
Since this is aggregating multiple columns in a row instead of multiple rows in a column, it seems like I'd need a sub-query, temp table, maybe a CTE, but I'm not sure how to identify which direction to take or how to write it.
Desired result:
| SalesTerritory | SalesPeople | 2011 | 2012 | 2013 | 2014 | AvgSales
+----------------+-------------+--------+---------+---------+---------+----------
| Australia | 1 | NULL | NULL | 184105 | 1237705 | [avg]
| Canada | 2 | 115360 | 3426082 | 2568323 | etc... | [avg]
Code:
SELECT
pvt.SalesTerritory,
COUNT(pvt.SalesPersonID) AS SalesPeople,
SUM(pvt.[2011]),
SUM(pvt.[2012]),
SUM(pvt.[2013]),
SUM(pvt.[2014])
--What's the best way to AVG the sales by year by sales person for each territory here?
FROM
(SELECT
st.[Name] AS [SalesTerritory],
soh.[SalesPersonID],
soh.[SubTotal],
YEAR(DATEADD(m, 6, soh.[OrderDate])) AS [FiscalYear]
FROM
[Sales].[SalesPerson] sp
INNER JOIN
[Sales].[SalesOrderHeader] soh ON sp.[BusinessEntityID] = soh.[SalesPersonID]
INNER JOIN
[Sales].[SalesTerritory] st ON sp.[TerritoryID] = st.[TerritoryID]
INNER JOIN
[HumanResources].[Employee] e ON soh.[SalesPersonID] = e.[BusinessEntityID]
INNER JOIN
[Person].[Person] p ON p.[BusinessEntityID] = sp.[BusinessEntityID]) AS soh
PIVOT
(SUM([SubTotal]) FOR [FiscalYear] IN ([2011], [2012], [2013], [2014])) AS pvt
GROUP BY
pvt.SalesTerritory
You have several options:
1) Use cross apply. Query would look like:
select
*
from
(
--put your query here
) t
cross apply (select AvgSales = avg(v) from (values ([2011]), ([2012]), ([2013]), ([2014])) q(v)) q
2) Count average by yourself
SELECT
pvt.SalesTerritory,
COUNT(pvt.SalesPersonID) AS SalesPeople,
SUM(pvt.[2011]),
SUM(pvt.[2012]),
SUM(pvt.[2013]),
SUM(pvt.[2014]),
ISNULL(SUM(pvt.[2011]), 0) + ISNULL(SUM(pvt.[2012]), 0)
+ ISNULL(SUM(pvt.[2013]), 0) + ISNULL(SUM(pvt.[2014]), 0)
/ CASE WHEN SUM(pvt.[2011]) > 0 THEN 1 ELSE 0 END
+ CASE WHEN SUM(pvt.[2012]) > 0 THEN 1 ELSE 0 END
+ CASE WHEN SUM(pvt.[2013]) > 0 THEN 1 ELSE 0 END
+ CASE WHEN SUM(pvt.[2014]) > 0 THEN 1 ELSE 0 END
FROM
...
GROUP BY
pvt.SalesTerritory
As I understand your question, you need the average of the yearly sales per salesperson, for each territory. Uzi's answer provides the average of the yearly sales for all salespersons together, for each territory. You could divide that result by the number of salespersons, or use a query like this:
SELECT pvt.SalesTerritory, COUNT(pvt.SalesPersonID) AS SalesPeople,
SUM(pvt.[2011]) AS [2011], SUM(pvt.[2012]) AS [2012],
SUM(pvt.[2013]) AS [2013], SUM(pvt.[2014]) AS [2014],
AVG(pvt.AvgSubTotal) AS AvgSubTotal
FROM (
SELECT y.SalesTerritory, y.SalesPersonID, y.FiscalYear, y.SubTotal,
AVG(y.SubTotal) OVER (PARTITION BY y.SalesTerritory) AS AvgSubTotal
FROM (
SELECT x.SalesTerritory, x.SalesPersonID, x.FiscalYear, SUM(x.SubTotal) AS SubTotal
FROM (
SELECT st.Name AS SalesTerritory, soh.SalesPersonID, soh.SubTotal,
YEAR(DATEADD(m, 6, soh.OrderDate)) AS FiscalYear
FROM Sales.SalesPerson sp
INNER JOIN Sales.SalesOrderHeader soh ON sp.BusinessEntityID = soh.SalesPersonID
INNER JOIN Sales.SalesTerritory st ON sp.TerritoryID = st.TerritoryID
INNER JOIN HumanResources.Employee e ON soh.SalesPersonID = e.BusinessEntityID
INNER JOIN Person.Person p ON p.BusinessEntityID = sp.BusinessEntityID
) x
GROUP BY x.SalesTerritory, x.SalesPersonID, x.FiscalYear
) y
) AS soh
PIVOT (SUM(SubTotal) FOR FiscalYear IN ([2011], [2012], [2013], [2014])) AS pvt
GROUP BY pvt.SalesTerritory;
The result is different for the Northwest territory, but I'm not sure which result you want.

TSQL - Return recent date

Having issues getting a dataset to return with one date per client in the query.
Requirements:
Must have the recent date of transaction per client list for user
Will need have the capability to run through EXEC
Current Query:
SELECT
c.client_uno
, c.client_code
, c.client_name
, c.open_date
into #AttyClnt
from hbm_client c
join hbm_persnl p on c.resp_empl_uno = p.empl_uno
where p.login = #login
and c.status_code = 'C'
select
ba.payr_client_uno as client_uno
, max(ba.tran_date) as tran_date
from blt_bill_amt ba
left outer join #AttyClnt ac on ba.payr_client_uno = ac.client_uno
where ba.tran_type IN ('RA', 'CR')
group by ba.payr_client_uno
Currently, this query will produce at least 1 row per client with a date, the problem is that there are some clients that will have between 2 and 10 dates associated with them bloating the return table to about 30,000 row instead of an idealistic 246 rows or less.
When i try doing max(tran_uno) to get the most recent transaction number, i get the same result, some have 1 value and others have multiple values.
The bigger picture has 4 other queries being performed doing other parts, i have only included the parts that pertain to the question.
Edit (2011-10-14 # 1:45PM):
select
ba.payr_client_uno as client_uno
, max(ba.row_uno) as row_uno
into #Bills
from blt_bill_amt ba
inner join hbm_matter m on ba.matter_uno = m.matter_uno
inner join hbm_client c on m.client_uno = c.client_uno
inner join hbm_persnl p on c.resp_empl_uno = p.empl_uno
where p.login = #login
and c.status_code = 'C'
and ba.tran_type in ('CR', 'RA')
group by ba.payr_client_uno
order by ba.payr_client_uno
--Obtain list of Transaction Date and Amount for the Transaction
select
b.client_uno
, ba.tran_date
, ba.tc_total_amt
from blt_bill_amt ba
inner join #Bills b on ba.row_uno = b.row_uno
Not quite sure what was going on but seems the Temp Tables were not acting right at all. Ideally i would have 246 rows of data, but with the previous query syntax it would produce from 400-5000 rows of data, obviously duplications on data.
I think you can use ranking to achieve what you want:
WITH ranked AS (
SELECT
client_uno = ba.payr_client_uno,
ba.tran_date,
be.tc_total_amt,
rnk = ROW_NUMBER() OVER (
PARTITION BY ba.payr_client_uno
ORDER BY ba.tran_uno DESC
)
FROM blt_bill_amt ba
INNER JOIN hbm_matter m ON ba.matter_uno = m.matter_uno
INNER JOIN hbm_client c ON m.client_uno = c.client_uno
INNER JOIN hbm_persnl p ON c.resp_empl_uno = p.empl_uno
WHERE p.login = #login
AND c.status_code = 'C'
AND ba.tran_type IN ('CR', 'RA')
)
SELECT
client_uno,
tran_date,
tc_total_amt
FROM ranked
WHERE rnk = 1
ORDER BY client_uno
Useful reading:
Ranking Functions (Transact-SQL)
ROW_NUMBER (Transact-SQL)
WITH common_table_expression (Transact-SQL)
Using Common Table Expressions

Looking up the first row of results

I have a history table containing a snapshot of each time a record is changed. I'm trying to return a certain history row with the original captured date. I am currently using this at the moment:
select
s.Description,
h.CaptureDate OriginalCaptureDate
from
HistoryStock s
left join
( select
StockId,
CaptureDate
from
HistoryStock
where
HistoryStockId in ( select MIN(HistoryStockId) from HistoryStock group by StockId )
) h on s.StockId = h.StockId
where
s.HistoryStockId = #HistoryStockId
This works but with 1 Million records its on the slow side and I'm not sure how to optimize this query.
How can this query be optimized?
UPDATE:
WITH OriginalStock (StockId, HistoryStockId)
AS (
SELECT StockId, min(HistoryStockId)
from HistoryStock group by StockId
),
OriginalCaptureDate (StockId, OriginalCaptureDate)
As (
SELECT h.StockId, h.CaptureDate
from HistoryStock h join OriginalStock o on h.HistoryStockId = o.HistoryStockId
)
select
s.Description,
h.OriginalCaptureDate
from
HistoryStock s left join OriginalCaptureDate h on s.StockId = h.StockId
where
s.HistoryStockId = #HistoryStockId
I've update the code to use CTE but I'm not better off performance wise, only have small performance increase. Any ideas?
Just another note, I need to get to the first record in the history table for StockId and not the earliest Capture date.
I am not certain I understand entirely how the data works from your query but nesting queries like that is never good for performance in my opinion. You could try something along the lines of:
WITH MinCaptureDate (StockID, MinCaptureDate)
AS (
SELECT HS.StockID
,MIN(HS.CaptureDate) AS OriginalCaptureDate
FROM HistoryStock HS
GROUP BY
HS.Description
)
SELECT HS.Description
,MCD.OriginalCaptureDate
FROM HistoryStock HS
JOIN MinCaptureDate MCD
ON HS.StockID = MCD.StockID
WHERE HS.StockID = #StockID
I think i see what you are trying to achieve. You basically want the description of the specified history stock record, but you want the date associated with the first history record for the stock... so if your history table looks like this
StockId HistoryStockId CaptureDate Description
1 1 Apr 1 Desc 1
1 2 Apr 2 Desc 2
1 3 Apr 3 Desc 3
and you specify #HistoryStockId = 2, you want the following result
Description OriginalCaptureDate
Desc 2 Apr 1
I think the following query would give you a slightly better performance.
WITH OriginalStock (StockId, CaptureDate, RowNumber)
AS (
SELECT
StockId,
CaptureDate,
RowNumber = ROW_NUMBER() OVER (PARTITION BY StockId ORDER BY HistoryStockId ASC)
from HistoryStock
)
select
s.Description,
h.CaptureDate
from
HistoryStock s left join OriginalStock h on s.StockId = h.StockId and h.RowNumber = 1
where
s.HistoryStockId = #HistoryStockId

Sql Server double subquery

I have a table which is kinda like an historic table... so I have data like this
idA numberMov FinalDate
1 10 20090209
2 14 20090304
1 12 20090304
3 54 20080508
4 42 20090510
... ... ....
I need to retrieve the numberMov based on the newest finalDate from each idA so I use this
select a.numberMov from (select idA, max(finalDate) maxDate from table1 group by idA) as b inner join table1 a on a.idA=b.idA and a.finalDate = b.maxDate
Now I have another query like this
select m fields from n tables where n5.numberMov in ("insert first query here")
I feel like there is a better solution but can't think of any, I really dont like having two subqueries in there.
Any suggestions?
Not enough information to test it myself but something like this might work.
select m fields
from a inner join
(select numberMov,
max(FinalDate) as maxDate
from a
group by numberMov) b
on a.numberMov = b.numberMov
and a.FinalDate = b.maxDate inner join
n tables on a.numberMov = n.numberMov
You don't say which edition of SQL server, but this will work in SQL 2005+
;WITH rankCTE
AS
(
SELECT idA
,numberMov
,FinalDate
,ROW_NUMBER() OVER (PARTITION BY idA
ORDER BY FinalDate DESC
) AS rn
FROM table1
)
,latestCTE
AS
(
SELECT idA
,numberMov
,FinalDate
FROM rankCTE
WHERE rn = 1
)
SELECT m fields
FROM n tables
WHERE n5.numberMov IN (SELECT numberMov FROM latestCTE)

Resources