Help with a query - sql

Based on the following table
ID Effort Name
-------------------------
1 1 A
2 1 A
3 8 A
4 10 B
5 4 B
6 1 B
7 10 C
8 3 C
9 30 C
I want to check if the total effort against a name is less than 40 then add a row with effort = 40 - (Total Effort) for the name. The ID of the new row can be anything. If the total effort is greater than 40 then trucate the data for one of the rows to make it 40.
So after applying the logic above table will be
ID Effort Name
-------------------------
1 1 A
2 1 A
3 8 A
10 30 A
4 10 B
5 4 B
6 1 B
11 25 B
7 10 C
8 3 C
9 27 C
I was thinking of opening a cursor, keeping a counter of the total effort, and based on the logic insert existing and new rows in another temporary table.
I am not sure if this is an efficient way to deal with this. I would like to learn if there is a better way.

I think the first part could be done this way:
INSERT INTO tbl(Effort, Name)
SELECT 40 - SUM(Effort), Name
FROM tbl
GROUP BY Name
HAVING SUM(Effort) < 40)
The second part is harder. Perhaps you could do something like this instead?
INSERT INTO tbl(Effort, Name)
SELECT 40 - SUM(Effort), Name
FROM tbl
GROUP BY Name
HAVING SUM(Effort) <> 40)
What this does is, rather than making changes to your actual data, adds a row with a negative number for the Name if the total effort is > 40 hours, or a positive value if it is < 40 hours. This seems much safer for your data integrity than messing with the original values.

In SQL Server 2008, this may be done with a single MERGE statement:
DECLARE #efforts TABLE (id INT NOT NULL PRIMARY KEY, effort INT NOT NULL, name CHAR(1))
INSERT
INTO #efforts
VALUES (1, 1, 'A'),
(2, 1, 'A'),
(3, 8, 'A'),
(4, 10, 'B'),
(5, 4, 'B'),
(6, 1, 'B'),
(7, 10, 'C'),
(8, 3, 'C'),
(9, 30, 'C'),
(10, 60, 'C')
SELECT *
FROM #efforts
ORDER BY
name, id
;WITH total AS
( SELECT *
FROM #efforts e
UNION ALL
SELECT ROW_NUMBER() OVER(ORDER BY name) +
(
SELECT MAX(id)
FROM #efforts
),
40 - SUM(effort),
name
FROM #efforts
GROUP BY
name
HAVING SUM(effort) < 40
),
source AS
(
SELECT *,
(
SELECT SUM(effort)
FROM total ep
WHERE ep.name = e.name
AND ep.id <= e.id
) AS ce,
COALESCE(
(
SELECT SUM(effort)
FROM total ep
WHERE ep.name = e.name
AND ep.id < e.id
), 0) AS cp
FROM total e
)
MERGE
INTO #efforts e
USING source s
ON e.id = s.id
WHEN MATCHED AND 40 BETWEEN cp AND ce THEN
UPDATE
SET e.effort = s.effort + 40 - ce
WHEN MATCHED AND cp > 40 THEN
DELETE
WHEN NOT MATCHED BY TARGET THEN
INSERT (id, effort, name)
VALUES (id, effort, name);
SELECT *
FROM #efforts
ORDER BY
name, id
In SQL Server 2005, you'll need two statements (in one transaction):
DECLARE #efforts TABLE (id INT NOT NULL PRIMARY KEY, effort INT NOT NULL, name CHAR(1))
INSERT
INTO #efforts
VALUES (1, 1, 'A')
INSERT
INTO #efforts
VALUES (2, 1, 'A')
INSERT
INTO #efforts
VALUES (3, 8, 'A')
INSERT
INTO #efforts
VALUES (4, 10, 'B')
INSERT
INTO #efforts
VALUES (5, 4, 'B')
INSERT
INTO #efforts
VALUES (6, 1, 'B')
INSERT
INTO #efforts
VALUES (7, 10, 'C')
INSERT
INTO #efforts
VALUES (8, 3, 'C')
INSERT
INTO #efforts
VALUES (9, 30, 'C')
INSERT
INTO #efforts
VALUES (10, 60, 'C')
;WITH total AS
(
SELECT *,
COALESCE(
(
SELECT SUM(effort)
FROM #efforts ep
WHERE ep.name = e.name
AND ep.id <= e.id
), 0) AS cp
FROM #efforts e
)
DELETE
FROM total
WHERE cp > 40
INSERT
INTO #efforts
SELECT (
SELECT MAX(id)
FROM #efforts
) +
ROW_NUMBER() OVER (ORDER BY name),
40 - SUM(effort),
name
FROM #efforts
GROUP BY
name
HAVING SUM(effort) < 40
SELECT *
FROM #efforts
ORDER BY
name, id

