SQL - finding the value between rows - sql

I have 2 tables:
Employee table with 2 columns Name and Sales
Rewards table with 2 columns Bonus and Range
Sample data:
Employee Rewards
| Name | Sales | | Bonus | Range |
+------+-------+ +-------+-------+
| John | 112 | | 2 | 200 |
| Mary | 201 | | 3 | 300 |
| Joe | 400 | | 5 | 500 |
| Jack | 300 |
Each employee deserves bonus from the Rewords table if his sales <= Rewards.Range.
I want to select Employee.Name and Rewards.Bonus.
In this case the result should be:
| Name | Bonus |
+------+-------+
| John | 2 |
| Mary | 3 |
| Joe | 5 |
| Jack | 3 |
Any idea what this SQL query will be?
Thanks,
zb

I suggest this approach. There might be a syntax error or two.
select name
, (select bonus from rewards
where range =
(select min(range)
from rewards
where range >= sales)
)
from employee

I just tested this one out and it retrieved the right results with your test tables:
SELECT a.name,
MIN(b.bonus) AS bonus
FROM db.employee a
INNER JOIN db.rewards b
ON a.sales <= b.range
GROUP BY a.name;

I'd use lag to get the bottom part part of each range and join it on the employee's table:
SELECT name, bonus
FROM employee e
JOIN (SELECT bonus,
range AS top_range,
COALESCE(LAG(range) OVER (ORDER BY bonus ASC), 0) AS bottom_range
FROM rewards) r ON e.sales BETWEEN r.bottom_range AND r.top_range

;with OrderedBonuses as (
select e.name, r.bonus, row_number() over (partition by e.name order by r.Range desc) as ord
from Employee e
JOIN Rewards r on e.Sales <= r.Range
)
select name, bonus
from OrderedBonuses
where ord = 1;

Related

How to handle duplicates created by LEFT JOIN

LEFT TABLE:
+------+---------+--------+
| Name | Surname | Salary |
+------+---------+--------+
| Foo | Bar | 100 |
| Foo | Kar | 300 |
| Fo | Ba | 35 |
+------+---------+--------+
RIGHT TABLE:
+------+-------+
| Name | Bonus |
+------+-------+
| Foo | 10 |
| Foo | 20 |
| Foo | 50 |
| Fo | 10 |
| Fo | 100 |
| F | 1000 |
+------+-------+
DESIRED OUTPUT:
+------+---------+--------+-------+
| Name | Surname | Salary | Bonus |
+------+---------+--------+-------+
| Foo | Bar | 100 | 80 |
| Foo | Kar | 300 | 0 |
| Fo | Ba | 35 | 110 |
+------+---------+--------+-------+
The closest I get is this:
SELECT
a.Name,
Surname,
sum(Salary),
sum(Bonus)
FROM (SELECT
Name,
Surname,
sum(Salary) as Salary
FROM input
GROUP BY 1,2) a LEFT JOIN (SELECT Name,
SUM(Bonus) as Bonus
FROM input2
GROUP BY 1) b
ON a.Name = b.Name
GROUP BY 1,2;
Which gives:
+------+---------+-------------+------------+
| Name | Surname | sum(Salary) | sum(Bonus) |
+------+---------+-------------+------------+
| Fo | Ba | 35 | 110 |
| Foo | Bar | 100 | 80 |
| Foo | Kar | 300 | 80 |
+------+---------+-------------+------------+
I can't figure out how to get rid of Bonus duplication. Ideal solution for me would be as specified in the 'DESIRED OUTPUT', which is adding Bonus to only one Name and for other records with the same Name adding 0.
You can use row_number():
select l.*, (case when l.seqnum = 1 then r.bonus else 0 end) as bonus
from (select l.*, row_number() over (partition by name order by salary) as seqnum
from "left" l
) l left join
(select r.name, sum(bonus) as bonus
from "right" r
group by r.name
) r
on r.name = l.name
Try a Row_number over the Name category partioned by Name. This will give you different numbers for your duplicates. You can then search for the case when this number is 1 and return the result you want. Else return 0. The code can look something like this.
SELECT
a.Name,
Surname,
sum(Salary),
Case when Duplicate_Order = 1
then bonus
else 0
end as 'Bonus'
FROM (SELECT
Name,
Surname,
sum(Salary) as Salary
,ROW_NUMBER() over (partition by Name order by name) as [Duplicate_Order]
FROM input
GROUP BY 1,2) a
LEFT JOIN (SELECT Name,
SUM(Bonus) as Bonus
FROM input2
GROUP BY 1) b
ON a.Name = b.Name
GROUP BY 1,2;
Hope that helps!
You can use Correlated Subquery with sum() aggregation to compute the bonus column, and then apply lag() window analytic function to get the zeros for successively identical valued column values for the name column :
select Name, Surname, Salary,
bonus - lag(bonus::int,1,0) over (partition by name order by salary) as bonus
from
(
select i1.*,
( select sum(Bonus)
from input2 i2
where i1.Name = i2.Name
group by i2.Name ) as bonus
from input i1
) ii
order by name desc, surname;
Demo

