MySQL - custom, additional columns based on foreign keys with other table - mysql

I have the following database schema:
Table: products
| id | name | content |
|----|--------|---------|
| 1 | Pen | ... |
| 2 | Pencil | ... |
| 3 | Rubber | ... |
| 4 | Ruler | ... |
Table: feature_types
| id | name |
|----|----------|
| 1 | Color |
| 2 | Material |
| 3 | ... |
| 4 | ... |
Table: features
| id | product_id | feature_type_id | value |
|----|------------|-----------------|-----------|
| 1 | 1 | 1 | Red |
| 2 | 1 | 2 | Aluminum |
| 3 | 2 | 1 | Green |
| 4 | 2 | 2 | Wood |
| 5 | 3 | 1 | White |
| 6 | 4 | 2 | Plastic |
My question is how can I do something like this:
SELECT *, ... FROM products ...
With result:
| id | name | content | feature_type_1 | feature_type_2 |
|----|--------|---------|----------------|----------------|
| 1 | Pen | ... | Red | Aluminum |
| 2 | Pencil | ... | Green | Wood |
| 3 | Rubber | ... | White | NULL |
| 4 | Ruler | ... | NULL | Plastic |
So as you see, in results we have all columns from products table and additional columns for specified feature_types. Column names correspond to their identifiers, according to the pattern: "feature_type_{ID}".
I know feature_types IDs so it is not necessary to add all possible columns feature_types. I need only 2 additional columns with ID 1 and 2.

If you are only interested in the features: Color and Material, join the tables and group by product:
select
p.id, p.name, p.content,
max(case when t.name = 'Color' then f.value end) Color,
max(case when t.name = 'Material' then f.value end) Material
from products p
left join features f on f.product_id = p.id
left join feature_types t
on t.id = f.feature_type_id and t.name in ('Color', 'Material')
group by p.id, p.name, p.content
I guess in your sample data you did a mistake by setting 1 instead of 2 as feature_type_id for Plastic in the table features.
See the demo.
Results:
| id | name | content | Color | Material |
| --- | ------ | ------- | ----- | --------- |
| 1 | Pen | ... | Red | Aluminium |
| 2 | Pencil | ... | Green | Wood |
| 3 | Rubber | ... | White | |
| 4 | Ruler | ... | | Plastic |

Here's one solution to the part of the problem with which you are struggling...
SELECT product_id
, MAX(CASE WHEN feature_type_id = 1 THEN value END) feature_type_1
, MAX(CASE WHEN feature_type_id = 2 THEN value END) feature_type_2
FROM features
GROUP
BY product_id;
+------------+----------------+----------------+
| product_id | feature_type_1 | feature_type_2 |
+------------+----------------+----------------+
| 1 | Red | Aluminium |
| 2 | Green | Wood |
| 3 | White | NULL |
| 4 | Plastic | NULL |
+------------+----------------+----------------+
4 rows in set (0.03 sec)
or...
SELECT f1.product_id
, f1.value feature_type_1
, f2.value feature_type_2
FROM features f1
LEFT
JOIN features f2
ON f2.product_id = f1.product_id
AND f2.feature_type_id = 2
WHERE f1.feature_type_id = 1;

a semplified way is based on building a string using group_concat
select p.id, p.name, p.content , group_concat( concat(t.name,':',f.value )) all_features
from products p
inner join features f on f.product_id = p.id
inner join feature_types t on t.id = f.feature_type_id
group by p.id

Related

SQL - Get rows with one result and selected option - Relation Table

