Multiple aggregations with different where clauses - sql-server

I have a table called Entries that manages transactions for specific Units.
In entries, there is a column called Sales Source that specifies the type of sale completed. I am trying to create a table that shows the total amount of revenue made per unit per sale source type.
For example:
A unit called AB104 has 8000$ in revenue, 5000$ was made via a Phone source and 3000$ was from a Fax source. I want to display a table that shows the unit number and the amount made for each sale type in the same line.
I tried the following query:
SELECT Unit,
sum(MoneyIN + MoneyOUT) AS Revenue,
sum(VolumeOUT+ VolumeIN) AS volume,
sum(WeightOUT + WeightIN)AS weight,
PercentageR = convert(VARCHAR,convert(MONEY,sum(MoneyIN+MoneyOUT)*100 /
(SELECT sum(MoneyIN + MoneyOUT)
FROM Entries)), 1) + '%',
PercentageV = convert(VARCHAR,convert(MONEY,sum(VolumeIN+VolumeOUT)*100 /
(SELECT sum(VolumeIN + VolumeOUT)
FROM Entries)), 1) + '%',
PercentageW = convert(VARCHAR,convert(MONEY,sum(WeightIN+WeightOUT)*100 /
(SELECT sum(WeightIN + WeightOUT)
FROM Entries)), 1) + '%',
LinkRevenue=
(SELECT sum(MoneyIN+MoneyOUT)
WHERE salesSource ='Link'),
PhoneRevenue=
(SELECT sum(MoneyIN+MoneyOUT)
WHERE salesSource ='Phone'),
EmailRevenue=
(SELECT sum(MoneyIN+MoneyOUT)
WHERE salesSource ='Email'),
FaxRevenue=
(SELECT sum(MoneyIN+MoneyOUT)
WHERE salesSource ='Fax'),
NoneRevenue=
(SELECT sum(MoneyIN+MoneyOUT)
WHERE salesSource ='None')
FROM Entries
GROUP BY Unit,
SalesSource
ORDER BY Unit
The problem with that query however, is that it doesnt show the different types of revenues for each unit in one line. This query would output:
Unit | rev | vol | weight | % Revenue | % Weight | % Volume | $ Link |$Phone |$Email|$Fax |$None
AB104|5000$|22|15000| 17%| 10%| 15%| 0$|5000$|0$|0$|0$
AB104|3000$|12|18000| 21%| 21%| 7%| 0$|0$|0$|3000$|0$
instead I want it to group all the details for the same unit in one line, like this:
Unit | rev | vol | weight | % Revenue | % Weight | % Volume | $ Link |$Phone |$Email|$Fax |$None
AB104|8000$|34|33000| 38%| 31%| 22%| 0$|5000$|0$|3000$|0$
How can I accomplish that? when I take salesSource out of the grouping, I get an error.

Your group by has two columns in it. You are seeing a Unit row for every SalesSource. You can add SalesSource as a column by itself and see.
Also, you want to avoid adding additional selects in the select clause cause your making SQL run a separate query for every result, in your case 3 queries for every result. This will make your query not very scalable.
I think you need to remove the SalesSource from you group by clause and use case statements.
Sum(Case when SalesSource = 'Link' then (MoneyIn+MoneyOut) else 0 end) as LinkRevenue,
You can get rid of the select-in-a-select by using a common table expression:
;with totals as (
SELECT sum(WeightIN + WeightOUT) as WeightTotal,
sum(VolumeIN + VolumeOUT) as VolumeTotal,
sum(MoneyIN + MoneyOUT) as MoneyTotal
FROM Entries
)
SELECT Unit,
sum(MoneyIN + MoneyOUT) AS Revenue,
sum(VolumeOUT+ VolumeIN) AS volume,
sum(WeightOUT + WeightIN)AS weight,
PercentageR = convert(VARCHAR,convert(MONEY,sum(MoneyIN+MoneyOUT)*100 /
totals.MoneyTotal), 1) + '%',
PercentageV = convert(VARCHAR,convert(MONEY,sum(VolumeIN+VolumeOUT)*100 /
totals.VolumeTotal), 1) + '%',
PercentageW = convert(VARCHAR,convert(MONEY,sum(WeightIN+WeightOUT)*100 /
totals.WeightTotal), 1) + '%',
Sum(Case when SalesSource = 'Link' then (MoneyIn+MoneyOut) else 0 end) as LinkRevenue,
....
FROM Entries, totals
GROUP BY Unit
ORDER BY Unit

