How to join a polymorphic table with its child tables? - sql

I am sorry about not being able to articulate the title of the question or the description of this question better. However, I will give the schema, sample data and expected result. Please help me write a query for such a use case.
Schema of restaurants
id
name
item_type
item_id
Schema of foods
id
name
Schema of food_items
id
name
food_id
Sample data in restaurants
|---------------------|------------------|---------------------|------------------|
| id | name | item_type | item_id |
|---------------------|------------------|---------------------|------------------|
| 1 | Apple Crushers | food_items | 1 |
|---------------------|------------------|---------------------|------------------|
| 2 | Retro Cafe | foods | 2 |
|---------------------|------------------|---------------------|------------------|
| 3 | Fruit Mania | foods | 1 |
|---------------------|------------------|---------------------|------------------|
| 4 | Meat and Eat | NULL | NULL |
|---------------------|------------------|---------------------|------------------|
Sample data in foods:
|---------------------|------------------|
| id | Name |
|---------------------|------------------|
| 1 | Fruits |
|---------------------|------------------|
| 2 | Chocolates |
|---------------------|------------------|
Sample data in food_items
|---------------------|------------------|---------------------|
| id | Name | food_id |
|---------------------|------------------|---------------------|
| 1 | Apple | 1 |
|---------------------|------------------|---------------------|
| 2 | Mango | 1 |
|---------------------|------------------|---------------------|
I need to write a query such that I get this as my result.
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
| r_id | r_name | food_id | food_name | food_item_id | food_item_name |
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
| 1 | Apple Crushers | 1 | Fruit | 1 | Apple |
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
| 2 | Retro Cafe | 2 | Chocolates | NULL | NULL |
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
| 3 | Fruit Mania | 1 | Fruit | NULL | NULL |
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
| 4 | Meat and Eat | NULL | NULL | NULL | NULL |
|---------------------|------------------|---------------------|------------------|---------------------|------------------|
p.s: It will also be very helpful if someone could come up with an appropriate title and description for this problem. I am lost for words to describe this.

You must join the food table twice and use COALESCE:
select
r.id,
r.name,
coalesce(f.id, fif.id) as food_id,
coalesce(f.name, fif.name) as food_name,
fi.id as food_item_id,
fi.name as food_item_name
from restaurants r
left join foods f on f.id = r.item_id and r.item_type = 'foods'
left join food_items fi on fi.id = r.item_id and r.item_type = 'food_items'
left join foods fif on fif.id = fi.food_id
order by r.id;

select r.id, r.name, foods.id, foods.name, food_items.id, food_items.name from restaurents as r
left join food_items on r.item_type = 'food_items' and r.item_id = food_items.id
left join foods on (r.item_type = 'foods' and r.item_id = foods.id) or (r.item_type = 'foods' and r.item_id = food_items.food_id)
This might be having some syntax issues related to table names, but it should work.

For your database, I would suggest another data model based on compound keys. This would guarantee data consistency and makes queries a tad simpler:
food_group
(
food_group_no integer not null,
name varchar(100) not null,
primary key (food_group_no)
);
food
(
food_group_no integer not null,
food_no integer not null,
name varchar(100) not null,
primary key (food_group_no, food_no)
);
restaurant
(
restaurant_no integer not null,
name varchar(100) not null,
food_group_no integer not null,
food_no integer null,
primary key (restaurant_id),
foreign key (food_group_no) references food_group(food_group_no),
foreign key (food_group_no, food_no) references food(food_group_no, food_no)
);
The query:
select
r.restaurant_no,
r.name as restaurant_name,
fg.food_group_no,
fg.name as food_group_name,
f.food_id,
f.name as food_name
from restaurants r
join food_group fg on fg.id = r.food_group_no
left join food f on f.food_group_no = r.food_group_no and f.food_no = r.food_no
order by r.id;

Related

SQL select columns ordered by a query