This will give you the names that need modify:
SELECT Name, SUM(Effort)
FROM Table
GROUP BY Name
HAVING SUM(Effort) < 40
Select this into a temp table, Add a column for 40 - SUM, then create an insert statement from that. Much better than a cursor.

This will do the first part:
Insert Into dbo.Test (Name, Effort)
Select t.Name, 40 - SUM(t.Effort)
From dbo.Test t
Group By t.Name
Having SUM(t.Effort) < 40
And this will do the second part:
Update a
Set a.Effort = a.Effort - b.AmountToDeduct
From dbo.Test a
Join (
Select t.Name, (40 - SUM(t.Effort)) as 'AmountToDeduct'
From dbo.Test t
Group By t.Name
Having SUM(t.Effort) > 40
)b on a.Name = b.Name
Where a.ID = (Select MAX(c.ID)
From dbo.Test c
Where c.Name = a.Name
)

Related

SQL Group percentage calculation using windowing functions

Trying to convert a row-by-row percentage calculation query to dynamic by using window function over a partition by column. Not sure this is the right way please suggest.
create table qdetails
(
qcode int,
qcode_detail_01 int,
qcode_detail_02 int
);
insert into qdetails(qcode, qcode_detail_01, qcode_detail_02)
values (25, 999, 56),
(95, 999, 67),
(96, 999, 68),
(21, 888, 56),
(22, 888, 67),
(26, 888, 68);
create table qmaster
(
qcode int,
qtype text,
qvalue int
);
insert into qmaster (qcode, qtype, qvalue)
values
(25, 'XYZ', 25),
(95, 'XYZ', 34),
(96, 'XYZ', 17),
(99, 'XYZ', 6),
(91, 'XYZ', 4),
(92, 'XYZ', 14),
(21, 'ABC', 7),
(22, 'ABC', 23),
(23, 'ABC', 11),
(24, 'ABC', 6),
(24, 'ABC', 4),
(26, 'ABC', 14);
For these table structures I have the following code; I'm trying to do this in a single query without repeating this for each row. This is on SQLite but that should not matter.
select
a.qcode_detail_01,
b.qtype,
a.qcode,
b.qvalue,
/* calculating % for one qtype at a time manually*/
sum(cast(b.qvalue as float))/(select sum(cast(b.qvalue as float)) from qmaster b where b.qtype = "XYZ") as 'Percentage'
from
qdetails a,
qmaster b
where
b.qtype = "XYZ"
and a.qcode = b.qcode
group by
a.qcode, a.qcode_detail_01;
/*repeat for ABC*/
select
a.qcode_detail_01,
b.qtype,
a.qcode,
b.qvalue,
/* calculating % for one qtype at a time manually*/
sum(cast(b.qvalue as float))/(select sum(cast(b.qvalue as float)) from qmaster b where b.qtype = "ABC") as 'Percentage'
from
qdetails a,
qmaster b
where
b.qtype = "ABC"
and a.qcode = b.qcode
group by
a.qcode, a.qcode_detail_01;
The part that is not working is this:
/*Avoid Repetition by doing this dynamically using Window Function */
select
a.qcode_detail_01,
b.qtype,
a.qcode,
b.qvalue,
cast(b.qvalue as float)/sum(cast(b.qvalue as float)) OVER (PARTITION BY b.qtype) as 'Percentage'
from qdetails a,
qmaster b
and a.qcode = b.qcode
group by a.qcode,a.qcode_detail_01;
Here is a SQLFiddle for the same; please advise.
For the sample data above, i'm looking to get the following result with a single query
qcode_detail_01 qtype qcode qvalue Percentage
999 XYZ 25 25 0.25
999 XYZ 95 34 0.34
999 XYZ 96 17 0.17
888 ABC 21 7 0.1076923076923077
888 ABC 22 23 0.35384615384615387
888 ABC 26 14 0.2153846153846154
The simplest way to do this is to join a CTE that returns the total values for each qtype:
WITH cte AS (SELECT qtype, SUM(qvalue) total_value FROM qmaster GROUP BY qtype)
SELECT d.qcode_detail_01, m.qtype, d.qcode, m.qvalue,
SUM(CAST(m.qvalue AS FLOAT)) / c.total_value Percentage
FROM qdetails d
INNER JOIN qmaster m ON d.qcode = m.qcode
INNER JOIN cte c ON c.qtype = m.qtype
GROUP BY d.qcode, d.qcode_detail_01, m.qtype
ORDER BY d.qcode_detail_01;
Another way to do it, with a LEFT join and window function SUM():
SELECT *
FROM (
SELECT DISTINCT d.qcode_detail_01, m.qtype, d.qcode, m.qvalue,
SUM(CAST(m.qvalue AS FLOAT)) /
SUM(SUM(CAST(m.qvalue AS FLOAT))) OVER (PARTITION BY m.qtype) AS Percentage
FROM qmaster m LEFT JOIN qdetails d
ON d.qcode = m.qcode
GROUP BY m.qcode, d.qcode_detail_01, m.qtype
)
WHERE qcode_detail_01 IS NOT NULL;
See the demo.
Also, use proper joins with ON clauses.

