This question is hard to explain without a example.
I have 2 tables, companies and employees, they have one-to-many relationship, one Company can attend many Employees.
Simplest table structure is shown here
Company
| id | name |
Employee
| id | name | company_id | join_date |
Now the question is:
How could I select the first 2 employees and show their joining date as column in company table?
So the result looks like this
| id | company_name | first_employee_join_at | second_employee_join_at |
Assuming there is a foreign key column company_id in the employee table:
with emps as (
select id, name, company_id,
row_number() over (partition by company_id order by join_date) as rn
from employee
)
select c.id, c.name as company_name,
e1.join_date as first_employee_join_at,
e2.join_date as second_employee_join_at
from company c
left join emps e1 on e1.company_id = c.id and e1.rn = 1
left join emps e2 on e2.company_id = c.id and e2.rn = 2;
This is not going to be terribly efficient though. A slightly more efficient version would use conditional aggregation:
with emps as (
select id, name, company_id,
row_number() over (partition by company_id order by join_date) as rn
from employee
)
select c.id, c.name as company_name,
max(e.join_date) filter (where rn = 1) as first_employee_join_at,
max(e.join_date) filter (where rn = 2) as second_employee_join_at
from company c
join emps e on e.company_id = c.id and e.rn in (1,2)
group by c.id, c.name;
Related
I have the following 2 tables, with hierarchical data in the first one:
COMPANIES:
COMPANY_ID PARENT_COMPANY_ID
---------- -----------------
1 NULL
2 1
3 2
4 3
5 NULL
6 5
USERS:
USER_ID COMPANY_ID
------- ----------
1 1
2 4
3 5
4 6
I would like to make a query which outputs user ids next to their root company ids. I have tried with the following Common Table Expression (CTE) query, but it fails to output users that are directly under a root company:
Query:
WITH ROOT (COMPANY_ID, ROOT_ID) AS (
SELECT
CHILD.COMPANY_ID,
PARENT.COMPANY_ID
FROM COMPANIES CHILD
INNER JOIN COMPANIES PARENT
ON CHILD.PARENT_COMPANY_ID = PARENT.COMPANY_ID
WHERE
PARENT.PARENT_COMPANY_ID IS NULL
UNION ALL
SELECT
C.COMPANY_ID,
ROOT.ROOT_ID
FROM ROOT
INNER JOIN COMPANIES C
ON ROOT.COMPANY_ID = C.PARENT_COMPANY_ID
)
SELECT
U.USER_ID,
R.ROOT_ID
FROM USERS U
INNER JOIN ROOT R
ON U.COMPANY_ID = R.COMPANY_ID;
Actual output:
USER_ID ROOT_COMPANY_ID
------- ---------------
4 5
2 1
Expected output:
USER_ID ROOT_COMPANY_ID
------- ---------------
1 1
2 1
3 5
4 5
So my query is missing the users with user id 1 and 2 with their respective root companies 1 and 5.
I have created this sqlfiddle with my example:
http://sqlfiddle.com/#!4/36d33a/1
What am I missing here?
I am using Oracle 11 but using H2 for unit tests. So my query needs to be a CTE query and not an Oracle connect by query since H2 only understands the former.
You need to select COMPANY_ID as both COMPANY_ID and ROOT_ID for root companies:
WITH ROOT(COMPANY_ID, ROOT_ID) AS (
SELECT COMPANY_ID, COMPANY_ID FROM COMPANIES WHERE PARENT_COMPANY_ID IS NULL
UNION ALL
SELECT C.COMPANY_ID, ROOT.ROOT_ID FROM COMPANIES C JOIN ROOT
ON C.PARENT_COMPANY_ID = ROOT.COMPANY_ID
) SELECT USER_ID, ROOT_ID FROM USERS JOIN ROOT
ON USERS.COMPANY_ID = ROOT.COMPANY_ID;
You can use Oracle's classical Hierarchical Query including CONNECT_BY_ROOT together with ROW_NUMBER() Analytic Function to filter out the root company in the first query(subquery) for companies table, and then join with the users table :
WITH company AS
(
SELECT c.company_id,
CONNECT_BY_ROOT NVL(c.parent_company_id,c.company_id) AS root_company_id,
ROW_NUMBER() OVER (PARTITION BY c.company_id ORDER BY level DESC) AS rn
FROM companies c
CONNECT BY PRIOR c.company_id = c.parent_company_id
)
SELECT u.user_id, c.root_company_id
FROM company c
JOIN users u
ON u.company_id = c.company_id
WHERE rn = 1
Demo
I am trying out a problem which states me to find " For each year, count the number of movies in that year that had only female actors".
Table schema is as follows:
-------------------- ----------------------- ----------------------
| Movie | | Person | | Cast |
-------------------- ------------------------ ----------------------
| MovieID | year | | PersonID | Gender | | MovieID | PersonID |
-------------------- ------------------------ ----------------------
Running the following query:
SELECT M.YEAR, COUNT(M.MID) NUMBER_OF_FEMALE_ONLY_MOVIES FROM MOVIE M
WHERE M.MID IN (SELECT X.MID FROM (SELECT AX.MID, COUNT(AX.PID) TOTAL_CAST
FROM M_CAST AX GROUP BY AX.MID) X
WHERE
X.TOTAL_CAST = (SELECT COUNT(A.PID) FROM M_CAST A, PERSON B WHERE A.MID =
X.MID AND
TRIM(B.PID) = TRIM(A.PID) AND B.GENDER = 'Female')) GROUP BY M.YEAR
My results are :
---------------------------------------
| year | NUMBER_OF_FEMALE_ONLY_MOVIES |
---------------------------------------
| 1999 | 1 |
| 2005 | 1 |
| 2009 | 1 |
| 2012 | 1 |
| 2018 | 1 |
----------------------------------------
But I need to return 0 as count for the years which do not have any such movies.
Eg.
2013 0
WITH
PERSON_CAST_MERGE AS
(
SELECT P.PID,C.MID,GENDER
FROM PERSON P
INNER JOIN M_CAST C ON C.PID = P.PID
),
MALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) NOT LIKE "%FEMALE%"
),
FEMALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) LIKE "%FEMALE%"
),
ONLY_FEMALE AS
(
SELECT F.MID FROM FEMALE_COUNT F
WHERE F.MID NOT IN (SELECT M.MID FROM MALE_COUNT M)
),
TEST AS
(
SELECT M.YEAR,COUNT(M.MID) AS NO_OF_MOVIES
FROM ONLY_FEMALE F
INNER JOIN MOVIE M ON M.MID = F.MID
GROUP BY M.YEAR
)
SELECT M.YEAR,
CASE
WHEN M.YEAR IN (SELECT F.YEAR FROM TEST F) THEN
(SELECT F.NO_OF_MOVIES FROM TEST F WHERE F.YEAR = M.YEAR)
WHEN M.YEAR <> (SELECT F.YEAR FROM TEST F) THEN
0
END
AS NO_OF_MOVIES
FROM MOVIE M
GROUP BY M.YEAR
I'd suggest exploring the data within the CTE to get a better understanding.
First CTE (all_cast): Return the entire movie cast
Second CTE (male_present): Return movie id's from all_cast where there exists male actors.
Result: Return movies from all_cast where movie id is not present in male_present
WITH all_cast AS (
SELECT SUBSTR(m."year",-4) as 'Year', m.title, trim(m.MID) as MID, p.Name, trim(p.Gender) as Gender
FROM Movie m
JOIN M_Cast mc
ON m.MID = mc.MID
JOIN Person p
ON trim(mc.PID) = p.PID
),
male_present AS (
SELECT year, mid, name
FROM all_cast
WHERE Gender = 'Male'
)
SELECT year, COUNT(DISTINCT mid) as 'All Female Cast'
FROM all_cast a
WHERE NOT EXISTS (SELECT * FROM male_present WHERE a.mid = mid)
GROUP BY year
You need only the group by with subquery as you require reference to the movieids of personids with gender as female in person
SELECT YEAR, COUNT(*) FROM
MOVIE
Where MovieId IN (SELECT MOVIEId
from CAST WHERE PERSONID IN
(Select PersonId from Person Where
Gender ='FEMALE'))
Group by Year
Try this- A DISTINCT MovieID is required as there may have multiple Female casting for a single movie. Distinct will provide the actual count of movies.
SELECT
M.Year,
COUNT(DISTINCT MovieID)
FROM Movie M
INNER JOIN Cast C ON M.MovieID = C.MovieID
INNER JOIN Person P ON C.PersonID = P.PersonID
WHERE P.Gender = 'Female'
GROUP BY M.Year;
I think the problem can be solved by joining all tables and filtering on WHERE clause for female actors. In this case joining tables will also give better performance rather than sub-querying.
Please try the following code:
Select year, count(*)
from movie
join Cast on movie.movieid = cast.movieid
join person on person.personid = cast.personid
where person.gender = 'Female'
group by year
Please let me know if that works fine for you.
By merging your query with the Movie table using the outer left join, you can get the desired results. The time taken will be very low compared to the answer posted by #Lucky
WITH FEMALE_ONLY AS
(SELECT M.YEAR,
COUNT(M.MID) COUNT_ALL_FEMALE
FROM MOVIE M
WHERE M.MID IN
(SELECT Q.MID
FROM
(SELECT MC.MID,
COUNT(MC.PID) total
FROM M_CAST MC
GROUP BY MC.MID) Q
WHERE Q.total =
(SELECT COUNT(A.PID)
FROM M_CAST A,
PERSON B
WHERE A.MID = Q.MID
AND TRIM(B.PID) = TRIM(A.PID)
AND B.Gender = 'Female'))
GROUP BY M.YEAR)
SELECT DISTINCT M.year,
coalesce(FO.COUNT_ALL_FEMALE, 0) FEMALE_ONLY_MOVIES
FROM Movie M
LEFT OUTER JOIN FEMALE_ONLY FO ON M.year = FO.year
ORDER BY M.year;
You can do like this
select z.year, count(*)
from Movie z
where not exists (select *
from Person x, M_Cast xy
where x.PID = xy.PID and xy.MID = z.MID and x.gender!='Female')
group by z.year;
I'm trying to select all company rows from a [Company] table that share with at least one other company, the same number of employees (from an [Employee] table that has a CompanyId column), where each group of respective employees share the same set of LocationIds (a column in the [Employee] table) and in the same proportion.
So, for instance, two companies with three employees each that have the locationIds 1,2, and 2, would be selected by this query.
[Employee]
EmployeeId | CompanyId | LocationId |
========================================
1 | 1 | 1
2 | 1 | 2
3 | 1 | 2
4 | 2 | 1
5 | 2 | 2
6 | 2 | 2
7 | 3 | 3
[Company]
CompanyId |
============
1 |
2 |
3 |
Returns the CompanyIds:
======================
1
2
CompanyIds 1 and 2 are selected because they share in common with at least one other company: 1. the number of employees (3 employees); and 2. the number/proportion of LocationIds associated with those employees (1 employee has LocationId 1 and 2 employees have LocationId 2).
So far I think I want to use a HAVING COUNT(?) > 1 statement, but I'm having trouble working out the details. Does anyone have any suggestions?
This is ugly, but the only way I can think of to do it:
;with CTE as (
select c.Id,
(
select e.Location, count(e.Id) [EmployeeCount]
from Employee e
where e.IdCompany=c.Id
group by e.Location
order by e.Location
for xml auto
) LocationEmployeeData
from Company c
)
select c.Id
from Company c
join (
select x.LocationEmployeeData, count(x.Id) [CompanyCount]
from CTE x
group by x.LocationEmployeeData
having count(x.Id) >= 2
) y on y.LocationEmployeeData = (select LocationEmployeeData from CTE where Id = c.Id)
See fiddle: http://www.sqlfiddle.com/#!6/6bc16/5
It works by encoding the Employee count per Location data (multiple rows) into an xml string for each Company.
The CTE code on its own:
select c.Id,
(
select e.Location, count(e.Id) [EmployeeCount]
from Employee e
where e.IdCompany=c.Id
group by e.Location
order by e.Location
for xml auto
) LocationEmployeeData
from Company c
Produces data like:
Id LocationEmployeeData
1 <e Location="1" EmployeeCount="2"/><e Location="2" EmployeeCount="1"/>
2 <e Location="1" EmployeeCount="2"/><e Location="2" EmployeeCount="1"/>
3 <e Location="3" EmployeeCount="1"/>
Then it compares companies based on this string (rather than trying to ascertain whether multiple rows match, etc).
An alternative solution could look like this. However it also requires performance testing in advance (I don't feel quite confident with <> type join).
with List as
(
select
IdCompany,
Location,
row_number() over (partition by IdCompany order by Location) as RowId,
count(1) over (partition by IdCompany) as LocCount
from
Employee
)
select
A.IdCompany
from List as A
inner join List as B on A.IdCompany <> B.IdCompany
and A.RowID = B.RowID
and A.LocCount = B.LocCount
group by
A.IdCompany, A.LocCount
having
sum(case when A.Location = B.Location then 1 else 0 end) = A.LocCount
Related fiddle: http://sqlfiddle.com/#!6/d9f2e/1
I need a query that will get students whom get Course1, Course2 and Course3. I can query it like that:
SELECT k.name as firstname, k.surname as lastname, k.Email
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
WHERE dn.CourseID IN
(SELECT CourseID FROM Courses WHERE CourseName IN ('Course1','Course2','Course3'))
But i need a result set like that: Name, Surname, Email, Course1, Course2, Course3. No multiple rows for a student, one row and write course name if students get that course.
In fact i can imagine how to write that query (with subselects), wonder about better alternatives.
You can use the PIVOT table operator to do this, something like this:
WITH CTE
AS
(
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, c.CourseID
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
INNER JOIN Courses c ON dn.CourseID = c.CourseID
WHERE c.CourseName IN ('Course1','Course2','Course3')
)
SELECT *
FROM CTE AS c
PIVOT
(
MAX(CourseID)
FOR CourseName IN ([Course1], [Course2], [Course3])
) u;
SQL Fiddle Demo
Note that, Since you don't have other columns to display under each column of course for each students, this query will display CourseID value for each course name, null if the student doesn't have this course. You should choose to display more appropriate column instead, like mark for example:
WITH CTE
AS
(
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, dn.Mark
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
INNER JOIN Courses c ON dn.CourseID = c.CourseID
WHERE c.CourseName IN ('Course1','Course2','Course3')
)
SELECT *
FROM CTE AS c
PIVOT
(
MAX(Mark)
FOR CourseName IN ([Course1], [Course2], [Course3])
) u;
Updated SQL Fiddle Demo
This will give you:
| firstname | lastname | Email | Course1 | Course2 | Course3 |
|-----------|----------|-------|---------|---------|---------|
| StudentA | test | test | 19 | 20 | 15 |
| StudentB | test | test | 16 | 17 | 20 |
| StudentC | test | test | 20 | 19 | 15 |
Also note that, you should be careful about the columns you choose in the anchor query:
...
SELECT
k.name as firstname,
k.surname as lastname,
k.Email,
c.CourseName, dn.Mark
...
Because the PIVOT table operator will group by all the columns except the columns you listed and used to pivot:
MAX(CourseID)
FOR CourseName IN
So in your case, it will group by firstname, lastname, Email.
I think you can use a better query than yours and use MAX(CASE ...) to achieve your expected result like this:
SELECT k.name as firstname, k.surname as lastname, k.Email,
MAX(CASE WHEN c.CourseName = 'Course1' THEN 'Yes' ELSE 'No' END) As Course1,
MAX(CASE WHEN c.CourseName = 'Course2' THEN 'Yes' ELSE 'No' END) As Course2,
MAX(CASE WHEN c.CourseName = 'Course3' THEN 'Yes' ELSE 'No' END) As Course3
FROM Students k
JOIN StudentCourses dn ON dn.StudentID = k.StudentID
JOIN Courses c ON c.CourseID = dn.CourseID AND c.CourseName Like 'Course[1-3]'
GROUP BY k.name, k.surname, k.Email;
I have this table structure:
TABLE: PERSON TABLE: CAR
PersonID PersonID | CarID
------ ---------|---------
1 1 | 51
1 | 52
TABLE: PET TABLE: AGE
PersonID | PetID Person | AgeID
---------|---- -------|----
1 | 81 1 | 20
1 | 82
1 | 81
One person can have many cars and pets, but only one age.
I want to count the number of cars someone has, count the number of pets someone has, and list their age.
This is what I have so far:
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
This produces:
Person | Cars | Pets
-------|------|-----
1 | 2 | null
1 | null | 3
But I'd like the results to look like this:
Person | Cars | Pets | Age
-------|------|------|----
1 | 2 | 3 | 20
There's a fiddle here: http://sqlfiddle.com/#!3/f584a/1/0
I'm completely stuck on how to bring the records into one row and add the age column.
SQL Fiddle
Query 1:
SELECT p.PersonID,
( SELECT COUNT(1) FROM CAR c WHERE c.PersonID = p.PersonID ) AS Cars,
( SELECT COUNT(1) FROM PET t WHERE t.PersonID = p.PersonID ) AS Pets,
a.AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN
AGE a
ON ( p.PersonID = a.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Query 2:
WITH numberOfPets AS (
SELECT PersonID,
COUNT(1) AS numberOfPets
FROM PET
GROUP BY PersonID
),
numberOfCars AS (
SELECT PersonID,
COUNT(1) AS numberOfCars
FROM CAR
GROUP BY PersonID
)
SELECT p.PersonID,
COALESCE( numberOfCars, 0 ) AS Cars,
COALESCE( numberOfPets, 0 ) AS Pets,
AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN AGE a ON ( p.PersonID = a.PersonID )
LEFT OUTER JOIN numberOfPets t ON ( p.PersonID = t.PersonID )
LEFT OUTER JOIN numberOfCars c ON ( p.PersonID = c.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Should work with duplicate Petid or duplicate carid
SqlFiddle Demo
WITH person_cte
AS (SELECT *
FROM person),
car_count
AS (SELECT Count(1) AS car,
p.personid
FROM person_cte p
LEFT OUTER JOIN car c
ON p.personid = c.personid
GROUP BY p.personid),
pet_count
AS (SELECT Count(1) AS Pet,
p.personid
FROM person_cte p
LEFT OUTER JOIN pet c
ON p.personid = c.personid
GROUP BY p.personid)
SELECT c.personid,
c.car,
p.pet,
a.ageid
FROM car_count c
INNER JOIN age a
ON c.personid = a.personid
INNER JOIN pet_count p
ON p.personid = c.personid;
If there wont be any duplicates in Carid or Petid then use this
SqlFiddle Demo
SELECT p.personid,
a.ageid,
Count(DISTINCT carid) as carid,
Count(DISTINCT petid) as petid
FROM person p
INNER JOIN age a
ON p.personid = a.personid
LEFT OUTER JOIN car c
ON p.personid = c.personid
LEFT OUTER JOIN pet pe
ON p.personid = pe.personid
GROUP BY p.personid,
a.ageid
One issue I see with most of these responses is that they will only include people who own a car. What if the person doesn't have a vehicle, but has pets? What if they haven't entered their age, yet? You'd lose that metric.
Tie the person table into this as the main requirement. To get the rest of the numbers you could take various approcahes, such as a simple series of left outer joins on the other tables and count their result.
Also note that tagging "ID" at the end of values is a misnomer and considered bad design practice. If it's an age, just call it "age" or "age_value", but not "AgeID". I would also suggest denormalizing your AGE and PERSON tables and make Age (not AgeID) a nullable field.
E.G.
SELECT
PERSON.PersonID,
AgeID AS Age,
CarCount,
PetCount
FROM
#PERSON AS PERSON
LEFT OUTER JOIN AGE AS AGE
ON AGE.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS CarCount FROM CAR GROUP BY PersonID ) AS CAR
ON CAR.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS PetCount FROM PET GROUP BY PersonID ) AS PET
ON PET.PersonID = PERSON.PersonID
You need to be joining on single values, so do your counts within subqueries
select c.PersonID,a.CarID,b.PetID,c.AgeID from (
select person.PersonID, COUNT(car.CarID) as CarID
from Person INNER JOIN Car on Person.PersonID = Car.PersonID
group by Person.PersonID) a
inner join (
select person.PersonID, COUNT(Pet.PetID) as PetID
from Person INNER JOIN Pet on Person.PersonID = Pet.PersonID
group by Person.PersonID) b
on a.PersonID = b.PersonID
inner join (select PersonID,AgeID from Age) c
on a.PersonID = c.PersonID
Another method is
select person,
sum(cars) as cars,
sum(pets) as pets
from
(
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
) as t
group by person
Do you want to count distinct number of cars/pets? If so, add a distinct within the count.
select
person.personid as person,
count(car.carid) as cars,
count(pet.petid) as pets
age.ageID
from person
left outer join pet on pet.personid = person.personid
left outer join car on car.personid = person.personid
left outer join age on age.personid = person.personid
where car.personid = 1
group by car.personid, age.ageID;