group by on a range - sql

i have a table with employee names and their vendor experiences.
i have to create a table with the following data
data given to me is like
empname vendor experience
a 1
b 2
c 10
d 11
e 20
f 12
g 21
h 22
i want to generate a SQL query to display data like this
vendor_experience(months) count
0-6 2
0-12 5
0-18 5
more 8
please help me with the query.

You might employ case statement to get counts of exclusive ranges:
select case when [vendor experience] <= 6 then '0-6'
when [vendor experience] <= 12 then '0-12'
when [vendor experience] <= 18 then '0-18'
else 'more'
end [vendor_experience(months)],
count (*) [count]
from experiences
group by
case when [vendor experience] <= 6 then '0-6'
when [vendor experience] <= 12 then '0-12'
when [vendor experience] <= 18 then '0-18'
else 'more'
end
This produces the same result as yours (inclusive ranges):
; with ranges as
(
select 6 as val, 0 as count_all
union all
select 12, 0
union all
select 18, 0
union all
select 0, 1
)
select case when ranges.count_all = 1
then 'more'
else '0-' + convert (varchar(10), ranges.val)
end [vendor_experience(months)],
sum (case when ranges.count_all = 1
or experiences.[vendor experience] <= ranges.val
then 1 end) [count]
from experiences
cross join ranges
group by ranges.val, ranges.count_all
count_all is set to 1 to mark open-ending range.
Sql Fiddle is here.
UPDATE: an attempt at explanation.
The first part starting with with and ending with closing bracket is called CTE. Sometimes it is referred to as inline view because it can be used multiple times in the same query and under some circumstances is updateable. Here it is used to prepare data for ranges and is appropriately named ranges. This name one uses in main query. Val is maximum value of a range, count_all is 1 if range has no upper end (18+, more, or however you wish to call it). Data rows are combined by means of union all. You might copy/paste section between parenthesis only and run it just to see the results.
Main body joins experiences table with ranges using cross join. This creates combinations of all rows from experiences and ranges. For row d 11 there will be 4 rows,
empname vendor experience val count_all
d 11 6 0
d 11 12 0
d 11 18 0
d 11 0 1
First case statement in select list produces caption by checking count_all - if it is one, outputs more, else constructs caption using upper range value. Second case statement counts using sum(1). As aggregate functions ignore nulls, and case having no else evaluates to null if match was not found, it is sufficient to check if count_all is true (meaning that this row from experiences is counted in this range) or if vendor experience is less or equal to upper range value of current range. In example above 11 will not be counted for first range but will be counted for all the rest.
Results are then grouped by val and count_all. To better see how it works you might remove group by and sum() and look at numbers before aggregation. Order by empname, val will help to see how values of [count] change depending on different val per an employee.
Note: I did my best with my current level of english language. Please don't hesitate to ask for clarification if you need one (or two, or as many as you need).

A bit more dynamic, implement a table for the groupings:
create table #t (name varchar(10),e int)
insert into #t values ('a',0)
insert into #t values ('b',4)
insert into #t values ('c',3)
insert into #t values ('d',13)
insert into #t values ('e',25)
insert into #t values ('f',4)
insert into #t values ('g',19)
insert into #t values ('h',15)
insert into #t values ('i',7)
create table #g (t int, n varchar(10))
insert into #g values (6, '0-6')
insert into #g values (12, '0-12')
insert into #g values (18, '0-18')
insert into #g values (99999, 'more')
select #g.n
,COUNT(*)
from #g
inner join #t on #t.e <= #g.t
group by #g.n
you might want to play around with the value 99999 for example.

Here is a way to get the cumulative values:
select sum(mon0_6) as mon0_6, sum(mon0_12) as mon0_12, sum(mon0_18) as mon0_18,
sum(more) as more
from (select e.*,
(case when [vendor experience] <= 6 then 1 else 0 end) as mon0_6,
(case when [vendor experience] <= 12 then 1 else 0 end) as mon0_12,
(case when [vendor experience] <= 18 then 1 else 0 end) as mon0_18,
1 as more
) e
This puts them in separate columns. You can then use unpivot to put them in separate rows.
However, you might consider doing the cumulative sum at the application layer. I often do this sort of thing in Excel.
Doing a cumulative sum in SQL Server 2008 requires a self-join, either explicitly or via a correlated subquery. SQL Server 2012 supports much simpler syntax for cumulative sums (the over clause takes an order by argument).