Oracle SQL [1..*] Relation | Select all rows from [1...] + top-row from [...*] after order by in [...*]

I want to select * from Table TRAINEE + the first REPORT.DATE within the last 2 months.
TRAINEE
+----+----------+
| ID | NAME |
+----+----------+
| 1 | John Doe |
+----+----------+
| 2 | Jane Doe |
+----+----------+
REPORT
+------------+------------+---------------+
| TRAINEE_ID | DATE | REPORT |
+------------+------------+---------------+
| 1 | 01.07.2018 | Not Important |
+------------+------------+---------------+
| 1 | 02.07.2018 | Not Important |
+------------+------------+---------------+
| 1 | 03.07.2018 | Not Important |
+------------+------------+---------------+
| 2 | 02.07.2018 | Not Important |
+------------+------------+---------------+
| 2 | 02.07.2018 | Not Important |
+------------+------------+---------------+
| 2 | 03.07.2018 | Not Important |
+------------+------------+---------------+
Wanted results
+----+----------+--------------+
| ID | NAME | FIRST_REPORT |
+----+----------+--------------+
| 1 | John Doe | 01.07.2018 |
+----+----------+--------------+
| 2 | Jane Doe | 02.07.2018 |
+----+----------+--------------+
I have tried...
This way gave me only one row with a date, rest filled with NULL, because the join only returns one row. Removing the ROWNUM filter will make my query return one row of TRAINEE for each row found in REPORT. So this wouldn't work as well. Where do I have to put the ROWNUM filter?
SELECT
TRAINEE.*,
OUTER_ORDER_DATE.DATE
FROM
Trainee
LEFT JOIN
(
SELECT
DATE,
ID,
ROWNUM as rnum
FROM
(
SELECT
DATE,
ID,
FROM
REPORT INNER_ORDER_DATE
WHERE
INNER_ORDER_DATE.DATE >= add_months(sysdate,-2)
ORDER BY
INNER_ORDER_DATE.DATE ASC
)
WHERE
rnum < 2
) ON OUTER_ORDER_DATE ON OUTER_ORDER_DATE.ID = a.ID
I then tried the following query, that has some wrong syntax; the inner query cannot access the outer query's Trainee.ID.
SELECT
Trainee.*,
(SELECT
DATE
FROM (
SELECT
DATE
FROM
REPORT
WHERE
ID = TRAINEE.ID
AND
DATE >= add_months(sysdate,-2)
ORDER BY
DATE ASC
)
WHERE
ROWNUM < 2
) as DATE
FROM
TRAINEE
What does my query has to look like, to get the wanted result above?
If this question was answered already please link it for me. I have no idea how I could search for this scenario. Thank you.
You should try using this code:
SELECT trainee.id, trainee.name, report.report_date
FROM trainee
JOIN ( SELECT trainee_id, MIN (report_date) AS report_date
FROM report
WHERE report_date >= ADD_MONTHS (SYSDATE, -2)
GROUP BY trainee_id) report
ON (report.trainee_id = trainee.id)
Try this:
select t.id, t.name, min(r.date) firstdate
from trainee t, report r
where t.id = r.id
and r.date >= add_months(sysdate,-2)

how to select unique records from a table based on a column which has distinct values in another column