You can clean it up with a couple more cte's, I didn't format the Money columns with the $ sign but you can add that into the final select
With
--create CTE with money columns summed for the pivotcte
TotalsCTEBySource (Unit, [Revenue], SalesSource) AS
(SELECT Unit, MoneyIn+MoneyOut , SalesSource FROM Entries),
--create CTE with table grand totals for % calcs
GrandTotalsCTE ([Revenue], [Volume],[Weight]) AS
(SELECT sum(MoneyIn+MoneyOut) , sum(VolumeIn+VolumeOut) , sum(WeightIn+WeightOut) FROM Entries),
--create totals by unit cte
TotalsCTEByUnit (Unit, [Revenue], [Volume],[Weight]) AS
(SELECT Unit, sum(MoneyIn+MoneyOut) , sum(VolumeIn+VolumeOut) , sum(WeightIn+WeightOut)
FROM Entries GROUP BY Unit),
--create pivot cte to get the sales source values as column headers, use coalesce() to turn NULL into 0
PivotCTE (Unit, [$ Link],[$ Phone],[$ Fax],[$ Email],[$ None])AS
( select Unit,coalesce(sum([Link]),0), coalesce(sum([Phone]),0),coalesce(sum([Fax]),0),
coalesce(sum([Email]),0),coalesce(sum([None]),0) FROM(
select Unit, [Link],[Phone],[Fax],[Email],[None] from TotalsCTEBySource
PIVOT
(sum(Revenue)FOR SalesSource in ([Link],[Phone],[Fax],[Email],[None])) pivottable
) moneybygroup
group by Unit )
--bring it all together
SELECT t.Unit, t.Revenue, t.Volume, t.Weight,
CONVERT(VARCHAR(50),round(t.Revenue/g.revenue*100,0))+' %' AS [% Revenue],
CONVERT(VARCHAR(50),round(t.Volume/g.Volume*100,0))+' %' AS [% Volume],
CONVERT(VARCHAR(50),round(t.Weight/g.Weight*100,0))+' %' AS [% Weight]
p.[$ Link],p.[$ Phone],p.[$ Fax],p.[$ Email],p.[$ None]
FROM TotalsCTEByUnit t
Join PivotCTE p on t.unit=p.unit
join GrandTotalsCTE g on 1=1

Related

Return all records with a balance below a threshold value

I'm trying to setup a query to return all order line items with an outstanding balance below a certain threshold value (5%, for example). I managed this query without any concerns, but there is a complication. I only want to return these line items in cases where there aren't any line items outside of this threshold.
For example, if line item 1 has an Ordered Qty of 100, and 98 have been received, this line item would be returned unless there is a line item 2 with an Order qty of 100 and 50 received (since this is above the 5% threshold).
This might be more easily demonstrated than explained, so I set up a simplified SQL Fiddle to show what I have thus far. I'm using a CTE to add a remaining balance field and then querying against that within my threshold. I appreciate any advice
In the fiddle example, OrderNum 987654 should NOT be returned since that order has a second line item with 50% remaining.
SQL Fiddle
;WITH cte as (
SELECT
h.OrderNum
,d.ItemNumber
,d.OrderedQty
,d.ReceivedQty
,100.0 * (1 - (CAST(d.ReceivedQty as Numeric(10, 2)) / d.OrderedQty)) as RemainingBal
FROM OrderHeader h
INNER JOIN OrderDetail d
ON h.OrderNum = d.OrderNum
)
SELECT * FROM Cte
WHERE RemainingBal >0 and RemainingBal <= 5.0
I got this to work...
;WITH cte as (
SELECT
h.OrderNum
,d.ItemNumber
,d.OrderedQty
,d.ReceivedQty
,100.0 * (1 - (CAST(d.ReceivedQty as Numeric(10, 2)) / d.OrderedQty)) as
RemainingBal
FROM OrderHeader h
INNER JOIN OrderDetail d
ON h.OrderNum = d.OrderNum
)
SELECT * FROM Cte WHERE OrderNum IN(
SELECT OrderNum
FROM Cte
GROUP BY OrderNum
HAVING CAST((SUM(OrderedQty)) - (SUM(ReceivedQty)) AS
DECIMAL(10,2))/CAST(SUM(OrderedQty) AS DECIMAL(10,2)) <= .05
)