Try this:
INSERT INTO ResultTable ([vendor_experience(months)], count)
Select *FROM
(
(SELECT '0-6', Count(*) From TableA WHERE [vendor experience] <= 6
UNION ALL
SELECT '0-12', Count(*) From TableA WHERE [vendor experience] <= 12
UNION ALL
SELECT '0-18', Count(*) From TableA WHERE [vendor experience] <= 18
UNION ALL
SELECT 'more', Count(*) From TableA) as Temp
)
If duplicate counts not needed, then try this:
select t.[vendor_experience(months)], count(*) as count
from (
select case
when [vendor experience] between 0 and 6 then ' 0-6'
when [vendor experience] between 7 and 12 then '0-12'
when [vendor experience] between 13 and 18 then '0-18'
when [vendor experience] >= 19 then 'more'
else 'other' end as [vendor_experience(months)]
from TableA) t
group by t.[vendor_experience(months)]

Related

Trying to find unique records in a table that don't have a negating record

I have a table with a whole bunch of records.
table looks like this (simplified):
ID DoID DoQty DoType DoValue
1 17 1 Door 15
2 17 -1 Door -15
3 18 1 Window 75
4 19 1 Bed 125
5 19 1 Bed 134
so this is what I'd like to pull
ID DoId DoQty DoType DoValue
3 18 1 WIndows 75
4 19 1 Bed 125
5 19 1 Bed 134
I don't need DoID=17 because it has a 2nd line where DoQty is -1. SO that overall DoQty = 0. I only need records where there isn't a DoQty=-1. The problem here is that I do not want to group by DoID I want to be able to see the whole record line (no group by)
EDIT:
Unfortunately I might not have explained my question correctly. Basically, if I run the following query, i get the correct counts, however my goal is to get the details of each line.
SELECT t.DoID,
'Available' = Sum(t.DoQty)
From t
GROUP BY t.DoID
This gives me grouped results from which I can't do anything with.
As i understand you dont what to record where has any negative DoQty in any row. If it is correct, a possible solution is below,
SELECT t1.ID, t1.DoID, t1.DoQty, t1.DoType, t1.DoValue
FROM table t1
LEFT JOIN table t2 ON t2.DoQty < 0 AND t1.DoID = t2.DoID
WHERE t2.DoID IS NULL
We can use conditional aggregation here:
WITH cte AS (
SELECT DoID
FROM yourTable
GROUP BY DoID
HAVING SUM(CASE WHEN DoQty < 0 THEN 1 ELSE 0 END) = 0
)
SELECT *
FROM yourTable
WHERE DoID IN (SELECT DoID FROM cte);
This would return every DoID whose group of records does not have any DoQty values which are negative.
Is this what you want?
select t.*
from t
where not exists (select 1 from t t2 where t2.doid = t.doid and t2.doqty = - t.doqty);
This filters out rows where the "negative" value exists.
What I'm trying to do here is only pull up DoID's that don't have have a DoQty>0
Try this...
SELECT DoID
FROM table tbl
WHERE NOT EXISTS (
SELECT DoID FROM table WHERE DoQty <= 0 AND DoID = tbl.DoID
);
You can try the below:
With Agg AS
(
select DOID,sum(DoQty) as Qty from TableName
group by DOID having sum(DoQty)>0
)
select T.* from TableName T Inner Join Agg A
on T.DOID=A.DOID;
You seems want :
SELECT *, SUM(t.DoQty) OVER (PARTITION BY t.DoID) as Available
From t;

Check whether an employee is present on three consecutive days

