Related
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").
We have a little simulator of a tour-operator DB (MYSQL) and we are asked to get a Query that gives us the weighted avg of duration of the tours that we have.
https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
Using subquery I got to this point where I have the days that each tour lasts and the weight of each tour from the total of tours, but I am stuck and don't know how to get the weighted avg from here. I know I have to use another select from the result I already got but I would appreciate some help.
SQLfiddle down here:
http://sqlfiddle.com/#!9/53d80/2
Tables and data
CREATE TABLE STAGE
(
ID INT AUTO_INCREMENT NOT NULL,
TOUR INT NOT NULL,
TYPE INT NOT NULL,
CITY INT NOT NULL,
DAYS INT NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE TOUR
(
ID INT AUTO_INCREMENT NOT NULL,
DESCRIPTION VARCHAR(255) CHARACTER SET UTF8 COLLATE UTF8_UNICODE_CI
NOT NULL,
STARTED_ON DATE NOT NULL,
TYPE INT NOT NULL,
PRIMARY KEY (ID)
);
INSERT INTO TOUR (DESCRIPTION, STARTED_ON, TYPE) VALUES
('Mediterranian Cruise','2018-01-01',3),
('Trip to Nepal','2017-12-01',1),
('Tour in Nova York','2015-04-24',5),
('A week at the Amazones','2014-09-11',2),
('Visiting the Machu Picchu','2013-02-19',4);
INSERT INTO STAGE (TOUR, TYPE, CITY, DAYS) VALUES
(1, 1, 38254, 1),
(1, 2, 22460, 3),
(1, 2, 47940, 3),
(1, 2, 42600, 4),
(1, 3, 38254, 1),
(2, 1, 13097, 1),
(2, 2, 29785, 5),
(2, 3, 13097, 1),
(3, 1, 788, 2); ,
(3, 2, 48019, 6),
(3, 3, 788, 1),
(4, 1, 38254, 2),
(4, 2, 8703, 3);,
(4, 3, 38254, 4),
(5, 1, 10453, 1),
(5, 2, 32045, 5),
(5, 3, 10453, 2);
Query:
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
weigthed avg would be:
(1×7+1×8+2×9+1×12) / (1+1+2+1) = 9
Wheighted AVG can be calculated with SUM(value * wheight) / SUM(wheight). In your case:
SELECT SUM(AVERAGE_DAYS * WEIGHT) / SUM(WEIGHT)
FROM (
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
) sub
http://sqlfiddle.com/#!9/53d80/4
I'm not 100% sure, but it looks like the following query is doing exactly the same:
SELECT AVG(TOUR_DAYS)
FROM (
SELECT TOUR, SUM(DAYS) AS TOUR_DAYS
FROM STAGE
GROUP BY TOUR
) sub;
Or even without any subqueries:
SELECT SUM(DAYS) / COUNT(DISTINCT TOUR)
FROM STAGE;
That would mean, the requirement should be simplified to "Get average number of days per tour".
I am having trouble creating a query for an SQL table. The query I am trying to create shows the number of products within the category of "clothes" and does not show accessories for example a list of products that are entered as T-shirts or sweatshirts.
Here is the tables that have been created:
CREATE DATABASE IF NOT EXISTS product_list;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS product_categories;
DROP TABLE IF EXISTS categories;
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(50) DEFAULT NULL,
active BOOL DEFAULT NULL
);
CREATE TABLE categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
structure VARCHAR(50)
);
CREATE TABLE product_categories (
product_id INT,
category_id INT,
PRIMARY KEY(product_id, category_id)
);
INSERT INTO products VALUES
(NULL, "Blue Sweatshirt", false),
(NULL, "Short Sleeve T-Shirt", true),
(NULL, "White Vest", true),
(NULL, "Black Hairclip", true),
(NULL, "Knitted Hat", false),
(NULL, "Grey Sweatshirt", true),
(NULL, "Tartan Scarf", true);
INSERT INTO categories VALUES
(NULL, "Sweatshirts", "Clothes>Sweatshirts"),
(NULL, "T-Shirts", "Clothes>T-Shirts"),
(NULL, "Accessories", "Accessories"),
(NULL, "Winter", "Clothes>Winter"),
(NULL, "Vests", "Clothes>Vests");
INSERT INTO product_categories VALUES
(1, 1), (2, 2), (3, 5), (3, 4), (4, 3), (5, 3), (5, 4), (6, 1), (7, 3), (7, 4);
If I understand correctly, this is a set-within-sets query. You are looking for products that have at least one "clothes" category, and none of the categories are not clothes. I approach this using group by and having because it is quite flexible:
select pc.product_id
from Product_categories pc join
categories c
on pc.category_id = c.category_id
group by pc.product_id
having sum(case when c.structure like 'Clothes%' then 1 else 0 end) > 0 and
sum(case when c.structure not like 'Clothes%' then 1 else 0 end) = 0;
If you just want the count, then you can use this as a subquery and use count(*).
EDIT:
A small note. The question is now tagged with MySQL, which has convenient short-hand for the having clause:
having sum(c.structure like 'Clothes%') > 0 and
sum(c.structure not like 'Clothes%') = 0;
Try this query
select * from products a
join Product_categories b on a.product_id=b.product_id
join categories c on b.category_id=b.category_id
where c.name like '%Clothes%'
I'm working on a practice problem with DDL as follows:
CREATE TABLE people (
id SMALLINT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(50),
last_name VARCHAR(50),
PRIMARY KEY (id)
)
;
CREATE TABLE cd (
id SMALLINT NOT NULL AUTO_INCREMENT,
artist VARCHAR(50),
title VARCHAR(50),
PRIMARY KEY(id),
owner SMALLINT,
FOREIGN KEY (owner) REFERENCES people(id)
)
;
CREATE TABLE lend (
id SMALLINT NOT NULL AUTO_INCREMENT,
cd_id SMALLINT,
lend_to SMALLINT,
FOREIGN KEY (lend_to) REFERENCES people(id),
FOREIGN KEY (cd_id) REFERENCES cd(id),
lend_date DATE DEFAULT '0000-00-00',
PRIMARY KEY(id)
)
;
INSERT INTO people (id, first_name, last_name) VALUES
(1, 'Brett', 'CEO'),
(2, 'Jeff', 'President'),
(3, 'Beta', 'Media'),
(4, 'Casey', 'Content')
;
INSERT INTO cd (id, artist, title, owner) VALUES
(1, 'The xx', 'Coexist', 2),
(2, 'ACDC', 'High Voltage', 1),
(3, 'Bjork', 'Cocoon', 3),
(4, 'Ella Fitzgerald', 'Ella Sings Gershwin', 4),
(5, 'Fever Ray', 'Live in Lulea', 2),
(6, 'Tom Waits', 'Rain Dogs', 4),
(7, 'Howlin Wolf', 'Smokestack Lightning', 1),
(8, 'Tupac', 'Poetic Justice', 4)
;
INSERT INTO lend (id, cd_id, lend_to, lend_date) VALUES
(1, 2, 3, '2014/01/03'),
(2, 3, 1, '2014/04/02'),
(3, 7, 4, '2013/12/22'),
(4, 4, 2, '2014/01/03')
;
I want my query to show who the CD is lent to. I can get the ID from the lend table, but want to display the full name of the individual lending it from the people table. Do I need to rework the design of how the lend table connects to the people table, or just use some sort of case function in the query? Below is my query so-far where I'm getting the l.lent_to and want to be showing the CONCAT(p.first_name, ' ', p.last_name) who the CD is lent to.
SELECT /*cd.id,*/
CONCAT(p.first_name, ' ', p.last_name) 'CD OWNER',
cd.title,
l.lend_to,
p.id ,
(
CASE
WHEN l.lend_to IS NULL
THEN 'Not Lent'
ELSE DATE_FORMAT(l.lend_date, '%m-%d-%Y')
END
) 'LEND DATE',
(
CASE
WHEN l.lend_to IS NULL
THEN 'Not Lent'
ELSE TIMESTAMPDIFF(day, l.lend_date, NOW())
END
) 'DAYS LENT'
FROM
people p
LEFT JOIN cd cd
ON p.id = cd.owner
LEFT JOIN lend l
ON cd.id = l.cd_id
LEFT JOIN lend l1
on p.id = l1.lend_to
;
See if this query gives you the basic information you are looking for
select c.title as 'Title', c.artist as 'Artist', o.first_name as 'Owner',
l.lend_date as 'Lend Date', p.first_name as 'Lender'
from cd c
left outer join people o on c.owner = o.id
left outer join lend l on c.id = l.cd_id
left outer join people p on l.lend_to = p.id
You can add additional switch logic to refine the result, if this is what you are looking for.
I've resolved the issue with a data architecture redesign. Take a look if interested.
http://sqlfiddle.com/#!2/b6158/3
I maybe ask a relatively simple question. But I cannot find a solution to this. It's a matter of two tables MANY TO MANY, so there's a third table between them. The schema below:
CREATE TABLE `options` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `options` (`id`, `name`) VALUES
(1, 'something'),
(2, 'thing'),
(3, 'some option'),
(4, 'other thing'),
(5, 'vacuity'),
(6, 'etc');
CREATE TABLE `person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `person` (`id`, `name`) VALUES
(1, 'ROBERT'),
(2, 'BOB'),
(3, 'FRANK'),
(4, 'JOHN'),
(5, 'PAULINE'),
(6, 'VERENA'),
(7, 'MARCEL'),
(8, 'PAULO'),
(9, 'SCHRODINGER');
CREATE TABLE `person_option_link` (
`person_id` int(11) NOT NULL,
`option_id` int(11) NOT NULL,
UNIQUE KEY `person_id` (`person_id`,`option_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `person_option_link` (`person_id`, `option_id`) VALUES
(1, 1),
(2, 1),
(2, 2),
(3, 2),
(3, 3),
(3, 4),
(3, 5),
(4, 1),
(4, 3),
(4, 6),
(5, 3),
(5, 4),
(5, 5),
(6, 1),
(7, 2),
(8, 3),
(9, 4)
(5, 6);
The idea is as follow: I would like to retrieve all people who have a link to option_id=1 AND option_id=3.
The expected result should be one person: John.
But I tried with something like that, which doesn't work because it returns also people who have 1 OR 3:
SELECT *
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
What is the best practice in this case?
//////// POST EDITED: I need to focus on an other important point ////////
And what if we add a new condition with NOT IN? like:
SELECT *
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 3, 4 )
AND l.option_id NOT IN ( 6 )
In this case, the result should be FRANK, because PAULINE who has also 3 and 4, have the option 6 and we don't want that.
Thanks!
This is a Relational Division Problem.
SELECT p.id, p.name
FROM person p
INNER JOIN person_option_link l
ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(*) = 2
SQLFiddle Demo
if a unique constraint was not enforce on option_id for every id, a DISTINCT keyword is required to filter unique option_ID
SELECT p.id, p.name
FROM person p
INNER JOIN person_option_link l
ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(DISTINCT l.option_id) = 2
SQL of Relational Division
Use GROUP BY and COUNT:
SELECT p.id, p.name
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(Distinct l.option_id) = 2
I prefer using COUNT DISTINCT in case you could have the same option id multiple times.
Good luck.
It may not be the best option, but you could use a 'double join' to the person_option_link table:
SELECT *
FROM person AS p
JOIN person_option_link AS l1 ON p.id = l1.person_id AND l1.option_id = 1
JOIN person_option_link AS l2 ON p.id = l2.person_id AND l2.option_id = 3
This ensures that there is simultaneously a row with option ID of 1 and another with option ID of 3 for the given user.
The GROUP BY alternatives certainly work; they might well be quicker too (but you'd need to scrutinize query plans to be sure). The GROUP BY alternatives scale better to handle more values: for example, a list of the users with option IDs 2, 3, 5, 7, 11, 13, 17, 19 is fiddly with this variant but the GROUP BY variants work without structural changes to the query. You can also use the GROUP BY variants to select users with at least 4 of the 8 values which is substantially infeasible using this technique.
Using the GROUP BY does require a slight restatement (or rethinking) of the query, though, to:
How can I select people who have 2 of the option IDs in the set {1, 3}?
How can I select people who have 8 of the option IDs in the set {2, 3, 5, 7, 11, 13, 17, 19}?
How can I select people who have at least 4 of the option IDs in the set {2, 3, 5, 7, 11, 13, 17, 19}?
For the "has not these ids" part of the question, simply add a WHERE clause:
WHERE person_id NOT IN
(
SELECT person_id
FROM person_option_link
WHERE option_id = 4
)