Mysql left join table2 where field from table3 - mysql

table media
id, options
1 a
2 b
3 c
table maps
id, title, type
1 f x
2 g x
3 h y
4 z x
4 w y
table media maps
maps_id media_id
1 2
2 3
I am trying to select all data from table media and join table maps. Each media can contain multiple maps.
SELECT media_t.id, media_t.options,
GROUP_CONCAT(CASE WHEN maps_t.type = 'x' THEN maps_t.title END ORDER BY maps_t.title SEPARATOR ', ') as x,
GROUP_CONCAT(CASE WHEN maps_t.type = 'y' THEN maps_t.title END ORDER BY maps_t.title SEPARATOR ', ') as y
FROM media_table as media_t
LEFT JOIN maps_table as maps_t
ON media_t.id = (
SELECT maps_id FROM {table_media_maps}
WHERE media_id = maps_t.id
)
GROUP BY media_t.id

If you need a 3 tbals join you could try
SELECT me.id, me.options,
GROUP_CONCAT(CASE WHEN ma.type = 'x' THEN ma.title END ORDER BY ma.title SEPARATOR ', ') as x,
GROUP_CONCAT(CASE WHEN ma.type = 'y' THEN ma.title END ORDER BY ma.title SEPARATOR ', ') as y
FROM media as me
LEFT JOIN media_maps mm on me-id = mm.media_id
LEFT JOIN maps ma ON mm.maps_id = ma.id
GROUP BY me.id

your data is not coplete, for all case.
I put a right join for the maps table, so that you cansee that it works, problably you want there also a left join, but you still need to join all three tables
CREATE TABLE media_table (
`id` INTEGER,
`options` VARCHAR(1)
);
INSERT INTO media_table
(`id`, `options`)
VALUES
('1', 'a'),
('2', 'b'),
('3', 'c');
CREATE TABLE table_media_maps (
`maps_id` INTEGER,
`media_id` INTEGER
);
INSERT INTO table_media_maps
(`maps_id`, `media_id`)
VALUES
('1', '2'),
('2', '3');
CREATE TABLE maps_table (
`id` INTEGER,
`title` VARCHAR(1),
`type` VARCHAR(1)
);
INSERT INTO maps_table
(`id`, `title`, `type`)
VALUES
('1', 'f', 'x'),
('2', 'g', 'x'),
('3', 'h', 'y'),
('4', 'z', 'x'),
('4', 'w', 'y');
SELECT media_t.id, media_t.options,
GROUP_CONCAT(
CASE WHEN maps_t.type = 'x' THEN maps_t.title END
ORDER BY maps_t.title SEPARATOR ', ')
as x,
GROUP_CONCAT(
CASE WHEN maps_t.type = 'y' THEN maps_t.title END
ORDER BY maps_t.title SEPARATOR ', ')
as y
FROM media_table as media_t
LEFT JOIN table_media_maps m_m_t ON media_t.id = m_m_t.media_id
right JOIN maps_table as maps_t
ON maps_t.id = m_m_t.maps_id
GROUP BY media_t.id, media_t.options
id | options | x | y
---: | :------ | :- | :---
null | null | z | h, w
2 | b | f | null
3 | c | g | null
db<>fiddle here

Related

MySQL DB - how to group by firstName and surName columns in my select query?