I have tried and googled everything I can think of but I couldn't find an answer I have this simple database
technicien
+------------------+------------+------+-----+
| Field | Type | Null | Key |
+------------------+------------+------+-----+
| employe id | number(4) | NO | PRI |
| name | char(11) | NO | |
| salary | int(11) | NO | |
+------------------+------------+------+-----+
maintenance
+------------------+------------+------+---------+
| Field | Type | Null | Key |
+------------------+------------+------+---------+
| employe id | number(4) | NO | foreign |
| IP | char(11) | NO | foreign |
| maintenance_date | int(11) | NO | |
+------------------+------------+------+---------+
pc
+------------------+------------+------+---------+
| Field | Type | Null | Key |
+------------------+------------+------+---------+
| value | number(4) | NO | foreign |
| IP | char(11) | NO | PRI |
| price | int(11) | NO | |
+------------------+------------+------+---------+
what I need is to show the name, id, and salary of every technicien who has done a maintenance ordered by the total number of maintenances effected.
SELECT technicien.name, technicien.employeId, technicien.salary
FROM technicien
INNER JOIN maintenance
ON maintenance.employeId = technicien.employeId
INNER JOIN pc
ON maintenance.IP = pc.IP
ORDER BY COUNT(maintenance.maintenance_date)
What you need is to GROUPing with respect to the columns of table technicien, and ORDERing by count of maintenance tasks :
select t.*, count(0) cnt_maintenance
from technicien t
inner join maintenance m on ( t.employee_id = m.employee_id )
inner join pc p on ( p.value = m.IP )
group by t.employee_id, t.name, t.salary
order by count(0);
SQL Fiddle Demo

Sub-query producing an empty set

I am trying to provide a list of active quarterbacks who play for teams who had 20 or more sacks during the season.
I have information in two tables, one of them is a view(v_active_quarterbacks) which shows which quarterbacks are active, the other is the table team_game_stats.
I created the command that lists the teams that have 20+ sacks.
SELECT SUM(sacks)
FROM team_game_stats
GROUP BY team_code
HAVING SUM(sacks) > 20;
I now need to connect this to v_active_quarterbacks so that I can get a list. I have tried the following but it just provides an empty set.
SELECT player_code
FROM v_active_quaterbacks
INNER JOIN team_game_stats ON v_active_quaterbacks.team_code = team_game_stats.team_code
WHERE sacks IN (SELECT SUM(sacks)
FROM team_game_stats
GROUP BY team_code
HAVING SUM(sacks) > 20);
Here is the view description:
+-----------------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------+-------------+------+-----+---------+-------+
| player_code | int(11) | NO | | NULL | |
| first_name | varchar(30) | YES | | NULL | |
| last_name | varchar(30) | YES | | NULL | |
| team_code | int(11) | YES | | NULL | |
| uniform_number | varchar(3) | YES | | NULL | |
| passes_player_code | int(11) | YES | | NULL | |
| COUNT(passes.attempt) | bigint(21) | NO | | 0 | |
+-----------------------+-------------+------+-----+---------+-------+
At this point I am confused and stuck. Any help would be appreciated.
You could use a an inner join on subselect for team_code and sum
SELECT player_code , T.sum_sacks
FROM v_active_quaterbacks
INNER JOIN team_game_stats ON v_active_quaterbacks.team_code = team_game_stats.team_code
INNER JOIN (
SELECT team_code, SUM(sacks) sum_sacks
FROM team_game_stats
GROUP BY team_code
HAVING SUM(sacks) > 20
) T on T.team_code = v_active_quaterbacks.team_code

Doctrine query subset of one-to-many relation

I ve the following tables:
mysql> show columns from Person;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
|guid | varchar(255) | NO | PRI | NULL | |
+------------+--------------+------+-----+---------+-------+
mysql> show columns from Person_Func;
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| Person_id | varchar(255) | NO | PRI | NULL | |
| Func_id | varchar(255) | NO | PRI | NULL | |
+-----------+--------------+------+-----+---------+-------+
mysql> show columns from Func;
+-------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| entry | varchar(255) | NO | | NULL | |
| description | varchar(255) | NO | | NULL | |
| Guid | varchar(255) | NO | PRI | NULL | |
+-------------+--------------+------+-----+---------+-------+
Symfony class Person contains one-to-many relation to Func (one Person - several Funcs) using join table Person_Func. I want to query for Person, who has got a number of Funcs -
steve (a, b , c); john (a, b, d); ele (b, d) - and I query for (a, b) functions and steve and john should be returned.
Right now I am just iterating thru all the persons and querying for Functions - that's very-very slow. Could you please help me?
UPD
I have succeeded with
SELECT DISTINCT d1.guid from (select p.guid, f.entry from Person p, Person_Func jt, Func f where p.Guid = jt.person_id and jt.func_id = f.guid and f.entry in ('A', 'B')) as d1,
(select p.guid, f.entry from Person p, Person_Func jt, Func f where p.Guid = jt.person_id and jt.func_id = f.guid and f.entry in ('A', 'B')) as d2
where d1.guid=d2.guid and d1.entry != d2.entry
But I think that that's a not a good idea, yes?
Linking Person and Person_Func with JOIN is a cleaner way of doing it:
SELECT p.id
FROM Person p INNER JOIN Person_Func pf ON p.id = pf.person_id
INNER JOIN Person_Func pf2 ON p.id = pf2.person_id
WHERE
pf.func_id = 'a'
AND pf2.func_id = 'b'
Here you have the SQLFiddle code