I have a table that has the relations of products and colors. Each product has one or multiple colors. Is it possible to do a query that returns only the products that have one color only and the color wanted ?
Value from the api : color_slug = white ;
Sample table :
color_table
+----------+------------+
| color_id | color_slug |
+----------+------------+
| 1 | white |
| 2 | blue |
| 3 | black |
| 4 | green |
| 5 | red |
| 6 | yellow |
+----------+------------+
product_table
+------------+--------------+
| product_id | product_name |
+------------+--------------+
| 1 | shoes |
| 2 | shorts |
| 3 | t-shirt |
| 4 | jacket |
| 5 | watch |
| 6 | glasses |
+------------+--------------+
pc_relation
+----+------------+----------+
| id | product_id | color_id |
+----+------------+----------+
| 1 | 1 | 5 |
| 2 | 1 | 1 |
| 3 | 2 | 1 |
| 4 | 2 | 4 |
| 5 | 2 | 3 |
| 6 | 3 | 2 |
| 7 | 4 | 1 |
| 8 | 5 | 5 |
| 9 | 5 | 6 |
| 10 | 6 | 1 |
+----+------------+----------+
Select unique color values (if i put WHERE color_id = 1 the product colors are not longer of one color only) :
SELECT product_id
FROM pc_relation
// WHERE color_id = 1
GROUP BY product_id
HAVING MIN(color_id) = MAX(color_id)
pc_relation.id = 6,7,10
SELECT *
FROM color_table
INNER JOIN pc_relation ON pc_relation.color_id = color_table.color_id
INNER JOIN product_table ON pc_relation.product_id = product_table.product_id
WHERE colors.color_slug = 'white'
Values wanted (color_slug = white):
pc_relation.id = 7,10
product_table.product_name = jacket, glasses
*all the combinations are unique and indexed. For example I cannot have one product with the same color
twice.
You are on the right track with your first query. Move the color comparison to the HAVING clause:
SELECT product_id
FROM pc_relation
GROUP BY product_id
HAVING MIN(color_id) = MAX(color_id) AND
MIN(color_id) = 1;
You can also phrase this using NOT EXISTS:
select r.*
from pc_relation r
where r.color_id = 1 and
not exists (select 1
from pc_relation r2
where r2.product_id = r.product_id and r2.color_id <> r.color_id
);
However, the GROUP BY method is more general.

How can I determine which user is the top one in the specific tag?

I have a question and answer website like stackoverflow. Here is the structure of some tables:
-- {superfluous} means some other columns which are not related to this question
// q&a
+----+-----------------+--------------------------+------+-----------+-----------+
| id | title | body | type | related | author_id |
+----+-----------------+--------------------------+------+-----------+-----------+
| 1 | How can I ... | I'm trying to make ... | q | NULL | 3 |
| 2 | | You can do that by ... | a | 1 | 1 |
| 3 | Why should I .. | I'm wonder, why ... | q | NULL | 1 |
| 4 | | First of all you ... | a | 1 | 2 |
| 5 | | Because that thing ... | a | 3 | 2 |
+----+-----------------+--------------------------+------+-----------+-----------+
// users
+----+--------+-----------------+
| id | name | {superfluous} |
+----+--------+-----------------+
| 1 | Jack | |
| 2 | Peter | |
| 3 | John | |
+----+--------+-----------------+
// votes
+----+----------+-----------+-------+-----------------+
| id | user_id | post_id | value | {superfluous} |
+----+----------+-----------+-------+-----------------+
| 1 | 3 | 4 | 1 | |
| 2 | 1 | 1 | -1 | |
| 3 | 2 | 1 | 1 | |
| 4 | 3 | 2 | -1 | |
| 5 | 1 | 4 | 1 | |
| 6 | 3 | 5 | -1 | |
+----+--------+-------------+-------+-----------------+
// tags
+----+------------+-----------------+
| id | name | {superfluous} |
+----+------------+-----------------+
| 1 | PHP | |
| 2 | SQL | |
| 3 | MySQL | |
| 4 | HTML | |
| 5 | CSS | |
| 6 | C# | |
+----+------------+-----------------+
// q&aTag
+-------+--------+
| q&aid | tag_id |
+-------+--------+
| 1 | 1 |
| 1 | 4 |
| 3 | 5 |
| 3 | 4 |
| 4 | 6 |
+-------+--------+
Now I need to find top users in a specific tag. For example, I need to find Peter as top user in PHP tag. Because his answer for question1 (which has PHP tag) has earned 2 upvotes. Is doing that possible?
Try this:
select q1.title, u.id, u.name, sum(v.value) total from `q&a` q1
left join `q&atag` qt ON q1.id = qt.`q&aid`
inner join tags t ON qt.tag_id = t.id
left join `q&a` q2 ON q2.related = q1.id
left join users u ON q2.author_id = u.id
left join votes v ON v.post_id = q2.id
where t.name = 'PHP'
group by q1.id, u.id
and here is a simple divided solution:
Let us divide it into sub queries:
get the id of the tag you will search for: select id from tags where name = 'PHP'
get the questions with this tag: select 'q&aid' from 'q&aTag' where tag_id = 1.
get the ids of answers for that question: select id, author_id fromq&awhere related in (2.)
get the final query: select user_id, sum(value) from votes where post_id in (3.) group by user_id
Now combining them all give the result:
select user_id, sum(`value`) total from votes
where post_id in (
select id from `q&a` where related in (
select `q&aid` from `q&aTag` where tag_id IN (
select id from tags where name = 'PHP'
)
)
)
group by user_id
you can add this at the end if you want only one record:
order by total desc limit 1