I have a table called tbl_A with the following schema:
After insert, I have the following data in tbl_A:
Now the question is how to write a query for the following scenario:
Put (1) in front of any employee who was present three days consecutively
Put (0) in front of employee who was not present three days consecutively
The output screen shoot:
I think we should use case statement, but I am not able to check three consecutive days from date. I hope I am helped in this
Thank you
select name, case when max(cons_days) >= 3 then 1 else 0 end as presence
from (
select name, count(*) as cons_days
from tbl_A, (values (0),(1),(2)) as a(dd)
group by name, adate + dd
)x
group by name
With a self-join on name and available = 'Y', we create an inner table with different combinations of dates for a given name and take a count of those entries in which the dates of the two instances of the table are less than 2 units apart i.e. for each value of a date adate, it will check for entries with its own value adate as well as adate + 1 and adate + 2. If all 3 entries are present, the count will be 3 and you will have a flag with value 1 for such names(this is done in the outer query). Try the below query:
SELECT Z.NAME,
CASE WHEN Z.CONSEQ_AVAIL >= 3 THEN 1 ELSE 0 END AS YOUR_FLAG
FROM
(
SELECT A.NAME,
SUM(CASE WHEN B.ADATE >= A.ADATE AND B.ADATE <= A.ADATE + 2 THEN 1 ELSE 0 END) AS CONSEQ_AVAIL
FROM
TABL_A A INNER JOIN TABL_A B
ON A.NAME = B.NAME AND A.AVAILABLE = 'Y' AND B.AVAILABLE = 'Y'
GROUP BY A.NAME
) Z;
Due to the complexity of the problem, I have not been able to test it out. If something is really wrong, please let me know and I will be happy to take down my answer.
--Below is My Approch
select Name,
Case WHen Max_Count>=3 Then 1 else 0 end as Presence
from
(
Select Name,MAx(Coun) as Max_Count
from
(
select Name, (count(*) over (partition by Name,Ref_Date)) as Coun from
(
select Name,adate + row_number() over (partition by Name order by Adate desc) as Ref_Date
from temp
where available='Y'
)
) group by Name
);
select name as employee , case when sum(diff) > =3 then 1 else 0 end as presence
from
(select id, name, Available,Adate, lead(Adate,1) over(order by name) as lead,
case when datediff(day, Adate,lead(Adate,1) over(order by name)) = 1 then 1 else 0 end as diff
from table_A
where Available = 'Y') A
group by name;

Not a GROUP BY Expression & aggregate functions