help optimizing query (shows strength of two-way relationships between contacts)

i have a contact_relationship table that stores the reported strength of a relationship between one contact and another at a given point in time.
mysql> desc contact_relationship;
+------------------+-----------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+-----------+------+-----+-------------------+-----------------------------+
| relationship_id | int(11) | YES | | NULL | |
| contact_id | int(11) | YES | MUL | NULL | |
| other_contact_id | int(11) | YES | | NULL | |
| strength | int(11) | YES | | NULL | |
| recorded | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+------------------+-----------+------+-----+-------------------+-----------------------------+
now i want to get a list of two-way relationships between contacts (meaning there are two rows, one with contact a specifying a relationship strength with contact b and another with contact b specifying a strength for contact a -- the strength of the two-way relationship is the smaller of those two strength values).
this is the query i've come up with but it is pretty slow:
select
mrcr1.contact_id,
mrcr1.other_contact_id,
case when (mrcr1.strength < mrcr2.strength) then
mrcr1.strength
else
mrcr2.strength
end strength
from (
select
cr1.*
from (
select
contact_id,
other_contact_id,
max(recorded) as max_recorded
from
contact_relationship
group by
contact_id,
other_contact_id
) as cr2
inner join contact_relationship cr1 on
cr1.contact_id = cr2.contact_id
and cr1.other_contact_id = cr2.other_contact_id
and cr1.recorded = cr2.max_recorded
) as mrcr1,
(
select
cr3.*
from (
select
contact_id,
other_contact_id,
max(recorded) as max_recorded
from
contact_relationship
group by
contact_id,
other_contact_id
) as cr4
inner join contact_relationship cr3 on
cr3.contact_id = cr4.contact_id
and cr3.other_contact_id = cr4.other_contact_id
and cr3.recorded = cr4.max_recorded
) as mrcr2
where
mrcr1.contact_id = mrcr2.other_contact_id
and mrcr1.other_contact_id = mrcr2.contact_id
and mrcr1.contact_id != mrcr1.other_contact_id
and mrcr2.contact_id != mrcr2.other_contact_id
and mrcr1.contact_id <= mrcr1.other_contact_id;
anyone have any recommendations of how to speed it up?
note that because a user may specify the strength of his relationship with a particular user more than once, you must only grab the most recent record for each pair of contacts.
update: here is the result of explaining the query...
+----+-------------+----------------------+-------+----------------------------------------------------------------------------------------+------------------------------+---------+-------------------------------------+-------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------------+-------+----------------------------------------------------------------------------------------+------------------------------+---------+-------------------------------------+-------+--------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 36029 | Using where |
| 1 | PRIMARY | <derived4> | ALL | NULL | NULL | NULL | NULL | 36029 | Using where; Using join buffer |
| 4 | DERIVED | <derived5> | ALL | NULL | NULL | NULL | NULL | 36021 | |
| 4 | DERIVED | cr3 | ref | contact_relationship_index_1,contact_relationship_index_2,contact_relationship_index_3 | contact_relationship_index_2 | 10 | cr4.contact_id,cr4.other_contact_id | 1 | Using where |
| 5 | DERIVED | contact_relationship | index | NULL | contact_relationship_index_3 | 14 | NULL | 37973 | Using index |
| 2 | DERIVED | <derived3> | ALL | NULL | NULL | NULL | NULL | 36021 | |
| 2 | DERIVED | cr1 | ref | contact_relationship_index_1,contact_relationship_index_2,contact_relationship_index_3 | contact_relationship_index_2 | 10 | cr2.contact_id,cr2.other_contact_id | 1 | Using where |
| 3 | DERIVED | contact_relationship | index | NULL | contact_relationship_index_3 | 14 | NULL | 37973 | Using index |
+----+-------------+----------------------+-------+----------------------------------------------------------------------------------------+------------------------------+---------+-------------------------------------+-------+--------------------------------+
You are losing a lot lot lot of time selecting the most recent record. 2 options :
1- Change the way you are stocking data, and have a table with only recent record, and an other table more like historical record.
2- Use analytic request to select the most recent record, if your DBMS allows you to do this. Something like
Select first_value(strength) over(partition by contact_id, other_contact_id order by recorded desc)
from contact_relationship
Once you have the good record line, I think your query will go a lot faster.
Scorpi0's answer got me to thinking maybe I could use a temp table...
create temporary table mrcr1 (
contact_id int,
other_contact_id int,
strength int,
index mrcr1_index_1 (
contact_id,
other_contact_id
)
) replace as
select
cr1.contact_id,
cr1.other_contact_id,
cr1.strength from (
select
contact_id,
other_contact_id,
max(recorded) as max_recorded
from
contact_relationship
group by
contact_id, other_contact_id
) as cr2
inner join
contact_relationship cr1 on
cr1.contact_id = cr2.contact_id
and cr1.other_contact_id = cr2.other_contact_id
and cr1.recorded = cr2.max_recorded;
which i had to do twice (second time into a temp table named mrcr2) because mysql has a limitation where you can't alias the same temp table twice in one query.
with my two temp tables created my query then becomes:
select
mrcr1.contact_id,
mrcr1.other_contact_id,
case when (mrcr1.strength < mrcr2.strength) then
mrcr1.strength
else
mrcr2.strength
end strength
from
mrcr1,
mrcr2
where
mrcr1.contact_id = mrcr2.other_contact_id
and mrcr1.other_contact_id = mrcr2.contact_id
and mrcr1.contact_id != mrcr1.other_contact_id
and mrcr2.contact_id != mrcr2.other_contact_id
and mrcr1.contact_id <= mrcr1.other_contact_id;