Left join select using Propel ORM

I have 3 table
major table:
+----+------------+
| id | major |
+----+------------+
| 1 | Computer |
| 2 | Architect |
| 3 | Designer |
+----+------------+
classroom table:
+----+----------+-------+
| id | major_id | name |
+----+----------+-------+
| 1 | 1 | A |
| 2 | 1 | B |
| 3 | 1 | C |
| 4 | 2 | A |
| 5 | 2 | B |
| 6 | 3 | A |
+----+----------+-------+
and finally, student_classroom table
+----+------------+--------------+----------+
| id | student | classroom_id | status |
+----+------------+--------------+----------+
| 1 | John | 1 | Inactive |
| 2 | Defou | 2 | Active |
| 3 | John | 2 | Active |
| 4 | Alexa | 1 | Active |
| 5 | Nina | 1 | Active |
+----+------------+--------------+----------+
how can I use propel to build query below
select
a.id,
a.major,
b.number_of_student,
c.number_of_classroom
from major a
left join (
select
major.major_id,
count(student_classroom.id) as number_of_student
from major
left join classroom on classroom.major_id = major.id
left join student_classroom on student_classroom.classroom_id = classroom.id
where student_classroom.`status` = 'Active'
group by major_id
) b on b.major_id = a.major_id
left join (
select
major.major_id,
count(classroom.id) as number_of_classroom
from major
left join classroom on classroom.major_id = major.id
group by major_id
) c on c.major_id = a.major_id
Because I want the final result would be something like this, I spend hours trying to figure it out without success.
+----+------------+-------------------+---------------------+
| id | major | number_of_student | number_of_classroom |
+----+------------+-------------------+---------------------+
| 1 | Computer | 4 | 3 |
| 2 | Architect | 0 | 2 |
| 3 | Designer | 0 | 1 |
+----+------------+-------------------+---------------------+
Try this
select
m.id,
m.major,
count(distinct s.id) as number_of_student ,
count(distinct c.id) as number_of_classroom
from major m
left join classroom c on
(m.id = c.major_id)
left join student_classroom s
on (s.classroom_id = c.id and c.major_id = m.id and s.status = 'active')
group by m.id
order by m.id

Select multiple rows containing values from one column