I have below table SUBJ_SKILLS which has records like
TCHR_ID | LINE_NBR | SUBJ | SUBJ_TYPE
--------| ------- | ---------- | ----------
1 | 1 | Maths | R
1 | 2 | 101 | U
2 | 1 | BehaviourialTech | U
3 | 2 | Maths | R
4 | 1 | RegionalLANG | U
5 | 3 | ForeignLANG | U
5 | 4 | Maths | R
6 | 2 | Science | R
7 | 1 | 101 | U
7 | 3 | Physics | R
..
..
I am trying to retrieve records like below (i.e. single teacher who taught multiple different subjects)
TCHR_ID | LINE_NBR | SUBJ | SUBJ_TYPE
--------| ------- | ---------- | ----------
5 | 3 | ForeignLANG | U
5 | 4 | Maths | R
7 | 1 | 101 | U
7 | 3 | Physics | R
1 | 1 | Maths | R
1 | 2 | 101 | U
Here, the line numbers are unique, means that TCHR_ID:5 taught Physics (which was LINE_NBR=1, but was removed later). So, the LINE_NBR are not updated and stay as is.
i also have a look up table (SUBJ_LKUP) for subject and their categories/type like below ('R' for Regular subject and 'U' for Unique subject )
SUBJ | SUBJ_TYPE
----------------- | ------------
Maths | R
Physics | R
ForeignLANG | U
101 | U
Science | R
BehaviourialTech | U
RegionalLANG | U
My approach to resolve this was to create a table which have 2 records for Teacher and use another query on base table (SUBJ_SKILLS) and new table to filter out distinct records. I came up with below queries..
Query-1:
create table tchr_with_2_subj as select SS.TCHR_ID
from SUBJ_SKILLS SS, SUBJ_LKUP SL
where SS.SUBJ = SL.SUBJ
and SL.SUBJ_TYPE IN ('R', 'U') AND SS.TCHR_ID IN
(select SS.TCHR_ID from SUBJ_SKILLS SS)
GROUP BY SS.TCHR_ID HAVING COUNT(*) = 2)
Query-2:
select SS.TCHR_ID from SUBJ_SKILLS SS, tchr_with_2_subj tw2s
where SS.TCHR_ID = tw2s.TCHR_ID
GROUP BY SS.TCHR_ID,SS.SUBJ_TYPE HAVING COUNT(*) > 1)
Question:
1)'IN' condition in Query-1 is causing problems and pulling wrong records.
2) Is there a better way to write query to pull matching records using a single query (i.e. instead of creating a table)
Could someone help me on this pls.
For the answer to your original question, I would use window functions:
select ss.*
from (select ss.*,
min(subj) over (partition by tchr_id) as mins,
max(subj) over (partition by tchr_id) as maxs
from SUBJ_SKILLS ss
) ss
where mins <> maxs;
It is unclear how the subject type fits in, but if you need to include that, similar logic will work.
Your second table can be obtained from your first table with:
select ss.*
from
subj_skills as ss
inner join (
select tchr_id
from subj_skills
group by tchr_id
having count(*) > 1
) as mult on mult.tchr_id=ss.tchr_id;
I'd use analytic functions here, asomething like:
select tchr_id, line_nbr, subj, SUBJ_TYPE
from (select count(distinct subj) over (partition by tchr_id) as grp_cnt,
s.*
from subj_skills s)
where grp_cnt > 1
If you need to filter out invalid records, you can do it in the inner query. If a teacher cannot teach the same subject multiple times (the req 'multiple different subjects' can be translated to 'multiple subjects'), then I'd rather use count(*) instead of count(distinct subj).

Sql two table query most duplicated foreign key

I got those two tables sport and student:
First table sport:
|idsport | name |
_______________________
| 1 | bobsled |
| 2 | skating |
| 3 | boarding |
| 4 | iceskating |
| 5 | skiing |
Second table student:
foreign key
|idstudent | name | sport_idsport
__________________________________________
| 1 | john | 3 |
| 2 | pauly | 2 |
| 3 | max | 1 |
| 4 | jane | 2 |
| 5 | nico | 5 |
so far i did this it output which number is mostly inserted, but cant get it to work
with two tables
SELECT sport_idsport
FROM (SELECT sport_idsport FROM student GROUP BY sport_idsport ORDER BY COUNT(*) desc)
WHERE ROWNUM<=1;
I need to output name of most popular sport, in that case it would be skating.
I use oracle sql.
with counter as (
Select sport_idsport,
count(*) as cnt,
dense_rank() over (order by count(*) desc) as rn
from student
group by sport_idsport
)
select s.*, c.cnt
from sport s
join counter c on c.sport_idsport = s.idsport and c.rn = 1;
SQLFiddle example: http://sqlfiddle.com/#!4/b76e21/1
select cnt, sport_idsport from (
select count(*) cnt, sport_idsport
from student
group by sport_idsport
order by count(*) desc
)
where rownum = 1

Issue with SQL involving JOINS

I have 2 tables with similar layout, involving INCOME and EXPENSES.
The id column is a customer ID.
I need a result of customer TOTAL AMOUNT, summing up income and expenses.
Table: Income
| id | amountIN|
+--------------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
Table: Expenses
| id | amountOUT|
+---------------+
| 1 | -x |
| 4 | -z |
My problem is that some customers only have expenses and others just income... so cannot know in advance id I need to do a LEFT or RIGHT JOIN.
In the example above an RIGHT JOIN could do the trick, but if the situation is inverted (more customers on the Expenses table) it doesn't work.
Expected Result
| id | TotalAmount|
+--------------+
| 1 | a - x |
| 2 | b |
| 3 | c |
| 4 | d - z |
Any help?
select id, SUM(Amount)
from
(
select id, amountin as Amount
from Income
union all
select id, amountout as Amount
from Expense
) a
group by id
I believe a full join will solve your problem.
I would approach this as a union. Do that in your subquery then sum on it.
For instance:
select id, sum(amt) from
(
select i.id, i.amountIN as amt from Income i
union all
select e.id, e.amountOUT as amt from Expenses e
)
group by id
You should really have another table like client :
Table: Client
| id |
+----+
| 1 |
| 2 |
| 3 |
| 4 |
So you could do something like that
SELECT Client.ID, COALESCE(Income.AmountIN, 0) - COALESCE(Expenses.AmountOUT, 0)
FROM Client c
LEFT JOIN Income i ON i.ID = c.ID
LEFT JOIN Expense e ON e.ID = c.ID
Will be less complicated and i'm sure it will come handy another time :)