Show records in three tables with defiate number of rows in each table in SSRS report

I have a table in report like
I want to show the records in three tables on every page, each table contains only 20 records.
Page1:
Page2:
How can I achieve this type of pattern?
I can think of 2 ways to do this, as a MATRIX style report where the column group is your columns, and as a normal table where you JOIN the data to produce 3 copies of name, ID, and any other fields you want. The MATRIX style is definitely more elegant and flexible, but the normal table might be easier for customers to modify if you're turning the report over to power users.
Both solutions start with tagging the data with PAGE, ROW, and COLUMN information. Note that I'm sorting on NAME, but you could sort on any field. Also note that this solution does not depend on your ID being sequential and in the order you want, it generates it's own sequence numbers based on NAME or whatever else you choose.
In this demo I'm setting RowsPerPage and NumberofColumns as hard coded constants, but they could easily be user selected parameters if you use the MATRIX format.
DECLARE #RowsPerPage INT = 20
DECLARE #Cols INT = 3
;with
--Fake data generation BEGIN
cteSampleSize as (SELECT TOP 70 ROW_NUMBER () OVER (ORDER BY O.name) as ID
FROM sys.objects as O
), cteFakeData as (
SELECT N.ID, CONCAT(CHAR(65 + N.ID / 26), CHAR(65 + ((N.ID -1) % 26))
--, CHAR(65 + ((N.ID ) % 26))
) as Name
FROM cteSampleSize as N
),
--Fake data generation END, real processing begins below
cteNumbered as ( -- We can't count on ID being sequential and in the order we want!
SELECT D.*, ROW_NUMBER () OVER (ORDER BY D.Name) as SeqNum
--Replace ORDER BY D.Name with ORDER BY D.{Whatever field}
FROM cteFakeData as D --Replace cteFakeData with your real data source
), ctePaged as (
SELECT D.*
, 1+ FLOOR((D.SeqNum -1) / (#RowsPerPage*#Cols)) as PageNum
, 1+ ((D.SeqNum -1) % #RowsPerPage) as RowNum
, 1+ FLOOR(((D.SeqNum-1) % (#RowsPerPage*#Cols) ) / #RowsPerPage) as ColNum
FROM cteNumbered as D
)
--FINAL - use this for MATRIX reports (best)
SELECT * FROM ctePaged ORDER BY SeqNum
If you want to use the JOIN method to allow this in a normal table, replace the --FINAL query above with this one. Note that it's pretty finicky, so test it with several degrees of fullness in the final report. I tested with 70 and 90 rows of sample data so I had a partial first column and a full first and partial second.
--FINAL - use this for TABLE reports (simpler)
SELECT C1.PageNum , C1.RowNum , C1.ID as C1_ID, C1.Name as C1_Name
, C2.ID as C2_ID, C2.Name as C2_Name
, C3.ID as C3_ID, C3.Name as C3_Name
FROM ctePaged as C1 LEFT OUTER JOIN ctePaged as C2
ON C1.PageNum = C2.PageNum AND C1.RowNum = C2.RowNum
AND C1.ColNum = 1 AND (C2.ColNum = 2 OR C2.ColNum IS NULL)
LEFT OUTER JOIN ctePaged as C3 ON C1.PageNum = C3.PageNum
AND C1.RowNum = C3.RowNum AND (C3.ColNum = 3 OR C3.ColNum IS NULL)
WHERE C1.ColNum = 1
1) Add the dataset with the below query to get Page number and Table number. You can change the number 20 and 60 as per requirement. In my case, I need 20 records per section and having 3 sections, so total records per page are 60.
Select *,(ROW_NUMBER ( ) OVER ( partition by PageNumber order by Id )-1)/20 AS TableNumber from (
Select (ROW_NUMBER ( ) OVER ( order by Id )-1)/60 AS PageNumber
,* from Numbers
)Src
2)Add the table of one column and select the prepared dataset.
3)Add PageNumber in Group expression for Details group.
4)Add the Column parent group by right-clicking on detail row. Select Group by TableNumber.
5) Delete the first two rows. Select Delete rows only.
6) Add one more table and select the ID and Name.
7) Drag this newly created table into the cell of the previously created table. And increase the size of the table.
Result:
Each table section contains 20 records. and it will continue in next pages also.