I was wondering why, for this query that I have right here, why I have to use the MAX() aggregate function for the case statements, and not just jump directly into the case statement:
select
bank_id,
tran_branch_code,
acct_sol_id,
acct_sol_name,
transaction_date,
gl_date,
transaction_id,
account_number,
max(case
when cast(substr(GLSH_Code,0,1) as int) >= 1
and cast(substr(GLSH_Code,0,1) as int) <= 5
and trans_type = 'D'
then (trans_amount)
--else 0
end ) Ind_Part_Tran_Dr_RBU,
max(case
when cast(substr(GLSH_Code,0,1) as int) >= 1
and cast(substr(GLSH_Code,0,1) as int) <= 5
and trans_type = 'C'
then (trans_amount)
--else 0
end) Ind_Part_Tran_Cr_RBU,
max(case
when cast(substr(GLSH_Code,0,1) as int) = 0
or (cast(substr(GLSH_Code,0,1) as int) >= 6
and cast(substr(GLSH_Code,0,1) as int) <= 9)
and trans_type = 'D'
then (trans_amount)
--else 0
end)Ind_Part_Tran_Dr_FCDU,
max(case
when cast(substr(GLSH_Code,0,1) as int) = 0
or (cast(substr(GLSH_Code,0,1) as int) >= 6
and cast(substr(GLSH_Code,0,1) as int) <= 9)
and trans_type = 'C'
then (trans_amount)
--else 0
end) Ind_Part_Tran_Cr_FCDU,
ccy_alias,
ccy_name,
acct_currency,
tran_currency
from
(
SELECT
DTD.BANK_ID,
DTD.SOL_ID Acct_Sol_ID, --Account Sol ID
dtd.br_code Tran_branch_code, -- branch code of the transacting branch
sol.sol_desc Acct_sol_name, -- name/description of SOL
DTD.TRAN_DATE Transaction_Date, --TransactionDate
DTD.GL_DATE GL_Date, --GL Date
TRIM(DTD.TRAN_ID) Transaction_ID, --Transaction ID
DTD.GL_SUB_HEAD_CODE GLSH_Code, --GLSH Code
dtd.tran_amt trans_amount,
GAM.ACCT_CRNCY_CODE Acct_Currency, --Account Currency
DTD.TRAN_CRNCY_CODE Tran_Currency, --Transaction Currency
cnc.crncy_alias_num ccy_alias,
cnc.crncy_name ccy_name,
GAM.FORACID Account_Number, --Account Number
DTD.TRAN_PARTICULAR Transaction_Particulars, --Transaction Particulars
DTD.CRNCY_CODE DTD_CCY,
--GSH.CRNCY_CODE GSH_CCY,
DTD.PART_TRAN_TYPE Transaction_Code,
--'Closing_Balance',
DTD.PSTD_USER_ID PostedBy,
CASE WHEN DTD.REVERSAL_DATE IS NOT NULL
THEN 'Y' ELSE 'N' END Reversal,
TRIM(DTD.TRAN_ID) REV_ORIG_TRAN_ID,
--OTT.REF_NUM OAP_REF_NUM,
'OAP_SETTLEMENT',
'RATE_CODE',
EAB.EOD_DATE
FROM TBAADM.DTD
LEFT OUTER JOIN TBAADM.GAM ON DTD.ACID = GAM.ACID AND DTD.BANK_ID = GAM.BANK_ID
LEFT OUTER JOIN TBAADM.EAB ON DTD.ACID = EAB.ACID AND DTD.BANK_ID = EAB.BANK_ID AND EAB.EOD_DATE = '24-MAR-2014'
left outer join tbaadm.sol on dtd.sol_id = sol.sol_id and dtd.bank_id = sol.bank_id
left outer join tbaadm.cnc on dtd.tran_crncy_code = cnc.crncy_code
WHERE DTD.BANK_ID = 'CBC01'
AND GAM.ACCT_OWNERSHIP = 'O'
AND GAM.DEL_FLG != 'Y'
--AND DTD.TRAN_DATE = '14-APR-2014'
AND DTD.TRAN_DATE between '01-APR-2014' and '21-APR-2014'
--and foracid in ('50010112441109','50010161635051')
--and DTD.SOL_ID = '5001'
and GAM.ACCT_CRNCY_CODE = 'USD'
)
group by
bank_id,
tran_branch_code,
acct_sol_id,
acct_sol_name,
transaction_date,
gl_date,
transaction_id,
account_number,
ccy_alias,
ccy_name,
Acct_Currency,
Tran_Currency
Because If I would remove the MAX(), I'd get the "Not a GROUP BY Expression", and Toad points me to the first occurrence of the GLSH_Code. Based from other websites, the cure for this is really adding the MAX() function. I would just like to understand why should I use that particular function, what it exactly does in the query, stuff like that.
EDIT: inserted the rest of the code.
I know for sure what MAX() does, it returns the largest value in an expression. But in this case, I can't seem to figure out exactly what that largest value is that the function is attempting to return.
The GROUP BY statement declares that all columns returned in the SELECT should be aggregated, but that you want to separate the results by those listed in the GROUP BY.
This means we have to use aggregate functions like MIN, MAX, AVG, SUM, etc. on any column that is NOT listed in the GROUP BY.
It's about telling the SQL engine what the expected results should be when there is more than one option.
In a simple example, we have a table with three columns:
PrimaryId SubId RowValue
1 1 1
2 1 2
3 2 4
4 2 8
And an SQL like the following (which is invalid):
SELECT SubId, RowValue
FROM SampleTable
GROUP BY SubId
We know we want the distinct SubId's (because of the GROUP BY), but we don't know what RowValue should be when we aggregate the results.
SubId RowValue
1 ?
2 ?
We have to be explicit in our query, and indicate what RowValue should be as the results can vary.
If we choose MIN(RowValue) we see:
SubId RowValue
1 1
2 4
If we choose MAX(RowValue) we see:
SubId RowValue
1 2
2 8
If we choose SUM(RowValue) we see:
SubId RowValue
1 3
2 12
Without being explicit there's a high likelihood that the results will be wrong, so our SQL engine of choice protects us from ourselves by enforcing the need for aggregate functions.
You have group by clause at the end on all the columns except for Ind_Part_Tran_Dr_RBU, Ind_Part_Tran_Cr_RBU, Ind_Part_Tran_Dr_FCDU, Ind_Part_Tran_Cr_FCDU. In this case oracle wants you to tell what to do with these columns, i.e. based on which function it has to aggregate them for every group it finds.

Subtract value to multiple rows