Actually my question is almost the same with MySQL: Select multiple rows containing values from one column, I want to find the car_id of the cars that have MAKE='FORD' AND COLOR='SILVER', so in this case here it will returns car_id 1 and 2.
PS: There could be multiple criteria at once, like I can search by MAKE + CARLINE + COLOR, MAKE + CARLINE, and etc.
table_cars
+----+--------+----------+-----------+
| id | car_id | name | value |
+----+--------+----------+-----------+
| 1 | 1 | MAKE | FORD |
| 2 | 1 | CARLINE | FIESTA |
| 3 | 1 | COLOR | SILVER |
| 4 | 1 | TOPSPEED | 210KM/H |
| 5 | 2 | MAKE | FORD |
| 6 | 2 | CARLINE | FOCUS |
| 7 | 2 | COLOR | SILVER |
| 8 | 2 | TOPSPEED | 200KM/H |
| 9 | 3 | MAKE | HOLDEN |
| 10 | 3 | CARLINE | ASTRA |
| 11 | 3 | COLOR | WHITE |
| 12 | 3 | TOPSPEED | 212KM/H |
+----+--------+----------+-----------+
Thank you!
select car_id
from your_table
group by car_id
having sum(name = 'MAKE' and value = 'FORD') > 0
and sum(name = 'COLOR' and value = 'SILVER') > 0
Try with self join as below:
SELECT distinct car_id
FROM mytable mt1 INNER JOIN mytable mt2
ON mt1.car_id = mt2.car_id
WHERE mt1.name = 'MAKE'
AND mt1.value = 'FORD'
AND mt2.name = 'COLOR'
AND mt2.value = 'SILVER'

Querying across 6 tables, is there a better way of doing this?

What I did was, I wanted each user to have their own "unique" numbering system. Instead of auto incrementing the item number by 1, I did it so that Bob's first item would start at #1 and Alice's number would also start at #1. The same goes for rooms and categories. I achieved this by creating "mapping" tables for items, rooms and categories.
The query below works, but I know it can definitely be refactored. I have primary keys in each table (on the "ids").
SELECT unique_item_id as item_id, item_name, category_name, item_value, room_name
FROM
users_items, users_map_item, users_room, users_map_room, users_category, users_map_category
WHERE
users_items.id = users_map_item.map_item_id AND
item_location = users_map_room.unique_room_id AND
users_map_room.map_room_id = users_room.room_id AND
users_map_room.map_user_id = 1 AND
item_category = users_map_category.unique_category_id AND
users_map_category.map_category_id = users_category.category_id AND
users_category.user_id = users_map_category.map_user_id AND
users_map_category.map_user_id = 1
ORDER BY item_name
users_items
| id | item_name | item_location |item_category |
--------------------------------------------------------
| 1 | item_a | 1 | 1 |
| 2 | item_b | 2 | 1 |
| 3 | item_c | 1 | 1 |
users_map_item
| map_item_id | map_user_id | unique_item_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_rooms
| id | room_name |
----------------------
| 1 | basement |
| 2 | kitchen |
| 3 | attic |
users_map_room
| map_room_id | map_user_id | unique_room_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_category
| id | room_name |
----------------------
| 1 | antiques |
| 2 | appliance |
| 3 | sporting goods |
users_map_category
| map_room_id | map_user_id | unique_category_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
Rewriting your query with explicit JOIN conditions makes it more readable (while doing the same).
SELECT mi.unique_item_id AS item_id
, i.item_name
, c.category_name
, i.item_value
, r.room_name
FROM users_map_item mi
JOIN users_items i ON i.id = mi.map_item_id
JOIN users_map_room mr ON mr.unique_room_id = i.item_location
JOIN users_room r ON r.room_id = mr.map_room_id
JOIN users_map_category mc ON mc.unique_category_id = i.item_category
JOIN users_category c ON (c.user_id, c.category_id)
= (mc.map_user_id, mc.map_category_id)
WHERE mr.map_user_id = 1
AND mc.map_user_id = 1
ORDER BY i.item_name
The result is unchanged. Query plan should be the same. I see no way to improve the query further.
You should use LEFT [OUTER] JOIN instead of [INNER] JOIN if you want to keep rows in the result where no matching rows are found in the right hand table. You may want to move the additional WHERE clauses to the JOIN condition in this case, as it changes the outcome.