SQL join 3 tables (based on 2 criterias?) - sql

I have 3 tables setup like this (a bit simplified):
time_tracking: id, tr_proj_id, tr_min, tr_type
time_projects: id, project_name
time_tasks: id, task_name
Basically, I want to retrieve either project_name or task_name based on tr_type which can be of value "project" or "task"
An example
time_tracking
+----+------------+--------+---------+
| id | tr_proj_id | tr_min | tr_type |
+----+------------+--------+---------+
| 1 | 3 | 60 | project |
| 2 | 3 | 360 | task |
| 3 | 1 | 120 | project |
| 4 | 2 | 30 | project |
| 5 | 2 | 30 | task |
| 6 | 1 | 90 | task |
+----+------------+--------+---------+
time_projects
+----+------------------------+
| id | project_name |
+----+------------------------+
| 1 | Make someone happy |
| 2 | Start a project |
| 3 | Jump out of the window |
+----+------------------------+
time_tasks
+----+---------------------+
| id | task_name |
+----+---------------------+
| 1 | drink a beer |
| 2 | drink a second beer |
| 3 | drink more |
+----+---------------------+
Desired output
+----+------------------------+------------+--------+---------+
| id | name | tr_proj_id | tr_min | tr_type |
+----+------------------------+------------+--------+---------+
| 1 | Jump out of the window | 3 | 60 | project |
| 2 | drink more | 3 | 360 | task |
| 3 | Make someone happy | 1 | 120 | project |
| 4 | Start a project | 2 | 30 | project |
| 5 | drink a second beer | 2 | 30 | task |
| 6 | drink a beer | 1 | 90 | task |
+----+------------------------+------------+--------+---------+
And being really bad at the whole JOIN thing, here's the only thing I've come up with so far (which doesn't work..):
SELECT tt.tr_proj_id, tt.tr_type, tt.tr_min, pp.project_name, pp.id, ta.task_name, ta.id
FROM time_tracking as tt, time_projects as pp, time_tasks as ta
WHERE ((tt.tr_type = 'project' AND pp.id = tt.tr_proj_id) OR (tt.tr_type = 'task' AND ta.id = tt.tr_proj_id))
AND tt.tr_min > 0
ORDER BY tt.tr_proj_id DESC
If anyone has an idea on how to do this, feel free to share!
Update: Looks like I forgot to specify that I'm using an access database. Which apparently doesn't accept things like CASE or coalesce.. Apparently there is IIF() but I'm not quite sure on how to use it in this case.

Use join clauses and move your join conditions from the where clause into the on clauses:
SELECT
tt.tr_proj_id,
tt.tr_type,
tt.tr_min,
pp.project_name,
pp.id,
ta.task_name,
ta.id
FROM time_tracking as tt
left join time_projects as pp on tt.tr_type = 'project' AND pp.id = tt.tr_proj_id
left join time_tasks as ta on tt.tr_type = 'task' AND ta.id = tt.tr_proj_id
WHERE tt.tr_min > 0
ORDER BY tt.tr_proj_id DESC,tt.tr_day ASC
I've used left join, which gives you a row from the main table even if one doesn't exist for the join (you get nulls from columns in the joined table if there's no join)
A key point here, that many SQL programmers do not realise, is that the ON clause may contain any conditions, even ones not from the joined table (as in this example). Many programmers assume that the conditions must be only those relating to the formal foreign key relationship.

Try this:
SELECT
tt.id,
CASE WHEN tt.tr_type = 'project' THEN pp.project_name
WHEN tt.tr_type = 'task' THEN ta.task_name END as name,
tt.tr_proj_id,
tt.tr_type,
tt.tr_min,
FROM time_tracking as tt
left join time_projects as pp on pp.id = tt.tr_proj_id
left join time_tasks as ta on ta.id = tt.tr_proj_id
WHERE tt.tr_min > 0
ORDER BY tt.tr_proj_id DESC

perform a union on two joins:
select tt.id, tp.project_name name, tt.tr_proj_id, tt.tr_min, tt.tr_type
from time_tracking tt
inner join time_projects tp on tp.id = tt.tr_proj_id
where tt.tr_type = 'project'
union all
select tt.id, tp.project_name name, tt.tr_proj_id, tt.tr_min, tt.tr_type
from time_tracking tt
inner join time_tasks tk on tk.id = tt.tr_proj_id
where tt.tr_type = 'task'
That will give you the exact table results you want

SELECT
time_tracking.id,
time_tracking.tr_min,
time_tracking.tr_type,
coalesce(time_projects.project_name, time_tasks.task_name) as name
FROM time_tracking
LEFT OUTER JOIN time_projects on time_projects.id = time_tracking.tr_proj_id AND time_tracking.tr_type = 'project'
LEFT OUTER JOIN time_tasks on time_tasks.id = time_tracking.tr_proj_id AND time_tracking.tr_type = 'task'
WHERE time_tracking.tr_min > 0
ORDER BY time_tracking.id DESC -- ...
coalesce is MSSQL, there's equivalent ISNULL and such in other database technologies
The idea is you join to the tables and if the join fails, you'll get NULL where the join failed. Then you use COALESCE to pick out the successful join value.

Related

Replace subqueries in where statement

I've built a query that intends to find products (products table) with both a 'used' offer and a 'new' offer, and get the lowest price for each. A product can have multiple offers (link_prices table). The offer's condition is determined by the name of the merchant (merchants table): a name without used and occasion is a 'new' offer, a name with used is a 'used' offer.
Here's a sample of the tables (PostgreSQL):
merchants
+----+---------------+
| id | name |
+----+---------------+
| 1 | amazon_used |
| 2 | ebay_location |
| 3 | amazon |
| 4 | target |
| 5 | target_used |
+----+---------------+
link_prices
+----+-------------+------------+-------+
| id | merchant_id | product_id | price |
+----+-------------+------------+-------+
| 1 | 1 | 1 | |
| 2 | 1 | 2 | 20 |
| 3 | 4 | 2 | 30 |
| 4 | 5 | 2 | 5 |
| 5 | 2 | 3 | 10 |
| 6 | 1 | 4 | 80 |
| 7 | 1 | 3 | 100 |
+----+-------------+------------+-------+
In this case, I'm expecting my query to return
+------------+----------------+---------------+
| product_id | min_used_price | min_new_price |
+------------+----------------+---------------+
| 2 | 5 | 30 |
+------------+----------------+---------------+
I've got the following query to work but I feel like I shouldn't need to use subqueries to achieve this. I just can't work my head around it. Any help would be appreciated to optimize this query.
SELECT products.id,
MIN(CASE WHEN merchants.name ILIKE '%used%' THEN link_prices.price END) as min_used_price,
MIN(CASE WHEN merchants.name NOT ILIKE '%used%' THEN link_prices.price END) as min_new_price
FROM products
INNER JOIN link_prices ON link_prices.product_id = products.id
INNER JOIN merchants ON merchants.id = link_prices.merchant_id
WHERE
products.id IN (
SELECT products.id
FROM products
INNER JOIN link_prices ON link_prices.product_id = products.id
INNER JOIN merchants ON merchants.id = link_prices.merchant_id
AND merchants.name ILIKE '%used%'
AND link_prices.price IS NOT NULL
AND link_prices.price <> 0
)
AND products.id IN (
SELECT products.id
FROM products
INNER JOIN link_prices ON link_prices.product_id = products.id
INNER JOIN merchants ON merchants.id = link_prices.merchant_id
AND merchants.name NOT ILIKE '%used%'
AND merchants.name NOT ILIKE '%location%'
AND link_prices.price IS NOT NULL
AND link_prices.price <> 0
)
GROUP BY products.id
Thanks a ton!
Your description makes this sound like conditional aggregation:
select lp.product_id,
min(lp.price) filter (where m.name like '%used') as min_used_price,
min(lp.price) filter (where m.name not like '%used') as min_new_price
from merchants m join
link_prices lp
on lp.merchant_id = m.id
group by lp.product_id;
You sample query is much more complicated and has conditions that are not mentioned in the text of the question. But I think this structure will work for what you want to do.

SQL - Join with multiple condition

I'm trying to join my users table with my jobs table based on a mapping table users_jobs:
Here is what the users table looks like:
users
|--------|------------------|
| id | name |
|--------|----------------- |
| 1 | Ozzy Osbourne |
| 2 | Lemmy Kilmister |
| 3 | Ronnie James Dio |
| 4 | Jimmy Page |
|---------------------------|
jobs table looks like this:
|--------|-----------------|
| id | title |
|--------|-----------------|
| 1 | Singer |
| 2 | Guitar Player |
|--------------------------|
And users_jobs table looks like this:
|--------|-------------|-------------|---------------|-------------|
| id | user_id | job_id | column3 | column4 |
|--------|-------------|-------------|---------------|-------------|
| 1 | 1 | 1 | 0 | 1 |
| 2 | 2 | 1 | 1 | 0 |
| 3 | 3 | 1 | 0 | 1 |
| 4 | 4 | 2 | 1 | 0 |
|----------------------|-------------|---------------|-------------|
For example, let's say the ozzy does a query.
Here is what should expect:
|--------|------------------|------------|--------- |
| id | name | column3 | column4 |
|--------|----------------- |------------|----------|
| 1 | Ozzy Osbourne | 0 | 1 |
| 2 | Lemmy Kilmister | 1 | 0 |
| 3 | Ronnie James Dio | 0 | 1 |
|---------------------------|------------|----------|
Basically, he can only see the job in which he is registered (role) and the users included.
I tried to do this:
SELECT u1.*, uj1.colum3, uj1.column4
FROM users AS u1
JOIN users_jobs AS uj1 ON uj1.user_id = 1
JOIN jobs AS j1 ON j1.id = up1.job_id
WHERE uj1.job_id = 1
Any help would be great!
Looks like you need INNER JOIN Try this :
select u.id, u.column3 , u.column4 from users u
inner join user_jobs uj on u.id=uj.user_id
inner join jobs j on j.id=uj.job_id
where uj.job_id=1;
If you need by certain user_id
select u.id, u.column3 , u.column4 from users u
inner join user_jobs uj on u.id=uj.user_id
inner join jobs j on j.id=uj.job_id
where uj.job_id=1
and u.id=1;
I found a solution.
Using #stackFan approach adding an EXISTS clause to make sure that the user is in.
SELECT u.id, u.column3 , u.column4
FROM users u
INNER JOIN user_jobs uj on u.id = uj.user_id
INNER JOIN jobs j on j.id = uj.job_id
WHERE uj.job_id = <job-ID>
AND
EXISTS (
SELECT *
FROM users_jobs AS uj
WHERE uj.job_id = <job-ID>
AND uj.user_id = <user-ID>
);
Try LEFT JOIN. It will display all users, whether they have job or not.
SELECT u.id, u.name, uj.colum3, uj.column4
FROM users AS u
LEFT JOIN users_jobs uj ON uj.user_id = u.id
LEFT JOIN jobs j ON j.id = uj.job_id

SQL Server join where not exist on other table

+-------------------+ +-------------------+ +---------------------+
| Service | | Asset | | AssetService |
+-------------------+ +-------------------+ +---------------------+
| Id | Name | | Id | Name | | AssetId | ServiceId |
|-------------------| |-------------------| |---------------------|
| 1 | Service 1 | | 1 | Asset 1 | | 1 | 1 |
| 2 | Service 2 | | 2 | Asset 2 | | 1 | 2 |
| 3 | Service 3 | | 3 | Asset 3 | | 2 | 2 |
+-------------------+ +-------------------+ | 2 | 3 |
+---------------------+
So I have these tables. I want to get the Services that is not on AssetService where AssetId = 1
Like this:
+-------------------+
| Service |
| Id | Name |
+-------------------+
| 3 | Service 3 |
+-------------------+
Is this possible with just inner/left/right join? because I already tried different combinations of inner join but it's not working, like this inner join Asset a on a.Id != as.AssetId. I event tried left and right join.
Can somebody help me?
Thanks.
You can you use an intelligent left join to return non-matching rows only from left table(Service)
SELECT S.Id, S.Name FROM [Service] S
LEFT JOIN ServiceAsset SA
ON S.Id = SA.ServiceId
WHERE SA.ServiceId IS NULL
Note: INNER JOIN returns the matching rows whereas you want the non matching rows then use LEFT JOIN instead
The simplest I can think of:
select * from Service
where Id not in (
select ServiceId
from AssetService
where AssetId = 1);
SQLFiddle link
I don't think it's possible using inner join, because that would only retrieve records that match some criteria and you are looking for records that do not match.
It is, however, possible to do it with left join as Ctznkane525 shows in his answer.
Edit
As jarlh pointed out in the comments, not in might lead to surprising results when there are nulls in the subquery. So, here is the not exists version:
select Id, Name
from Service s
where not exists (
select *
from AssetService a
where AssetId = 1
and ServiceId = s.Id);
SQLFiddle link
Try this:
select * from Service where Id not in (
select ServiceId from AssetService where AssetId = 1
-- we have to filter out NULLs, in case of NULL values query result will be empty
and ServiceId not null
)
It doesn't require any join.
Here is solution with join:
select Id, Name from Service
except
select S.Id, S.Name from Service S join AssetService [AS] on S.Id = [AS].ServiceId
where [AS].AssetId = 1

Oracle ordering by several same meaning columns

I have to make sortable table like this:
Sortable table:
building_id | building_age | title |
-------------------------------------------------
1 | 100 | New york buil |
2 | 50 | House 1 |
3 | 50 | House 10 |
From these tables:
Building Table:
building_id | building_age | building_type_1_FK | building_type_2_FK
---------------------------------------------------------
1 | 100 | null | 1
2 | 50 | 1 | null
3 | 50 | 2 | null
building_type_1:
type_id | title | diff1 |
-------------------------------------------------
1 | New york buil| blablabla |
building_type_2:
building_id | title |
----------------------------
1 | House 1 |
2 | House 10 |
3 | House 500 |
While joining these tables I get several title columns where one of them is not null. Is there any way to sort by title and select top 10 results without fetching all the data and then sorting in the app?
p.s.. I know that in general this architecture is not good, but I can't change it.
Yes. You want to do a left outer join to the two tables, and then bring the results together:
select b.building_id, b.building_age, coalesce(bt1.title, bt2.title) as title
from building b left outer join
building_type_1 bt1
on b.building_type_1_FK = bt1.type_id left outer join
building_type_2 bt2
on b.building_type_2_FK = bt2.building_id;
To get the top 10 results in Oracle:
select *
from (select b.building_id, b.building_age, coalesce(bt1.title, bt2.title) as title
from building b left outer join
building_type_1 bt1
on b.building_type_1_FK = bt1.type_id left outer join
building_type_2 bt2
on b.building_type_2_FK = bt2.building_id
order by title
) b
where rownum <= 10;

SQlite join same table twice with different "on" statement

I couldn't find answer for my question, and I don't know if my query is correct and this could be a SQLite issue, please help me solve the problem.
I have two tables in my database:
processTable {id}
taskTable {id, processId, amount, done}
There is a many-to-one relation (one process can have multiple tasks assigned). The "amount" and "done" are integer values that provides task progress information. If "done" >= "amount", the task is done. I need to query database to get something like that:
+---------+-----------+------------+
| process | tasksDone | tasksCount |
+---------+-----------+------------+
| 1 | 1 | 3 |
+---------+-----------+------------+
| 2 | 2 | 5 |
+---------+-----------+------------+
Basing on data that I have in my tables:
processTable
+----+
| id |
+----+
| 1 |
+----+
| 2 |
+----+
tasksTable
+----+-----------+--------+------+
| id | processId | amount | done |
+----+-----------+--------+------+
| 1 | 1 | 10 | 10 | <- this task is done
+----+-----------+--------+------+
| 2 | 1 | 15 | 5 |
+----+-----------+--------+------+
| 3 | 1 | 80 | 5 |
+----+-----------+--------+------+
| 4 | 2 | 25 | 0 |
+----+-----------+--------+------+
| 5 | 2 | 60 | 60 | <- this task is done
+----+-----------+--------+------+
| 6 | 2 | 30 | 15 |
+----+-----------+--------+------+
| 7 | 2 | 40 | 40 | <- this task is done
+----+-----------+--------+------+
| 8 | 2 | 100 | 50 |
+----+-----------+--------+------+
So, I wrote this query:
SELECT processTable.id AS process,
COUNT(tasksTableDone.id) AS tasksDone,
COUNT(tasksTableAll.id) AS tasksCount
FROM processTable
LEFT JOIN tasksTable AS tasksTableAll
ON tasksTableAll.processId = processTable.id
LEFT JOIN tasksTable AS tasksTableDone
ON tasksTableDone.processId = processTable.id
AND
tasksTableDone.done >= tasksTableDone.amount
But what I've got is:
+---------+-----------+------------+
| process | tasksDone | tasksCount |
+---------+-----------+------------+
| 1 | 3 | 3 |
+---------+-----------+------------+
| 2 | 5 | 5 |
+---------+-----------+------------+
I was trying run the query with only one join at a time, and everything was working well.
Query with first join only:
SELECT processTable.id AS process,
COUNT(tasksTableAll.id) AS tasksCount
FROM processTable
LEFT JOIN tasksTable AS tasksTableAll
ON tasksTableAll.processId = processTable.id
Result:
+---------+------------+
| process | tasksCount |
+---------+------------+
| 1 | 3 |
+---------+------------+
| 2 | 5 |
+---------+------------+
Query with second join only:
SELECT processTable.id AS process,
COUNT(tasksTableDone.id) AS tasksDone
FROM processTable
LEFT JOIN tasksTable AS tasksTableDone
ON tasksTableDone.processId = processTable.id
AND
tasksTableDone.done >= tasksTableDone.amount
Result:
+---------+-----------+
| process | tasksDone |
+---------+-----------+
| 1 | 1 |
+---------+-----------+
| 2 | 2 |
+---------+-----------+
How to use this two joins within one query to get proper results? I know that instead of JOIN I could use another SELECT, but I think it would be more expensive in the performance meaning.
You can implement a CASE statement with an aggregate:
Version using SUM()
SELECT p.id AS process,
sum(case when t.amount = t.done then 1 else 0 end) AS tasksDone,
count(p.id) AS tasksCount
FROM processTable p
LEFT JOIN tasksTable t
ON t.processId = p.id
group by p.id
See SQL Fiddle with Demo
Version using COUNT():
SELECT p.id AS process,
count(case when t.amount = t.done then 1 else null end) AS tasksDone,
count(p.id) AS tasksCount
FROM processTable p
LEFT JOIN tasksTable t
ON t.processId = p.id
group by p.id
See SQL Fiddle with Demo
Edit, after your comment you can wrap this in a select to get the progress:
select process,
tasksDone,
tasksCount,
(tasksDone / tasksCount) progress
from
(
SELECT p.id AS process,
count(case when t.amount = t.done then 1 else null end) AS tasksDone,
count(p.id) AS tasksCount
FROM processTable p
LEFT JOIN tasksTable t
ON t.processId = p.id
group by p.id
) src