Calulating percentage table column in SQL - sql

How can I calculate the percentage of a whole column? Currently what I've written so far only gives me the percentage of each row. I want only 1 row with the total percentage. I want to sum all the values to calculate one percentage.
SELECT TOP 1000
CONVERT(DECIMAL(10, 2), CAST(Passed AS DECIMAL(10, 2))
/ ( Actual ) * 100) AS Percent
FROM Audits
WHERE
MONTH(TransActionDate) = MONTH(GETDATE())
GROUP BY Actual
, Passed
, transactiondate
ORDER BY TransActionDate DESC

Try using AVG() and remove the fields from the GROUP BY clause:
SELECT AVG(CONVERT(DECIMAL(10, 2), CAST(Passed AS DECIMAL(10, 2))
/ ( Actual ) * 100)) AS Percent
FROM Audits
WHERE MONTH(TransActionDate) = MONTH(GETDATE())
If you need TOP 1000, then use a subquery:
SELECT AVG(Percent)
FROM (
SELECT TOP 1000
CONVERT(DECIMAL(10, 2), CAST(Passed AS DECIMAL(10, 2))
/ ( Actual ) * 100) AS Percent
FROM Audits
WHERE MONTH(TransActionDate) = MONTH(GETDATE())
ORDER BY TransActionDate DESC
) t

Related

How to spread month to day with amount value divided by total days per month