MySQL - How to use subquery into IN statement by value

The question is to get table column data and use it as a value list for IN function;
For this example I created 2 tables: movies and genres
Table "movies" contains 3 columns: id, name and genre.
Table "genres" contains 2 columns: id and name.
+- movies-+
| |- movie_id - int(11) - AUTO_INCREMENT - PRIMARY
| |- movie_name - varchar(255)
| |- movie_genres - varchar(255)
|
|
+- genres-+
|- genre_id - int(11) - AUTO_INCREMENT - PRIMARY
|- genre_name - varchar(255)
Both tables contain some dummy data:
+----------+------------+--------------+
| movie_id | movie_name | movie_genres |
+----------+------------+--------------+
| 1 | MOVIE 1 | 2,3,1 |
| 2 | MOVIE 2 | 2,4 |
| 3 | MOVIE 3 | 1,3 |
| 4 | MOVIE 4 | 3,4 |
+----------+------------+--------------+
+----------+------------+
| genre_id | genre_name |
+----------+------------+
| 1 | Comedy |
| 2 | Fantasy |
| 3 | Action |
| 4 | Mystery |
+----------+------------+
My goal is to get result like this:
+----------+------------+--------------+-----------------------+
| movie_id | movie_name | movie_genres | movie_genre_names |
+----------+------------+--------------+-----------------------+
| 1 | MOVIE 1 | 2,3,1 | Fantasy,Action,Comedy |
| 2 | MOVIE 2 | 2,4 | Fantasy,Mystery |
| 3 | MOVIE 3 | 1,3 | Comedy,Action |
| 4 | MOVIE 4 | 3,4 | Action,Mystery |
+----------+------------+--------------+-----------------------+
I'm using this query and it's partly working only problem is that it uses the first value of the movie_genres field in the IN value list.
SELECT `m` . * , GROUP_CONCAT( `g`.`genre_name` ) AS `movie_genre_names`
FROM `genres` AS `g`
LEFT JOIN `movies` AS `m` ON ( `g`.`genre_id`
IN (
`m`.`movie_genres`
) )
WHERE `g`.`genre_id`
IN (
(
SELECT `movie_genres`
FROM `movies`
WHERE `movie_id` =1
)
)
GROUP BY 1 =1
The results greatly differ from the one I want:
+----------+------------+--------------+-------------------+
| movie_id | movie_name | movie_genres | movie_genre_names |
+----------+------------+--------------+-------------------+
| 1 | MOVIE 1 | 2,3,1 | Fantasy,Fantasy |
+----------+------------+--------------+-------------------+
Sorry if I missed some data I'm new to mysql.
What query should I use to get the wanted results?
This is a bad design. You should create a many-to-many link table (movie_id, genre_id)
If you cannot change this design, however, use this query:
SELECT movie.*
(
SELECT GROUP_CONCAT(genre_name)
FROM genres
WHERE find_in_set(genre_id, movie_genres)
) as movie_genre_names
FROM movies