Assigning data to groups based on percentage splits for count and total balance allocated

This might be a nice Friday afternoon SQL Puzzle for someone? ;D
I have a requirement for assigning customers to debt collection agencies (DCAs). Basically we agree a split, e.g. 65%:35%, or 50%:50%, or even 60%:30%:10% (as in my example) where the percentages govern two things:
the percentage of the number of customers assigned to each DCA;
the total percentage of debt assigned to each DCA.
My challenge is to come up with the best match I can find, e.g. if I had four customers:
Customer A owes £100;
Customer B owes £500;
Customer C owes £550;
Customer D owes £50.
The "ideal" match for a 50%:50% split would be:
DCA 1
- Customers A and B = 2 customers with a total debt of £600;
DCA 2
- Customers C and D = 2 customers with a total debt of £600.
...and a "bad" match would be:
DCA 1
- Customers A and D = 2 customers with a total debt of £150;
DCA 2
- Customers B and C = 2 customers with a total debt of £1050.
I can think of ways to solve this problem in .NET, and also "loop" based solutions for SQL scripts. These all work on the logic:
make random assignments by numbers only, i.e. put the right number of customers in each DCA "bucket", but don't attempt to split the balance properly;
does swapping one customer with another in a different bucket improve the balance assignment? If it does swap them;
repeat until all swaps have been considered.
I can't come up with a set-based solution to the problem.
Here is some test data, and a sample of what I have tried so far:
--Create some test data
DECLARE #table TABLE (
account_number VARCHAR(50),
account_balance NUMERIC(19,2));
INSERT INTO #table SELECT '1', 100;
INSERT INTO #table SELECT '2', 1200;
INSERT INTO #table SELECT '3', 500;
INSERT INTO #table SELECT '4', 300;
INSERT INTO #table SELECT '5', 200;
INSERT INTO #table SELECT '6', 250;
INSERT INTO #table SELECT '7', 400;
INSERT INTO #table SELECT '8', 300;
INSERT INTO #table SELECT '9', 280;
INSERT INTO #table SELECT '10', 100;
--Define the split by debt collection agency
DECLARE #split TABLE (
dca VARCHAR(10),
dca_split_percentage INT);
INSERT INTO #split SELECT 'dca1', 60;
INSERT INTO #split SELECT 'dca2', 30;
INSERT INTO #split SELECT 'dca3', 10;
--Somewhere to store the results
DECLARE #results TABLE (
dca VARCHAR(10),
account_number VARCHAR(50),
account_balance NUMERIC(19,2));
--Populate the results
WITH DCASplit AS (
SELECT dca, dca_split_percentage / 100.0 AS percentage FROM #split),
DCAOrdered AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY dca) AS dca_id FROM DCASplit),
Customers AS (
SELECT COUNT(*) AS customer_count FROM #table),
Random AS (
SELECT *, NEWID() AS random FROM #table),
Ordered AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY random) AS order_id FROM Random),
Splits AS (
SELECT TOP 1 d.dca_id, d.dca, 1 AS start_id, CONVERT(INT, c.customer_count * d.percentage) AS end_id FROM DCAOrdered d CROSS JOIN Customers c
UNION ALL
SELECT d.dca_id, d.dca, s.end_id + 1 AS start_id, CONVERT(INT, s.end_id + c.customer_count * d.percentage) AS end_id FROM Splits s INNER JOIN DCAOrdered d ON d.dca_id = s.dca_id + 1 CROSS JOIN Customers c)
INSERT INTO
#results
SELECT
d.dca,
o.account_number,
o.account_balance
FROM
DCAOrdered d
INNER JOIN Splits s ON s.dca_id = d.dca_id
INNER JOIN Ordered o ON o.order_id BETWEEN s.start_id AND s.end_id;
--Show the raw results
SELECT * FROM #results;
--Show the aggregated results
WITH total_debt AS (SELECT SUM(account_balance) AS total_balance FROM #results)
SELECT r.dca, COUNT(*) AS customers, SUM(r.account_balance), SUM(r.account_balance) / t.total_balance AS percentage_debt FROM #results r CROSS JOIN total_debt t GROUP BY r.dca, t.total_balance;
I know this works for splitting by the number of customers, but it makes no attempt to try and split out the balances using the percentage split. In fact the split is entirely random each time the script runs.
For example, I got these results from one run:
dca account_number account_balance
dca1 8 £300.00
dca1 2 £1200.00
dca1 5 £200.00
dca1 3 £500.00
dca1 9 £280.00
dca1 6 £250.00
dca2 10 £100.00
dca2 7 £400.00
dca2 1 £100.00
dca3 4 £300.00
But when I add that all up I get:
dca customers debt_assigned percentage_debt
dca1 6 £2730.00 75%
dca2 3 £600.00 17%
dca3 1 £300.00 8%
i.e. the 60:30:10 split is fine for customer counts, but I have assigned 75% of the debt to DCA1 when they should only be getting 60%, etc.
I could use a "brute force" solution, which would be to run that query multiple times, then pick the results set with the closest match by debt. However, the performance of that would be poor, it still wouldn't guarantee I would get the "best split", and I am dealing with hundreds of thousands of customers in real life.
Any ideas for how to solve this problem in a single set-based query?

Calculate percentage in integer of a column in SQL SERVER?

I have just started experimenting how to calculate the percentage of a row. This is the code I write.
SELECT DISTINCT
ServiceName
COUNT(serviceID) AS Services
FROM Tester_DW
WHERE DateToday=20150410
GROUP BY ServiceName
How can calculate the percentage of the column Services above, and have the percentage in integer? Is it easier to calculate the percentage of the code example if I put my query result in a #temp table and calculate the percentage from the #temp or is it possible to calculate the percentage in integer% on the fly?
ADDED:Output sketch
ServiceName|Services| % of Total
--------------------------------
TV-cable | 4500 | 40%
--------------------------------
Mobile BB | 3000 | 10%
--------------------------------
MOBILE wifi| 20 | 5%
--------------------------------
It is hard to get it right, because you should deal with the sum of rounded integer percentage to get it 100% in total.
Using Largest Remainder Method
;WITH x AS
(
SELECT
ServiceName,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () AS [Percent],
FLOOR(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER ()) AS [IntPercent],
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () % 1 AS [Remainder]
FROM Tester_DW
GROUP BY ServiceName
)
SELECT ServiceName, IntPercent + CASE WHEN Priority <= Gap THEN 1 ELSE 0 END AS IntPercent
FROM
(
SELECT *, ROW_NUMBER() OVER (ORDER BY Remainder DESC) AS Priority, 100 - SUM(IntPercent) OVER () AS Gap FROM x
) data
Percentage is a count divided by the overall (reference) count. You have to use an inner query to get that overall count:
SELECT ServiceName, COUNT(serviceID) AS Services,
FLOOR(COUNT(serviceID) / (SELECT COUNT(serviceID) FROM Tester_DW)) AS percent
FROM Tester_DW WHERE...
Depending on the output you want (that is, what your reference count is), you may have to add the WHERE clause (or parts of it) in the inner query as well.
Select
ServiceName,
Count(ServiceName) as Services,
(Count(ServiceName)* 100 / (Select Count(*) From Tester_DW)) as [% of total]
From Tester_DW
Group By ServiceName
You simply divide the count of a single service by the amount of all services.
Try with window functions:
Create table t(id int)
Insert into t values
(1),(1),(2)
Select * from (Select id,
100.0*count(*) over(partition by id)/ count(*) over() p
from t) t
Group by id, p

How do you create a summary column?

Here is my current SQL code,
select
coalesce(cast(machinename as varchar(28)), 'Grand Total:') as 'machinename',
(IsNull(cast(CRATE_SMALL / 60 as varchar(24)),'0') + ':' + IsNull(cast(CRATE_SMALL % 60 as varchar(24)),'0') ) as '1001' ,
(IsNull(cast(CRATE_MEDIUM / 60 as varchar(24)),'0') + ':' + IsNull(cast(CRATE_MEDIUM % 60 as varchar(24)),'0'))as '1002',
(IsNull(cast(NO_SCHEDULE / 60 as varchar(24)),'0') + ':' + IsNull(cast(NO_SCHEDULE % 60 as varchar(24)),'0')) as '9999'
from (
select machinename ,
sum(case when vfrm.job_id = '1001' then DateDiff(mi, 0, total_time) end) as CRATE_SMALL ,
sum(case when vfrm.job_id = '1002' then DateDiff(mi, 0, total_time) end) as CRATE_MEDIUM ,
sum(case when vfrm.job_id = '9999' then DateDiff(mi, 0, total_time) end) as NO_SCHEDULE
from ven_fullreportmaster vfrm
INNER JOIN ven_descriptionmaster VDM ON VDM.description_id = vfrm..description_id
inner join ven_machinemaster vm on vm.machine_id = vfrm..machine_id
where vfrm.entry_date = convert(varchar, getdate()-7, 105)
and vfrm.shift_id =1
and vfrm.is_task_completed ='Y'
group by machinename with rollup
) as SubQueryALias
the output :
machinename 1001 1002 9999
ARISTECH 0:0 0:0 10:0
FADAL 0:0 5:0 10:0
Grand Total: 0:0 5:0 20:0
problem:
is there a anyway to show only the column(s) whose total is greater than zero...
So, in the above example I dont want to show the column name '1001'.
In all honesty, you shouldn't. This is a display issue, and as such should be dealt with when you display the data, not retrieve it from the database.
Reports, and datagrids and the like generally have functionality to do exactly this. Perhaps give more info about how you are displaying the data and someone might be able to provide more info.
To add the totals row, you ought to look at the ROLLUP part of the GROUP BY clause. This can produce sub-totals as well as the grand total, depending on what your final requirements are.
For hiding a column, there's not a solution for that in tsql - a SELECT statement always produces result sets with the same shape (names and types of columns).
But both may be better served by a reporting tool, if that's where this data is going (which I expect it is, given the nature of the query). Reporting tools tend to have better post-processing facilities.
try this:
select
entry_date, machinename, [1001], [1002], [9999], [1001]+[1002]+[9999] as Total
FROM ( --your query here
) d
WHERE [1001]+[1002]+[9999]>0
how about use a temp table to store the data query out and then build the output data from the temp table?
just
1.{your select query} into #t {your from where query}
2.select entry_date, machinename, [1001], [1002], [9999] from #t
union select '' as entry_date, 'total', sum([1001]), sum([1002]), sum([9999]) from #t
the logic is more clear with these steps, however, you can also use similar subquery to get the same result

Resources