SQL select items between LAG and LEAD using as range

Is it possible to select and sum items from a table using Lag and lead from another table as range as below.
SELECT #Last = MAX(ID) from [dbo].[#Temp]
select opl.Name as [Age Categories] ,
( SELECT count([dbo].udfCalculateAge([BirthDate],GETDATE()))
FROM [dbo].[tblEmployeeDetail] ed
inner join [dbo].[tblEmployee] e
on ed.EmployeeID = e.ID
where convert(int,[dbo].udfCalculateAge(e.[BirthDate],GETDATE()))
between LAG(opl.Name) OVER (ORDER BY opl.id)
and (CASE opl.ID WHEN #Last THEN '100' ELSE opl.Name End )
) as Total
FROM [dbo].[#Temp] opl
tblEmployee contains the employees and their dates of birth
INSERT INTO #tblEmployees VALUES
(1, 'A', 'A1', 'A', '1983/01/02'),
(2, 'B', 'B1', 'BC', '1982/01/02'),
(3, 'C', 'C1', 'JR2', '1982/10/11'),
(4, 'V', 'V1', 'G', '1990/07/12'),
(5, 'VV', 'VV1', 'J', '1992/06/02'),
(6, 'R', 'A', 'D', '1982/05/15'),
(7, 'C', 'Ma', 'C', '1984/09/29')
Next table is a temp table which is created depending on the ages enter by user eg "20;30;50;60" generates a temp table below , using funtion split
select * FROM [dbo].[Split](';','20;30;50;60')
Temp Table
pn s
1 20
2 30
3 50
4 60
Desired output as below, though column Age Categories can be renamed in a data-table in C#. l need the total columns to be accurate on ranges.
Age Categories Total
up to 20 0
21 - 30 2
31 - 50 5
51 - 60 0
Something along these lines should work for you:
declare #tblEmployees table(
ID int,
FirstNames varchar(20),
Surname varchar(20),
Initial varchar(3),
BirthDate date)
INSERT INTO #tblEmployees VALUES
(1, 'A', 'A1', 'A', '1983/01/02'),
(2, 'B', 'B1', 'BC', '1982/01/02'),
(3, 'C', 'C1', 'JR2', '1982/10/11'),
(4, 'V', 'V1', 'G', '1990/07/12'),
(5, 'VV', 'VV1', 'J', '1992/06/02'),
(6, 'R', 'A', 'D', '1982/05/15'),
(7, 'C', 'Ma', 'C', '1984/09/29')
declare #temp table
(id int identity,
age int)
INSERT INTO #temp
SELECT cast(item as int) FROM dbo.fnSplit(';','20;30;50;60')
declare #today date = GetDate()
declare #minBirthCutOff date = (SELECT DATEADD(yy, -MAX(age), #today) FROM #temp)
declare #minBirth date = (SELECT Min(birthdate) from #tblEmployees)
IF #minBirth < #minBirthCutOff
BEGIN
INSERT INTO #temp VALUES (100)
end
SELECT COALESCE(CAST((LAG(t.age) OVER(ORDER BY t.age) + 1) as varchar(3))
+ ' - ','Up to ')
+ CAST(t.age AS varchar(3)) AS [Age Categories],
COUNT(e.id) AS [Total] FROM #temp t
LEFT JOIN
(SELECT te.id,
te.age,
(SELECT MIN(age) FROM #temp t WHERE t.age > te.age) AS agebucket
FROM (select id,
dbo.udfCalculateAge(birthdate,#today) age from #tblEmployees) te) e
ON e.agebucket = t.age
GROUP BY t.age ORDER BY t.age
Result set looks like this:
Age Categories Total
Up to 20 0
21 - 30 2
31 - 50 5
51 - 60 0
For future reference, particularly when asking SQL questions, you will get far faster and better response, if you provide much of the work that I have done. Ie create statements for the tables concerned and insert statements to supply the sample data. It is much easier for you to do this than for us (we have to copy and paste and then re-format etc), whereas you should be able to do the same via a few choice SELECT statements!
Note also that I handled the case when a birthdate falls outside the given range rather differently. It is a bit more efficient to do a single check once via MAX than to complicate your SELECT statement. It also makes it much more readable.
Thanks to HABO for suggestion on GetDate()

SQL Server recursive self join

I have a simple categories table as with the following columns:
Id
Name
ParentId
So, an infinite amount of Categories can be the child of a category. Take for example the following hierarchy:
I want, in a simple query that returns the category "Business Laptops" to also return a column with all it's parents, comma separator or something:
Or take the following example:
Recursive cte to the rescue....
Create and populate sample table (Please save us this step in your future questions):
DECLARE #T as table
(
id int,
name varchar(100),
parent_id int
)
INSERT INTO #T VALUES
(1, 'A', NULL),
(2, 'A.1', 1),
(3, 'A.2', 1),
(4, 'A.1.1', 2),
(5, 'B', NULL),
(6, 'B.1', 5),
(7, 'B.1.1', 6),
(8, 'B.2', 5),
(9, 'A.1.1.1', 4),
(10, 'A.1.1.2', 4)
The cte:
;WITH CTE AS
(
SELECT id, name, name as path, parent_id
FROM #T
WHERE parent_id IS NULL
UNION ALL
SELECT t.id, t.name, cast(cte.path +','+ t.name as varchar(100)), t.parent_id
FROM #T t
INNER JOIN CTE ON t.parent_id = CTE.id
)
The query:
SELECT id, name, path
FROM CTE
Results:
id name path
1 A A
5 B B
6 B.1 B,B.1
8 B.2 B,B.2
7 B.1.1 B,B.1,B.1.1
2 A.1 A,A.1
3 A.2 A,A.2
4 A.1.1 A,A.1,A.1.1
9 A.1.1.1 A,A.1,A.1.1,A.1.1.1
10 A.1.1.2 A,A.1,A.1.1,A.1.1.2
See online demo on rextester

SQL Server function to get top level parent in hierarchy

I have following table (master_group) structure :
code name under
1 National Sales Manager 1
2 regional sales manager 1
3 area sales manager 2
4 sales manager 3
How do I get the ultimate parent of a particular row like :
code name under ultimateparent
1 National Sales Manager 1 1
2 regional sales manager 1 1
3 area sales manager 2 1
4 sales manager 3 1
With recursive cte going from top to childs:
with cte as(
select *, code as ultimate from t where code = under
union all
select t.*, c.ultimate from t
join cte c on c.code = t.under
where t.code <> t.under
)
select * from cte
For data:
create table t (code int, name varchar(100), under int)
insert into t values
(1, 'National Sales Manager', 1),
(2, 'regional sales manager', 1),
(3, 'area sales manager', 2),
(4, 'sales manager', 3),
(5, 'a', 5),
(6, 'b', 5),
(7, 'c', 5),
(8, 'd', 7),
(9, 'e', 7),
(10, 'f', 9),
(11, 'g', 9)
it generates the output:
code name under ultimate
1 National Sales Manager 1 1
5 a 5 5
6 b 5 5
7 c 5 5
8 d 7 5
9 e 7 5
10 f 9 5
11 g 9 5
2 regional sales manager 1 1
3 area sales manager 2 1
4 sales manager 3 1
Fiddle http://sqlfiddle.com/#!6/17c12e/1
You can use a recursive CTE to walk the tree and then choose the highest level for each code:
with cte as (
select mg.code, mg.name as name, mg.under as under, mg.under as parent, 1 as lev
from master_group mg
union all
select mg.code, mg.name, mg.under, cte.under as parent, cte.lev + 1
from master_group mg join
cte
on mg.under = cte.code
where cte.under is not null and cte.under <> mg.code
)
select code, name, under, parent as ultimateparent
from (select cte.*, max(lev) over (partition by cte.code) as maxlev
from cte
) t
where lev = maxlev;
Here is a SQL Fiddle.
I would put NULL as under (in my example ParentId) when it's the top record. With this assumption here's a solution
;
WITH Result AS
(
SELECT Id, ParentId, Name, Id as [Top] FROM
sample
where ParentId IS NULL
UNION ALL
SELECT s.Id, s.ParentId, s.Name, [Top]
FROM sample s INNER JOIN Result R ON s.ParentId = R.Id
)
http://sqlfiddle.com/#!6/13b9d/14
I suggest you to use a recursive function like this:
CREATE FUNCTION dbo.parentID (#code int)
RETURNS int AS
BEGIN
DECLARE #ResultVar int
SELECT #ResultVar = (SELECT under FROM master_group WHERE code = #code)
IF #ResultVar <> #code
BEGIN
SELECT #ResultVar = dbo.parentID(#ResultVar)
END
RETURN #ResultVar
END
GO
An use it like this:
SELECT *,
dbo.parentId(code) AS ultimateparent
FROM master_group
I'm going to shamelessly steal the data setup from another answer and demonstrate how you'd do this with hierarchyid:
create table t (code int, name varchar(100), under int)
insert into t values
(1, 'National Sales Manager', null),
(2, 'regional sales manager', 1),
(3, 'area sales manager', 2),
(4, 'sales manager', 3),
(5, 'a', null),
(6, 'b', 5),
(7, 'c', 5),
(8, 'd', 7),
(9, 'e', 7),
(10, 'f', 9),
(11, 'g', 9);
with cte as (
select code, name, under as parentCode, code as ultimateParent, cast('/' + cast(code as varchar) + '/' as nvarchar(max)) as h
from t
where under is null
union all
select child.code, child.name, child.under as ParentCode, parent.ultimateParentCode, cast(parent.h + cast(child.code as varchar) + '/' as nvarchar(max))
from t as child
join cte as parent
on child.under = parent.code
), hier as (
select code, name, parentCode, ultimateParentCode, cast(h as hierarchyid) as h
from cte
)
select code, name, parentCode, ultimateParentCode, h.ToString(), h.GetAncestor(h.GetLevel()-1).ToString()
from hier
Keep in mind, the recursive CTE need only be done once (or on data changes). The point that I'm making is that once you have a hierarchyid calculated (which you can store in row, btw), it's easy to answer the question you're posing with method calls on the hierarchyid (and possibly a join if you want to get back the progenitor's info).

Running Total with minimum balance after modulus

Currently the code below does running total with 50 as its limit.
Now i would like to have 50 as the minimum value for the remaining. How could i possibly do this. Any hints will be really helpful. (min balance 50). This code is in sql 2012, need it to work sql2008 and above
Example :
- 10 10
- 20 30
- 30 60
- 40 100 --> 50 (min value of 50) 100-50 = 50
- 2 52
- 3 55
- 10 65
- 25 90
- 15 105 --> 55 (min value 50, 105-50 = 55)
- 5 60
DECLARE #Table TABLE (Id INT, ListItem INT);
INSERT INTO #Table VALUES (1, 10);
INSERT INTO #Table VALUES (2, 20);
INSERT INTO #Table VALUES (3, 30);
INSERT INTO #Table VALUES (4, 40);
INSERT INTO #Table VALUES (5, 2);
INSERT INTO #Table VALUES (6, 3);
INSERT INTO #Table VALUES (7, 10);
INSERT INTO #Table VALUES (8, 25);
INSERT INTO #Table VALUES (9, 15);
INSERT INTO #Table VALUES (10, 5);
WITH RunningTotal AS (
SELECT Id, ListItem, SUM(ListItem) OVER (ORDER BY Id) % 50 AS RT FROM #Table)
SELECT
rt.Id,
rt.ListItem,
CASE WHEN rt.RT < rt2.RT THEN rt.RT + 50 ELSE rt.RT END AS RunningTotal
FROM
RunningTotal rt
LEFT JOIN RunningTotal rt2 ON rt2.Id = rt.Id - 1
ORDER BY
rt.Id;
Edit :
Now i get the idea. But when i try to change the values, it only work once. Like say once the cumulative value exceeds 350000, deduct by 300000, meaning minimum balance is 50000.
Any hints ?
Link : http://sqlfiddle.com/#!6/c9051/1/0
Thank you.
The key is that when you only want to go modulo 50 when your number is above 100. This would then get you the "remainder" above 50, which you add 50 to to get your desired output
WITH RunningTotal AS (
SELECT Id, ListItem, SUM(ListItem) OVER (ORDER BY Id) AS RT FROM #Table)
SELECT Id, ListItem, RT, CASE WHEN RT > 100 THEN RT % 50 + 50 ELSE RT END AS NewRT
FROM RunningTotal