I have data with an amount of 1 month and want to change it to 30 days.
if 1 month the amount is 20000 then per day is 666.67
The following are sample data and results:
Account
Project
Date
Segment
Amount
Acc1
1
September 2022
Actual
20000
Result :
I need a query using sql server
You may try a set-based approach using an appropriate number table and a calculation with windowed COUNT().
Data:
SELECT *
INTO Data
FROM (VALUES
('Acc1', 1, CONVERT(date, '20220901'), 'Actual', 20000.00)
) v (Account, Project, [Date], Segment, Amount)
Statement for all versions, starting from SQL Server 2016 (the number table is generated using JSON-based approach with OPENJSON()):
SELECT d.Account, d.Project, a.[Date], d.Segment, a.Amount
FROM Data d
CROSS APPLY (
SELECT
d.Amount / COUNT(*) OVER (ORDER BY (SELECT NULL)),
DATEADD(day, CONVERT(int, [key]), d.[Date])
FROM OPENJSON('[1' + REPLICATE(',1', DATEDIFF(day, d.[Date], EOMONTH(d.[Date]))) + ']')
) a (Amount, Date)
Statement for SQL Server 2022 (the number table is generated with GENERATE_SERIES()):
SELECT d.Account, d.Project, a.[Date], d.Segment, a.Amount
FROM Data d
CROSS APPLY (
SELECT
d.Amount / COUNT(*) OVER (ORDER BY (SELECT NULL)),
DATEADD(day, [value], d.[Date])
FROM GENERATE_SERIES(0, DATEDIFF(day, d.[Date], EOMONTH(d.[Date])))
) a (Amount, Date)
Notes:
Both approaches calculate the days for each month. If you always want 30 days per month, replace DATEDIFF(day, d.[Date], EOMONTH(d.[Date])) with 29.
There is a rounding issue with this calculation. You may need to implement an additional calculation for the last day of the month.
You can use a recursive CTE to generate each day of the month and then divide the amount by the number of days in the month to achive the required output
DECLARE #Amount NUMERIC(18,2) = 20000,
#MonthStart DATE = '2022-09-01'
;WITH CTE
AS
(
SELECT
CurrentDate = #MonthStart,
DayAmount = CAST(#Amount/DAY(EOMONTH(#MonthStart)) AS NUMERIC(18,2)),
RemainingAmount = CAST(#Amount - (#Amount/DAY(EOMONTH(#MonthStart))) AS NUMERIC(18,2))
UNION ALL
SELECT
CurrentDate = DATEADD(DAY,1,CurrentDate),
DayAmount = CASE WHEN DATEADD(DAY,1,CurrentDate) = EOMONTH(#MonthStart)
THEN RemainingAmount
ELSE DayAmount END,
RemainingAmount = CASE WHEN DATEADD(DAY,1,CurrentDate) = EOMONTH(#MonthStart)
THEN 0
ELSE CAST(RemainingAmount-DayAmount AS NUMERIC(18,2)) END
FROM CTE
WHERE CurrentDate < EOMONTH(#MonthStart)
)
SELECT
CurrentDate,
DayAmount
FROM CTE
In case you want an equal split without rounding errors and without loops you can use this calculation. It spreads the rounding error across all entries, so they are all as equal as possible.
DECLARE #Amount NUMERIC(18,2) = 20000,
#MonthStart DATE = '20220901'
SELECT DATEADD(DAY,Numbers.i - 1,#MonthStart)
, ShareSplit.Calculated_Share
, SUM(ShareSplit.Calculated_Share) OVER (ORDER BY (SELECT NULL)) AS Calculated_Total
FROM (SELECT DISTINCT number FROM master..spt_values WHERE number BETWEEN 1 AND DAY(EOMONTH(#MonthStart)))Numbers(i)
CROSS APPLY(SELECT CAST(ROUND(#Amount * 100 / DAY(EOMONTH(#MonthStart)),0) * 0.01
+ CASE
WHEN Numbers.i
<= ABS((#Amount - (ROUND(#Amount * 100 / DAY(EOMONTH(#MonthStart)),0) / 100.0 * DAY(EOMONTH(#MonthStart)))) * 100)
THEN 0.01 * SIGN(#Amount - (ROUND(#Amount * 100 / DAY(EOMONTH(#MonthStart)),0) / 100.0 * DAY(EOMONTH(#MonthStart))))
ELSE 0
END AS DEC(18,2)) AS Calculated_Share
)ShareSplit

How to avoid function repetition in SELECT, GROUP BY, and ORDER BY in SQL

I am writing a statistical query where the value is duplicated in SELECT, GROUP BY, and ORDER BY. Having to repeat the same value makes it hard to read the query and modify it.
How can I avoid repeating FLOOR(COALESCE(LEN(Body), 0) / 100) 3-4 times in the query below.
SELECT FLOOR(COALESCE(LEN(Body), 0) / 100) * 100 as BodyLengthStart,
(FLOOR(COALESCE(LEN(Body), 0) / 100) + 1) * 100 - 1 as BodyLengthEnd,
COUNT(*) as MessageCount
FROM [Message]
GROUP BY FLOOR(COALESCE(LEN(Body), 0) / 100)
ORDER BY FLOOR(COALESCE(LEN(Body), 0) / 100)
The output of the query is the number of messages bucketed by how many hundreds of characters they have.
BodyLengthStart
BodyLengthEnd
MessageCount
0
99
130
100
199
76
200
299
36
Using CROSS APPLYs
SELECT BodyLengthStart,
BodyLengthEnd,
COUNT(*)
FROM [Message]
CROSS APPLY (
VALUES
(FLOOR(COALESCE(LEN(Body), 0) / 100))
) a1(v)
CROSS APPLY (
VALUES
(v * 100, (v + 1) * 100 - 1)
) a2(BodyLengthStart, BodyLengthEnd)
GROUP BY BodyLengthStart,
BodyLengthEnd
One option may be a CTE (Common Table Expression), something along these lines:
WITH x AS
(
SELECT FLOOR(COALESCE(LEN(Body), 0) / 100) AS BodyLength
FROM [Message]
)
SELECT BodyLength * 100 AS BodyLengthStart,
(BodyLength + 1) * 100 - 1 AS BodyLengthEnd,
COUNT(*) as MessageCount
FROM x
GROUP BY BodyLength
ORDER BY BodyLength
As a side note - if the statement prior to this doesn't end with a semi-colon (;), this will not work as expected.
Use a sub-select:
SELECT BodyLengthStart,
BodyLengthEnd,
COUNT(*)
FROM (SELECT FLOOR(COALESCE(LEN(Body), 0) / 100) * 100 as BodyLengthStart,
(FLOOR(COALESCE(LEN(Body), 0) / 100) + 1) * 100 - 1 as BodyLengthEnd
FROM [Message]) as a
GROUP BY BodyLengthStart,
BodyLengthEnd
You can define a SELECT after the FROM; in this way, you can elaborate previously your data.
You can use a common table expression:
WITH cte AS
(
SELECT FLOOR(COALESCE(LEN(Body), 0) / 100) * 100 as BodyLengthStart,
(FLOOR(COALESCE(LEN(Body), 0) / 100) + 1) * 100 - 1 as BodyLengthEnd
FROM [Message]
)
SELECT BodyLengthStart,BodyLengthEnd,COUNT(*)
FROM cte
GROUP BY BodyLengthStart,BodyLengthEnd

This query would be too heavy , need to be refactored. how can i do?

This query would be too heavy, needs to be refactored. How can I do that?
Please help
SELECT
contract_type, SUM(fte), ROUND(SUM(fte * 100 / t.s ), 0) AS "% of total"
FROM
design_studio_testing.empfinal_tableau
CROSS JOIN
(SELECT SUM(fte) AS s
FROM design_studio_testing.empfinal_tableau) t
GROUP BY
contract_type;
Output should be like this:
Use window functions:
SELECT contract_type,
SUM(fte),
ROUND(SUM(fte) * 100.0 / SUM(SUM(fte)) OVER (), 0) AS "% of total"
FROM design_studio_testing.empfinal_tableau
GROUP BY contract_type;
That said, your original version should not be that much slower than this, unless perhaps empfinal_tableau is a view.
If it is a table, you could further speed this with an index on empfinal_tableau(contract_type, fte).
There is no need to sum over the expression:
fte * 100 / t.s
which may slow the process.
Get SUM(fte) and then multiply and divide:
SELECT g.contract_type, g.sum_fte,
ROUND(100.0 * g.sum_fte / t.s, 0) AS [% of total]
FROM (
SELECT
contract_type,
SUM(fte) AS sum_fte
FROM design_studio_testing.empfinal_tableau
GROUP BY contract_type
) AS g CROSS JOIN (SELECT SUM(fte) AS s FROM design_studio_testing.empfinal_tableau) t
Edit for Oracle:
SELECT g.contract_type, g.sum_fte,
ROUND(100.0 * g.sum_fte / t.s, 0) AS "% of total"
FROM (
SELECT
contract_type,
SUM(fte) AS sum_fte
FROM empfinal_tableau
GROUP BY contract_type
) g CROSS JOIN (SELECT SUM(fte) AS s FROM empfinal_tableau) t

Calculating Percentages with SUM and Group by

I am trying to create an Over Time Calculation based on some set criteria. It goes as follows.
Overtime is posted on any day that is over 8 hrs but an employee has to reach 40 total hrs first and the calculation starts at the 1st day moving forward in the week. The Overtime is calculated based on the percentage taken of the SUM total of the cost codes worked.
First you have to find the percentage of each cost code worked for the entire week per employee id. See Example below
Then each day that is Over 8 hrs you take the time on that code for the day and multiply it by the calculated percentage. At the end of the week the regular hours must total 40hrs if they have gone over 40 for the week. See below example
CREATE TABLE [Totals](
[Day] nvarchar (10) null,
[EmployeeID] [nvarchar](100) NULL,
[CostCode] [nvarchar](100) NULL,
[TotalTime] [real] NULL,)
INSERT Into Totals (day,employeeid, CostCode, TotalTime) VALUES
('1','1234','1', 2),
('1','1234','2', 7.5),
('2','1234','1', 1.5),
('2','1234','2', 8),
('3','1234','1', 1),
('3','1234','2', 6),
('4','1234','1', 2),
('4','1234','2', 8),
('5','1234','1', 2),
('5','1234','2', 8),
('1','4567','1', 2),
('1','4567','2', 8.5),
('2','4567','1', 1.5),
('2','4567','2', 7.6),
('3','4567','1', 1),
('3','4567','2', 5),
('4','4567','1', 2),
('4','4567','2', 8),
('5','4567','1', 2),
('5','4567','2', 8)
To get the percentage of each cost Worked it is the SUM total time of each cost per week / SUM total time of the entire week
SELECT employeeid,CostCode,SUM(totaltime) As TotalTime ,
ROUND(SUM(Totaltime) / (select SUM(TotalTime) from Totals where employeeid = '1234') * 100,0) as Percentage
from Totals WHERE EmployeeID = '1234' group by EmployeeID, CostCode
Percentage Calculated for the Week by Cost = 18% worked on Cost 1 and 82% on Cost 2
I would like to take the percentage results for the week and calculate the total time each day in the query
Results Example Day 1: for EmployeeID 1234
Day CostCode RegTime OverTime
1 1 1.73 .27
1 2 6.27 1.23
After editing i get your result, try this:
select calc.*
--select [day], CostCode, EmployeeID
--, CPr * DayEmpRT RegTime_old
, TotalTime - CPr * DayEmpOT RegTime
, CPr * DayEmpOT OverTime
from (
select Agr.*
--, round(EmpC1T / EmpT, 2) C1Pr
--, round(1 - (EmpC1T / EmpT), 2) C2Pr
, round(EmpCT / EmpT, 2) CPr
, case when DayEmpT > 8 then 8 else DayEmpT end DayEmpRT
, case when DayEmpT > 8 then DayEmpT - 8 else 0 end DayEmpOT
from (
select Totals.*
, SUM(TotalTime) over (partition by EmployeeID, [day]) DayEmpT
--, SUM(case when CostCode = 1 then TotalTime end) over (partition by EmployeeID) EmpC1T
, SUM(TotalTime) over (partition by EmployeeID, CostCode) EmpCT
, SUM(TotalTime) over (partition by EmployeeID) EmpT
from Totals
WHERE EmployeeID = '1234' ) Agr ) calc
order by 1,2,3
here is simplest way to calculate this:
select calc.*
, TotalTime * pr4R RegTime
, TotalTime * pr4O OverTime
from(
select Agr.*
, case when EmpT > 40 then round(40/EmpT, 2) else 1 end pr4R
, case when EmpT > 40 then round(1 - 40/EmpT, 2) else 1 end pr4O
from (
select Totals.*
, SUM(TotalTime) over (partition by EmployeeID) EmpT
from Totals
WHERE EmployeeID = '1234' ) Agr ) calc
but be watch on day 3, because there is only 7h.
The 1st query calculate days separately and live day 3.
The 2nd query scale all hours.
it could be another one, that calculate all emp rows but scale separatly RegTime and OverTime, with exception on day where is no 8h and increment it to 8h from OverTime.
This should help you get started...
-- % based on hours worked for each code on a DAILY basis (The original 21% in the question was based on this)
SELECT
T.EmployeeId,
T.Day,
T.CostCode,
T.TotalTime,
CAST(100.0 * T.TotalTime / X.DailyHours AS DECIMAL(10,2)) AS PercentageWorked
FROM #Totals T
INNER JOIN (
SELECT
EmployeeId,
Day,
SUM(TotalTime) AS DailyHours
FROM #Totals
GROUP BY EmployeeId, Day
) X ON X.EmployeeId = T.EmployeeId AND X.Day = T.Day
-- % based on hours worked for each code on a WEEKLY basis (The revised question)
SELECT
T.EmployeeId,
T.CostCode,
SUM(T.TotalTime) AS TotalTime,
CAST(100.0 * SUM(T.TotalTime) / X.WeeklyHours AS DECIMAL(10,2)) AS PercentageWorked
FROM #Totals T
INNER JOIN (
SELECT
EmployeeId,
SUM(TotalTime) AS WeeklyHours
FROM #Totals
GROUP BY EmployeeId
) X ON X.EmployeeId = T.EmployeeId
GROUP BY
T.EmployeeId,
T.CostCode,
X.WeeklyHours

SQL Rounding integers with no decimals

I have what seems like a simple problem. I just want to show percentages with no decimal places and have the total add to 100. Here's a little snipit:
create table genderTable
(person varchar(10),
isMale varchar(5))
insert into genderTable values ('Mary', 'false')
insert into genderTable values ('Frank', 'true')
insert into genderTable values ('Bill', 'true')
insert into genderTable values ('Jessie', 'false')
insert into genderTable values ('Sue', 'false')
insert into genderTable values ('Beth', 'false')
insert into genderTable values ('Kris', 'false')
declare #total as int
set #total = 7
select
CASE isMale
WHEN 'True' THEN 'Male'
ELSE 'Female'
END as Gender,
CASE
WHEN #total > 0 THEN ROUND((count(isMale) * 100 / #total), 0)
ELSE 0
END as GenderPercent
from genderTable
group by isMale
The totals add to 99% instead of 100%. I've tried various rounding, but I either get one decimal or 99%. Any help? Please keep in mind, in another example, I have to do the same for ethnicity where there are more than two values, so subtracting from 100 probably won't work...
For you particular problem, I get numbers that add up to 100 when I do:
select isMale, count(*), sum(count(*)) over (),
round(100.0 * count(*) / sum(count(*)) over (), 0)
from genderTable t
group by isMale;
The actual problem with your implementation is that SQL Server does integer arithmetic. So, the expression ROUND((count(isMale) * 100 / #total), 0) is doing the calculation with integer division -- taking the floor() of the ratio before implementing the round().
There are ways to do what you want. They are more easily implemented in SQL Server 2012+ than in earlier versions:
select isMale,
round(100.0 * cnt / tot, 0) as p,
(case when seqnum = 1
then 100 - sum(round(100.0 * cnt / tot, 0)) over (order by seqnum desc rows between unbounded preceding and 1 preceding)
else round(100.0 * cnt / tot, 0)
end) as p_tot_100
from (select isMale, count(*)*1.0 as cnt, sum(1.0*count(*)) over () as tot,
row_number() over (order by isMale) as seqnum
from genderTable t
group by isMale
) t;
The idea is to sum the rounded versions of all other rows and subtract this from 100 for one of the rows.
EDIT:
The "integer" versions of these:
select isMale, count(*), sum(count(*)) over (),
cast(round(100.0 * count(*) / sum(count(*)) over (), 0) as int)
from genderTable t
group by isMale;
and:
select isMale,
round(100.0 * cnt / tot, 0) as p,
cast((case when seqnum = 1
then 100 - sum(round(100.0 * cnt / tot, 0)) over (order by seqnum desc rows between unbounded preceding and 1 preceding)
else round(100.0 * cnt / tot, 0)
end) as int) as p_tot_100
from (select isMale, count(*)*1.0 as cnt, sum(1.0*count(*)) over () as tot,
row_number() over (order by isMale) as seqnum
from genderTable t
group by isMale
) t;
I'm pretty sure that small integers are represented exactly, even with floating point representation, so there will be no problem of round() producing a value such as 29.99999999999997 instead of 30.
I just saw the answer above. Nice work Gordon!
Here is a similar version of the answer:
SELECT CASE isMale
WHEN 'True'
THEN 'Male'
ELSE 'Female'
END AS Gender
,CAST(left(round(count(isMale) * 100.00 /(select count(*) from gendertable), 0),2) AS VARCHAR) + '%' AS GenderPercent
FROM genderTable
GROUP BY isMale