Well I am stuck at a point where I need to distribute a value across multiple rows. Since I do not know the specific term, I would put it in the form of example below for better understanding:
Assuming the value of x to be 20, I need to distribute/subtract it to rows in descending order.
TABLE:
ID Value1
1 6
2 5
3 4
4 3
5 9
Result should look like: (x=20)
ID Value1 Answer
1 6 14
2 5 9
3 4 5
4 3 2
5 9 0
Can anyone just give me an idea how I could go with this?
Untested for syntax, but the idea should work in SQL Server 2005 and newer.
SQL Server 2012 has SUM OVER clause which makes this even handier.
SELECT ID, Value1, CASE WHEN 20-SumA < 0 THEN 0 ELSE 20-SumA END AS Answer
FROM TABLE A
CROSS APPLY (SELECT SUM(B.Answer) SumA FROM TABLE B
WHERE B.ID <= A.ID) CA
It is perhaps easier to think of this problem in a different way. You want to calculate the cumulative sum of value1 and then subtract that value from #X. If the difference is negative, then put in 0.
If you are using SQL Server 2012, then you have cumulative sum built-in. You can do this as:
select id, value1,
(case when #X - cumvalue1 < 0 then 0 else #X - cumvalue1 end) as answer
from (select id, value1,
sum(value1) over (order by id) as cumvalue1
from table t
) t;
If you don't have cumulative sum, you can do this with a subquery instead:
select id, value1,
(case when #X - cumvalue1 < 0 then 0 else #X - cumvalue1 end) as answer
from (select id, value1,
(select sum(value1)
from table t2
where t2.id <= t.id
) as cumvalue1
from table t
) t;
I don't understand your question. I know what I think you're trying to do. But your example doesn't make sense.
You say you want to distribute 20 over the 5 rows, yet the sum of the difference between Value1 and Answer is only 3 (8+4+1+-1+-9).
And how do you want to distribute the values? Using a spread/split based on the value in Value1?
Edit: I made an example which splits 20 over the values you've specified above:
DECLARE #x FLOAT = 20.0
DECLARE #values TABLE (
ID INT,
VALUE FLOAT,
NEWVAL FLOAT)
INSERT INTO #values (ID, VALUE) VALUES (1,6), (2,5),(3,4),(4,3),(5,9)
UPDATE f
SET [NEWVAL] = [newValue]
FROM #values f
INNER JOIN (
SELECT
ID,
value + ((VALUE / [maxValue]) * #x) [newValue]
FROM
#values
CROSS APPLY (
SELECT
SUM(value) [maxValue]
FROM
#values
) m
) a ON a.ID = f.ID
SELECT * FROM #values
Unfortunately I had to change your values to floats for this to work. If you require them as integers, you'll need to use rounding and then calculate the difference of the sum of new value - #x and then spread the difference over the rows (if > 1 then add to lowest number, if < 1 subtract from largest value). Your rounding should be usually just 1 or 2.
I don't even know if I this is what you're trying to do yet.

Select Distinct Attribute and Print out Count of another even when the count is 0

I don't quite know how I should describe the problem for title, but here's my question.
I have a table named hello with two columns named time and state.
Time | State
Here's an example of the data I have
1 DC
1 VA
1 VA
2 DC
2 MD
3 MD
3 MD
3 VA
3 DC
I would like to get all the possible time and the count of "VA" (0 if "VA" doesn't appear at the time)
The output would look like this
Time Number
1 2
2 0
3 1
I tried to do
SELECT DISTINCT time,
COUNT(state) as Number
FROM hello
WHERE state = 'VA'
GROUP BY time
but it doesn't seem to work.
This is a conditional aggregation:
select time, sum(case when state = 'VA' then 1 else 0 end) as NumVA
from hello
group by time
I want to add that you should never use distinct when you have a group by. The two are redundant. Distinct as a keyword is not even needed in the SQL language; semantically, it is just shorthand for grouping by all the columns.
SELECT TIME,
SUM(CASE WHEN State = 'VA' THEN 1 ELSE 0 END)
FROm tableName
GROUP BY Time
SQLFiddle Demo
One rule of thumb is to get your counts first and put them into a temp for use later.
See below:
Create table temp(Num int, [state] varchar(2))
Insert into temp(Num,[state])
Select 1,'DC'
UNION ALL
Select 1,'VA'
UNION ALL
Select 1,'VA'
UNION ALL
Select 2,'DC'
UNION ALL
Select 2,'MD'
UNION ALL
Select 3,'MD'
UNION All
Select 3,'MD'
UNION ALL
Select 3,'VA'
UNION ALL
Select 3,'DC'
Select t.Num [Time],t.[State]
, CASE WHEN t.[state] = 'VA' THEN Count(t.[State]) ELSE 0 END [Number]
INTO #temp2
From temp t
Group by t.Num, t.[state]
--drop table #temp2
Select
t2.[time]
,SUM(t2.[Number])
From #temp2 t2
group by t2.[time]