Could you please help me with group by firstName and surName columns in my select query ?
to view 4 rows instead of 8 rows without NULL values
i joined these two tables in MySQL:
CREATE TABLE IF NOT EXISTS `users` (
`ID` int NOT NULL AUTO_INCREMENT,
`userName` varchar(100) NOT NULL,
`firstName` varchar(100) NOT NULL,
`surName` varchar(100) NOT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `userName` (`userName`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
INSERT INTO `users` (`ID`, `userName`, `firstName`, `surName`) VALUES
(1, 'Toni_889', 'Toni', 'Saba'),
(2, 'Rani_185', 'Rani', 'Brown'),
(3, 'Mariaaa111', 'Maria', 'Rosee'),
(4, 'DDD_Ron', 'David', 'Rondy');
COMMIT;
CREATE TABLE IF NOT EXISTS `addresses` (
`ID` int NOT NULL AUTO_INCREMENT,
`type` char(1) NOT NULL,
`user_id` int NOT NULL,
`city` varchar(100) NOT NULL,
`street` varchar(100) NOT NULL,
`country` varchar(2) NOT NULL,
PRIMARY KEY (`ID`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3;
INSERT INTO `addresses` (`ID`, `type`, `user_id`, `city`, `street`, `country`) VALUES
(1, 'B', 3, 'Paris', 'Pariska 22', 'FR'),
(2, 'D', 3, 'Berlin', 'Avenue 33', 'GR'),
(3, 'B', 1, 'Damascus', 'Midan st 49', 'SY'),
(4, 'D', 1, 'Prague', 'Vinohradska 22', 'CZ'),
(5, 'B', 2, 'Prague', 'Italiska 36', 'CZ'),
(6, 'D', 2, 'London', 'Avnue 28', 'UK'),
(7, 'B', 4, 'Amsterdam', 'Sparta st 88', 'NL'),
(8, 'D', 4, 'Rome', 'Clombus 61', 'IT');
SELECT u.firstName firstName, u.surName surName ,
(SELECT a.city from addresses WHERE a.type = 'B' and a.user_id = u.ID limit 1 ) as BILLING_CITY,
(SELECT a.street from addresses WHERE a.type = 'B' and a.user_id = u.ID limit 1) as BILLING_STREET ,
(SELECT a.country from addresses WHERE a.type = 'B' and a.user_id = u.ID limit 1) as BILLING_COUNTRY ,
(SELECT a.city from addresses WHERE a.type = 'D' and a.user_id = u.ID limit 1) as DELIVERY_CITY ,
(SELECT a.street from addresses WHERE a.type = 'D' and a.user_id = u.ID limit 1) as DELIVERY_STREET ,
(SELECT a.country from addresses WHERE a.type = 'D' and a.user_id = u.ID limit 1) as DELIVERY_COUNTRY
FROM users u
JOIN addresses a
on a.user_id = u.ID;
but i got users duplicated in rows with null values like this screenshot :
i expected 4 rows only in result without null
Conditional aggregation it is called
SELECT
u.firstName firstName, u.surName surName,
MAX(CASE WHEN a.type = 'B' THEN a.city END) as BILLING_CITY,
MAX(CASE WHEN a.type = 'B' THEN a.street END) as BILLING_STREET ,
MAX(CASE WHEN a.type = 'B' THEN a.country END) as BILLING_COUNTRY ,
MAX(CASE WHEN a.type = 'D' THEN a.city END) as DELIVERY_CITY,
MAX(CASE WHEN a.type = 'D' THEN a.street END) as DELIVERY_STREET ,
MAX(CASE WHEN a.type = 'D' THEN a.country END) as DELIVERY_COUNTRY
FROM
`addresses` a JOIN `users` u ON a.`user_id` = u.`ID`
GROUP BY u.firstName, u.surName
| firstName | surName | BILLING\_CITY | BILLING\_STREET | BILLING\_COUNTRY | DELIVERY\_CITY | DELIVERY\_STREET | DELIVERY\_COUNTRY |
|:----------|:--------|:--------------|:----------------|:-----------------|:---------------|:-----------------|:------------------|
| Maria | Rosee | Paris | Pariska 22 | FR | Berlin | Avenue 33 | GR |
| Toni | Saba | Damascus | Midan st 49 | SY | Prague | Vinohradska 22 | CZ |
| Rani | Brown | Prague | Italiska 36 | CZ | London | Avnue 28 | UK |
| David | Rondy | Amsterdam | Sparta st 88 | NL | Rome | Clombus 61 | IT |
fiddle
I was able to join into the address table twice once with billing (B) and once with Delivery (D). This should give you the 4 rows.
SELECT
u.firstName FirstName
, u.surName SurName
, b.Billing_City
, b.Billing_Street
, b.Billing_Country
, d.Delivery_City
, d.Delivery_Street
, d.Delivery_Country
FROM users u
INNER JOIN
(
SELECT
City AS Billing_City
, Street AS Billing_Street
, Country AS Billing_Country
, user_id
FROM addresses a
WHERE a.type = 'B'
) b ON u.ID = b.user_id
INNER JOIN
(
SELECT
City AS Delivery_City
, Street AS Delivery_Street
, Country AS Delivery_Country
, user_id
FROM addresses a
WHERE a.type = 'D'
) d ON u.ID = d.user_id
ORDER BY u.FirstName
https://www.db-fiddle.com/f/iL3DwTo6U5m7AM8ch5C5xt/0
Join with addresses twice. Use the WHERE conditions to filter each reference to the table to a specific type.
select u.firstName firstName, u.surName surName,
a1.city AS billing_city, a1.street AS billing_street, a1.country AS billing_country,
a2.city AS delivery_city, a2.street AS delivery_street, a2.country AS billing_country
FROM users AS u
JOIN addresses AS a1 ON u.id = a1.user_id
JOIN addresses AS a2 on u.id = a2.user_id
WHERE a1.type = 'B' AND a2.type = 'D'

MySQL Select Data From two tables and generate column name dynamically

1: Product info (corewp_product) 2: Product Metadata (corewp_productmeta)
I want to select (assume the user is searching) by price, color, size e.t.c metadata depending on the search parameter and metadata available.
Eg. search might be
where (color=red and price between 100 and 500)
Since metadata is dynamically added I don't want to create a new column for each metadata. Some of the products are in group (eg. Sneaker might be in red,blue with various prices)
My Tables are like this:
corewp_product
id
idname
title
category
price
type
meta
1
A1
Sneakers
2
0
grouped
0
2
A2
Branded Shirts for sale
1
0
grouped
0
3
A3
Long Sleeve Shirts
1
0
grouped
0
corewp_productmeta
id
postid_field
group_id
meta_name
meta_value
1
A1
G1
color
red
2
A1
G1
size
EU40
3
A1
G1
price
28
4
A1
G2
color
black
5
A1
G2
size
EU41
6
A1
G2
price
30
7
A1
G3
color
red
8
A1
G3
size
E40
9
A1
G3
price
50
10
A2
G1
color
any
11
A2
G1
size
L
12
A2
G1
price
60
13
A3
G1
color
red
14
A3
G1
price
30
Problem:
Selecting products with color = red and price between 0 and 50 or with other metadata.
Approach 1- using join:
I have tried to do it this way
SELECT corewp_product.id, corewp_product.idname, corewp_product.title, P.amount, C.color FROM corewp_product JOIN ( SELECT `postid_field` as priceId, `meta_value` as amount, `group_id` as ggroup FROM `corewp_productmeta` WHERE (`meta_name` = 'price' AND `meta_value` BETWEEN 10 AND 50)) AS P JOIN (SELECT `postid_field` as colorId, `meta_value` as color, `group_id` as ggroup FROM `corewp_productmeta` WHERE (`meta_name` = 'color' AND `meta_value` = 'red') GROUP BY `group_id`,`postid_field`) AS C ON p.ggroup = C.ggroup WHERE corewp_product.idname = P.priceId AND corewp_product.idname = C.colorId
But the problem with the code above is what happen when a new meta data is added e.g: brand name
id
postid_field
group_id
meta_name
meta_value
15
A1
G1
brand
nike
and the new search has to include brand name color = red and brand = nike and price between 0 and 50, I will have to alter the query above which is something am looking to avoid.
Approach 2- using view:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'MAX(IF(pm.meta_name = ''',
meta_name,
''', pm.meta_value, NULL)) AS ',
meta_name
)
) INTO #sql
FROM corewp_productmeta;
SET #sql = CONCAT('SELECT p.idname , p.title, ', #sql, ' FROM corewp_product p LEFT JOIN corewp_productmeta AS pm ON p.idname = pm.postid_field GROUP BY pm.group_id,p.idname,p.title');
SET #qrys = CONCAT('CREATE OR REPLACE VIEW meta_view AS ',#sql);
PREPARE stmt FROM #qrys;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
The approach works as expected but now I have to get all the data from the view table which also I want to avoid now an issue comes when a new meta data is added e.g. brand same issue repeat.
It will be great if I could be able to query like select... where brand=xx and color=aa then results would come with column brand and name if brand doesn't exist then brand column returned as null or result not found same with other dynamic values passed in a query
Is there any way you can help me guys? I will appriciate.
N.B: this query will also include pagination limit (0,10) once full system is deployed.
SQL FORMAT
CREATE TABLE `corewp_product` (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`idname` varchar(20) NOT NULL,
`title` varchar(60) NOT NULL,
`category` int(11) NOT NULL,
`price` double(20,2) NOT NULL,
`type` varchar(20) NOT NULL,
`meta` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `corewp_product` (`id`, `idname`, `title`, `category`, `price`, `type`, `meta`) VALUES
(1, 'A1', 'Sneakers', 2, 0.00, 'grouped', 0),
(2, 'A2', 'Branded Shirts for sale', 1, 0.00, 'grouped', 0),
(3, 'A3', 'Long Sleeve Shirts', 1, 0.00, 'grouped', 0);
CREATE TABLE `corewp_productmeta` (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`postid_field` varchar(5) NOT NULL,
`group_id` varchar(5) NOT NULL,
`meta_name` varchar(50) NOT NULL,
`meta_value` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `corewp_productmeta` (`id`, `postid_field`, `group_id`, `meta_name`, `meta_value`) VALUES
(1, 'A1', 'G1', 'color', 'red'),
(2, 'A1', 'G1', 'size', 'EU40'),
(3, 'A1', 'G1', 'price', '28'),
(4, 'A1', 'G2', 'size', 'EU41'),
(5, 'A1', 'G2', 'color', 'black'),
(6, 'A1', 'G2', 'price', '30'),
(7, 'A1', 'G3', 'color', 'red'),
(8, 'A1', 'G3', 'size', 'E40'),
(9, 'A1', 'G3', 'price', '50'),
(10, 'A2', 'G1', 'color', 'any'),
(11, 'A2', 'G1', 'size', 'L'),
(12, 'A2', 'G1', 'price', '60'),
(13, 'A3', 'G1', 'color', 'red'),
(14, 'A3', 'G1', 'price', '30');
WITH vars AS (
SELECT postid_field,title,
GROUP_CONCAT(IF(meta_name='color', meta_value, NULL) SEPARATOR '') color,
GROUP_CONCAT(IF(meta_name='size', meta_value, NULL) SEPARATOR '') size,
GROUP_CONCAT(IF(meta_name='price', meta_value, NULL) SEPARATOR '') price
FROM corewp_productmeta,corewp_product
WHERE postid_field = idname
GROUP BY group_id,postid_field,title
)
select * from vars
where price > 29 AND price < 59
Query Demo
The query uses the WITH clause to create a sub-query that joins the two tables and assigns the resulting table to a variable eg: vars.
After that, you can query from the variable like any normal table and apply your filters in the where clause and you can filter using the extended columns. eg: where price > 29 AND price < 59
Check the Query Demo on the link above.
where (color=red and price between 100 and 500)
SELECT pr.*
FROM corewp_product pr
JOIN corewp_productmeta pm ON pr.idname = pm.postid_field
WHERE (pm.meta_name = 'color' AND meta_value = 'red')
OR (pm.meta_name = 'price' AND meta_value + 0 BETWEEN 100 AND 500)
GROUP BY pr.id
HAVING COUNT(*) = 2;
DEMO
Some explanations.
We select the metadata rows which matches any of the criteria. Then we group by the product and count the amount of matched meta values for it. Finally we return only those products which have the matched amount equal to total one.
This query does not need in dynamic SQL. You must only put correct values into the conditions.
Pay attention to this: meta_value + 0 BETWEEN 100 AND 500. The addition + 0 performs implicit data casting to numeric (of course we can use explicit CAST(meta_value AS UNSIGNED)). This allows make numeric comparing context. Without this addition the comparing context will be string, and we may obtain incorrect output (for example, for "price between 50 and 100").

Find DISTINCT LAST record with SQL LEFT JOIN

I'm running MySQL 5.6.
I have two related tables:
CREATE TABLE Cars (
id INT NOT NULL AUTO_INCREMENT,
plate VARCHAR(16) NOT NULL,
flag TINYINT,
PRIMARY KEY(id)
)
and:
CREATE TABLE Rents (
id INT NOT NULL AUTO_INCREMENT,
out_date DATE NOT NULL,
in_date DATE,
car_id INT,
FOREIGN KEY (car_id) REFERENCES Cars(id),
PRIMARY KEY(id)
)
I can have multiple rents for each car (0 to many).
I need to select all vehicles in table Cars (with flag = 1) along with their status i.e. I need to know if each car is currently unavailable (only out_date is filled) or availabe (out_date and in_date filled) of course also vehicles without any rents are to be considered available.
The result set need to include out_date and in_date values [Update 17/07/2022].
I tought to use something like:
SELECT
*,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rent.Car_id WHERE Cars.Flag = 1
but this of course will just return all the rows with positive flag match and a status evaluation (0 unavailable, 1 available):
id | plate | flag | id | out_date | in_date | car_id | status
---------------------------------------------------------------------
'1', 'FA787MX', '1', '1', '2022-07-14', '2022-07-15', '1', '1'
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
While I need to have this (edited 17/07/2022):
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
i.e. only the second row of FA787MX car should be mantained since it's the most recent out_date value (no matter if it's id is higher or lower).
For the sake of completeness: There is no guarantee that rental ids will be kept consistent with their rental history. In other words you cannot be sure that for a given car the rental where in_date = NULL is the correct one but you should compare them by out_date value.
Data sample:
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (1, 'FA787MX', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (2, 'EX431YY', 0);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (3, 'AB124DF', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (4, 'CC666VC', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (5, 'GG435ED', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (1, '2022-07-14', '2022-07-15', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (2, '2022-07-16', NULL, 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (3, '2022-07-16', NULL, 2);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (4, '2022-07-13', '2022-07-14', 3);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (5, '2022-07-16', NULL, 5);
One option is to join to find only those rentals that are still outstanding (in_date IS NULL). That will drop the old rentals having in_date not null.
Based on the updated requirements, there are a few ways to do it. One is a simple outer join to find the most recent rental per car to obtain the corresponding in_date as well...
MySQL 5.6 fiddle
SELECT Cars.*
, Rents.out_date
, Rents.in_date
, Rents.id IS NULL OR Rents.in_date IS NOT NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
LEFT JOIN Rents AS r2
ON Rents.out_date < r2.out_date
AND Rents.Car_id = r2.Car_id
WHERE Cars.Flag = 1
AND r2.Car_id IS NULL
ORDER BY Cars.id
;
The result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
2022-07-13
2022-07-14
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
Based on the original requirements: Try this (fiddle):
SELECT Cars.*
, Rents.in_date
, CASE WHEN in_date IS NOT NULL OR Rents.id IS NULL THEN 1 ELSE 0 END AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
and if the results contain only those with in_date IS NULL, this reduces to:
SELECT Cars.*
, out_date
, Rents.in_date
, Rents.id IS NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
Result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
If your version of MySql is 8.0+ use ROW_NUMBER() window function to pick the latest row for each car in Rents:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT *, ROW_NUMBER() OVER (PARTITION BY car_id ORDER BY out_date DESC) rn
FROM Rents
) r ON r.car_id = c.id AND r.rn = 1
WHERE c.flag = 1;
For previous versions use NOT EXISTS:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT r1.*
FROM Rents r1
WHERE NOT EXISTS (
SELECT *
FROM Rents r2
WHERE r2.car_id = r1.car_id AND r2.out_date > r1.out_date
)
) r ON r.car_id = c.id
WHERE c.flag = 1;
See the demo.
If you imagine the result of your query as a table, you can easily write a query that would give you what you need (the subquery is just yours with the select spelled out to give a unique column name to the second id column, as it seemed useful - the only way to uniquely identify a row):
SELECT MAX(rent_id) FROM (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
) as rental_status
WHERE status = 0
GROUP BY car_id;
Which tells you which rows are interesting:
+--------------+
| MAX(rent_id) |
+--------------+
| 2 |
| 5 |
+--------------+
Now you can use a join to return the results of your initial query only for the interesting rows. To avoid having to spell out that query all over again, MySQL 8 has a way to stash the results of your core query and use it like a table:
WITH
status_data AS (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
)
SELECT * from status_data
JOIN (
SELECT MAX(rent_id) as rent_id FROM status_data
WHERE status = 0
GROUP BY car_id
) as ids using(rent_id);
Giving the result:
+---------+----+---------+------+------------+---------+--------+--------+
| rent_id | id | plate | flag | out_date | in_date | car_id | status |
+---------+----+---------+------+------------+---------+--------+--------+
| 2 | 1 | FA787MX | 1 | 2022-07-16 | NULL | 1 | 0 |
| 5 | 5 | GG435ED | 1 | 2022-07-16 | NULL | 5 | 0 |
+---------+----+---------+------+------------+---------+--------+--------+

Mysql: rows having minimum value and prioritize where condition based on case

I have two tables, first table is:
docs
and another table:
doc_val
with doc_id as foreign key from table docs
I need to get list of docs (including val, type and criteria from doc_val ) which matches certain conditions, say for example: doc_val.criteria = 'L' and docs.rev = 1
While getting this list of docs I also need to make sure that the doc_val.val for given doc_id is the minimum. AND also make sure that doc_val.type = 'D', given that the there exists doc_val.type = 'D' ELSE we should just simply get doc_val for given doc_id which has minimum doc_val.val.
CREATE TABLE IF NOT EXISTS `docs` (
`id` int(6) unsigned NOT NULL,
`rev` int(3) unsigned NOT NULL,
`content` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `doc_val` (
`id` int(6) unsigned NOT NULL,
`doc_id` int(6) unsigned NOT NULL,
`val` int(3) unsigned NOT NULL,
`type` varchar(2) NOT NULL,
`criteria` varchar(2) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `docs` (`id`, `rev`, `content`) VALUES
('1', '1', 'The earth is flat'),
('2', '1', 'One hundred angels can dance on the head of a pin'),
('3', '1', 'The earth is flat and rests on a bull\'s horn'),
('4', '4', 'The earth is like a ball.');
INSERT INTO `doc_val` (`id`, `doc_id`, `val`, `type`, `criteria`) VALUES
('1', '1', 100, 'D', 'L'),
('2', '1', 101, 'D', 'L'),
('3', '1', 80, 'H', 'L'),
('4', '2', 10, 'H', 'S'),
('5', '2', 90, 'H', 'L'),
('6', '3', 100, 'D', 'L'),
('7', '3', 100, 'D', 'L');
With this query if I take b.type = 'D' simply as part of where condition, I loose all docs which do not have type as D.
SELECT a.id, a.rev, a.content, b.val, b.type, b.criteria
FROM `docs` a
JOIN `doc_val` b ON b.doc_id = a.id
WHERE a.`rev` = 1 and b.type = 'D' and b.criteria = 'L'
GROUP BY `a`.`id`
HAVING min(b.`val`)
If we do not consider type=D as condition at all, the output for this condition kind of worked but,
SELECT a.id, a.rev, a.content, b.val, b.type, b.criteria
FROM `docs` a
JOIN `doc_val` b ON b.doc_id = a.id
WHERE a.`rev` = 1 and b.criteria = 'L'
GROUP BY `a`.`id`
HAVING min(b.`val`)
final expected output:
But Technically without type=D as condition, I should have received an output for doc.id = 1 as:
So I am probably doing something wrong with use of HAVING any direction would be helpful.
Is it possible to prioritize doc_val.type with doc_val.type = D, such that when a row with type = D it takes priority, if it doesn't exist simply take one with minimum value without considering type?
You can try below -
DEMO
SELECT
*
FROM
(SELECT
a.id, a.rev, a.content, MIN(b.val) val, b.type, b.criteria
FROM
`docs` a
JOIN `doc_val` b ON b.doc_id = a.id
WHERE
a.`rev` = 1 AND b.criteria = 'L'
GROUP BY a.id , a.rev , a.content , b.type , b.criteria) A
WHERE
val IN (SELECT
MAX(val)
FROM
(SELECT
a.id, a.rev, a.content, MIN(b.val) val, b.type, b.criteria
FROM
`docs` a
JOIN `doc_val` b ON b.doc_id = a.id
WHERE
a.`rev` = 1 AND b.criteria = 'L'
GROUP BY a.id , a.rev , a.content , b.type , b.criteria) B
WHERE
A.content = B.content)
OUTPUT:
id rev content val type criteria
1 1 The earth is flat 100 D L
2 1 One hundred angels can dance on the head of a pin 90 H L
3 1 The earth is flat and rests on a bull's horn 100 D L
After multiple tests, I've come up with something like this:
SELECT a.id, a.rev, a.content, c.val, c.type, c.criteria
FROM `docs` a
JOIN
(SELECT doc_id,criteria,
LEFT(gctv,1) AS 'Type',
SUBSTRING(SUBSTRING_INDEX(REPLACE(gctv,',',' '),' ',1),2) AS 'val'
FROM
(SELECT doc_id,criteria,
CASE
WHEN INSTR(ctv,#type) <> 0 THEN REPLACE(MID(ctv,INSTR(ctv,#type)) ,' ','')
WHEN INSTR(ctv,#type)=0 THEN REPLACE(ctv,' ','') END AS gctv
FROM
(SELECT doc_id ,criteria,
GROUP_CONCAT(tv ORDER BY val ASC) AS ctv
FROM
(SELECT doc_id,criteria,val,
CONCAT_WS(' ',TYPE,VAL) tv
FROM doc_val
WHERE criteria='L'
)A
GROUP BY doc_id
)B, (SELECT #type:='D') temp
)C) c
ON a.id=c.doc_id
WHERE rev=1;
I try to break it down:
This is the core of the query,
SELECT doc_id,criteria,val,
CONCAT_WS(' ',TYPE,VAL) tv
FROM doc_val
WHERE criteria='L';
Basically, what its doing here is to combine Type and val into one column, with the condition of criteria='L'. The results look like this:
The first outer query,
SELECT doc_id, criteria,
GROUP_CONCAT(tv ORDER BY val ASC) AS ctv
FROM
(SELECT doc_id,criteria,val,
CONCAT_WS(' ',TYPE,VAL) tv
FROM doc_val
WHERE criteria='L'
)A
GROUP BY doc_id
is performing a GROUP_CONCAT based on the core query result and its grouped by doc_id. Which produces a result like below:
You notice in the GROUP_CONCAT I've added a condition to ORDER BY val ASC. This will return the smallest value first from left to right order.
Then we go to the third query:
SELECT doc_id,criteria,
CASE
WHEN INSTR(ctv,#type) <> 0 THEN REPLACE(MID(ctv,INSTR(ctv,#type)) ,' ','')
WHEN INSTR(ctv,#type)=0 THEN REPLACE(ctv,' ','') END AS gctv
FROM
(SELECT doc_id ,
GROUP_CONCAT(tv ORDER BY val ASC) AS ctv
FROM
(SELECT doc_id,val,
CONCAT_WS(' ',TYPE,VAL) tv
FROM doc_val
WHERE criteria='L'
)A
GROUP BY doc_id
)B, (SELECT #type:='D') temp
This is where the type condition is used and instead of typing one by one (which what I've done earlier), I use a variable so if the type condition is no longer 'D', you only need to change it from the variable. You will see more operators is used here.
INSTR is to find whether in the 'ctv' column has the #type variable which was set to 'D' or not. This will return the starting position of 'D'. For example, in the second image, the first result is [H 80,D 100,D 101] so the operator INSTR will look-up the position of first 'D' occurrence which will return 6 (counting from left to right including spaces and comma). The second return 0 because it did not find any D inside the column. CASE WHEN will check if the value=0 then it will return the value in the column as is, if the value <> 0, it will return value based on the position extracted from INSTR(ctv,#type). That is why I've added another operator to get the column value from the position (MID ). I've included REPLACE to remove the spaces in between type and val. To understand the query more, I have prepared a query breakdown of the operation below:
SELECT doc_id,criteria,
INSTR(ctv,#type),
MID(ctv,INSTR(ctv,#type)),
REPLACE(MID(ctv,INSTR(ctv,#type)) ,' ',''),
CASE
WHEN INSTR(ctv,#type) <> 0
THEN REPLACE(MID(ctv,INSTR(ctv,#type)) ,' ','')
WHEN INSTR(ctv,#type)=0
THEN REPLACE(ctv,' ','')
END AS gctv,
SUBSTRING(REPLACE(ctv,' ',''),1)
FROM
(SELECT doc_id,criteria,
GROUP_CONCAT(tv ORDER BY val ASC) AS ctv
FROM
(SELECT doc_id,criteria,val,
CONCAT_WS(' ',TYPE,VAL) tv
FROM doc_val
WHERE criteria='L'
)A
GROUP BY doc_id
)B, (SELECT #type:='D') temp
Query above will return the following:
The last part here:
SELECT doc_id,
LEFT(gctv,1) AS 'Type',
SUBSTRING(SUBSTRING_INDEX(REPLACE(gctv,',',' '),' ',1),2) AS 'val'
the first operator of REPLACE is to change commas into spaces (refer gctv result in photo above). Then with SUBSTRING_INDEX, it takes the first 'type+val' in the column then SUBSTRING will return value from position 2 - which was from taken from val column (this is assuming that your type column only consists of single character).
Fiddle here: DB Fiddle
This should give you your desired outcome
See demo here
http://sqlfiddle.com/#!9/0e32ec/32
Select *
from(
SELECT a.id, a.rev, a.content, b.val, b.type, b.criteria
FROM `docs` a
JOIN `doc_val` b ON b.doc_id = a.id
WHERE a.`rev` = 1 and b.criteria = 'L'
Order by `a`.`id`, FIELD(b.type, 'D','H','A'), b.`val` ) as sub
GROUP BY `id`
OUTPUT:
id rev content val type criteria
1 1 The earth is flat 100 D L
2 1 One hundred angels can dance on the head of a pin 90 H L
3 1 The earth is flat and rests on a bull's horn 100 D L

Mysql query to pull data in the form of matrix

I have data in database with three columns id, topic, and subtopic. Something like this,
CREATE TABLE Table2 (`id` int, `topic` varchar(5), `subtopic` varchar(6));
INSERT INTO Table2 (`id`, `topic`, `subtopic`) VALUES
(1, 'place', 'paris'),
(1, 'group', 'A'),
(1, 'group', 'B'),
(2, 'place', 'us'),
(2, 'group', 'C'),
(3, 'group', 'A'),
(3, 'water', 'salt'),
(4, 'water', 'sweet'),
(4, 'world', 'ep'),
(5, 'place', 'venus'),
(5, 'place', 'paris'),
(5, 'group', 'A');
I want to output the result matrix place vs group from topic. Something like this.
Paris|US|Venus
A 2 |0 | 1
B 1 |0 | 0
C 0 |1 | 0
Idea is to pick up all the values of 'Group' (A,B,C) and 'Places' (paris,us,venus) in subtopic column. And then find number of co-occurrences with such condition.
Any idea how to solve in MySql?
You will need to join on your table twice to get the group and places, then you can use an aggregate function with a CASE expression to convert the rows into columns:
select g.subtopic as `group`,
sum(case when p.subtopic = 'Paris' then 1 else 0 end) Paris,
sum(case when p.subtopic = 'US' then 1 else 0 end) US,
sum(case when p.subtopic = 'Venus' then 1 else 0 end) Venus
from table2 g
left join table2 p
on g.id = p.id
and p.topic = 'place'
where g.topic = 'group'
group by g.subtopic;
See SQL Fiddle with Demo.
If you are going to have unknown values for the subtopic, then you can use a prepared statement and dynamic SQL:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'sum(case when p.subtopic = ''',
subtopic,
''' then 1 else 0 end) as `',
subtopic, '`')
) INTO #sql
FROM table2
where topic = 'place';
SET #sql = CONCAT('SELECT g.subtopic as `group`, ', #sql, '
from table2 g
left join table2 p
on g.id = p.id
and p.topic = ''place''
where g.topic = ''group''
group by g.subtopic');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo