How to use WHERE IN in subquery with value from string column? - mysql

I have a problem to use WHERE IN with a column value
I have data like below
Table Service
id name sd_ids
1 House Cleaning 1,2
Table Service Detail
id name
1 living room
2 bedroom
Schema
CREATE TABLE IF NOT EXISTS `service` (
`id` int(6) unsigned NOT NULL,
`name` varchar(200) NOT NULL,
`sd_ids` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `service` (`id`, `name`,`sd_ids`) VALUES
('1','House Cleaning','1,2');
CREATE TABLE IF NOT EXISTS `service_detail` (
`id` int(6) unsigned NOT NULL,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `service_detail` (`id`, `name`) VALUES
('1','living room'),
('2','bedroom');
I already tried this
SELECT *,
(
SELECT
GROUP_CONCAT(name)
from service_detail
where id in(service.sd_ids)
) as service_detail
FROM service;
but the result it's not what I want
id name sd_ids service_detail
1 House Cleaning 1,2 living
I put schema and my testing here
http://sqlfiddle.com/#!9/49c34e/7
What I want to achieve to shows the result like below
id name sd_ids service_detail
1 House Cleaning 1,2 living room,bedroom
I know this schema is not best practice, but I need suggestion to achieve that.
thanks

You may join using FIND_IN_SET in the join criteria:
SELECT
s.id,
s.name,
s.sd_ids,
GROUP_CONCAT(sd.name ORDER BY sd.id) AS service_detail
FROM service s
INNER JOIN service_detail sd
ON FIND_IN_SET(sd.id, s.sd_ids) > 0
GROUP BY
s.id,
s.name,
s.sd_ids;
Demo
In general you should avoid storing CSV data in your SQL tables. CSV data represents unnormalized data, and is difficult to work with (q.v. the above query). Instead, break every service ID onto a separate record for best results.

Related

How to select columns from one table where id in JSON column of other table

I have two tables topics and assessments the topics table is a basic table with id and name and in assessment table there is a JSON column called topic_list where array of topics is stored. I need the list of topics for a specific assessment with id and name.
CREATE TABLE `topics` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT 'topic name will capture here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1
CREATE TABLE `assessments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`topic_list` json DEFAULT NULL,
`assessment_name` varchar(150) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1
INSERT INTO `topics` (name) values
('topic1'),
('topic2'),
('topic3');
INSERT INTO `assessments` (topic_list, assessment_name) values
('[1,2]','assessment1'),
('[2,3]', 'assessment2'),
('[1,3]', 'assessment3');
SELECT t1.id,t1.name FROM topics AS t1
WHERE id IN (SELECT topic_list FROM assessments WHERE id = 1)
This is what I have tried but just getting first row:
SELECT id,name from topics where id in(select JSON_UNQUOTE(TRIM(TRAILING ']' FROM TRIM(LEADING '[' FROM JSON_EXTRACT(topic_list, '$[*]')))) from assessments where id = 1)
What I got is:
1 topic1
Results just first column from the array.
I was trying for:
1 topic1
2 topic2
You can use one of the following solutions using JSON_EXTRACT and JSON_CONTAINS:
SELECT id, name
FROM topics WHERE (
SELECT JSON_CONTAINS(
JSON_EXTRACT(topic_list, '$[*]'),
CONVERT(topics.id, JSON)
) FROM assessments WHERE id = 1
);
... or (with INNER JOIN):
SELECT topics.id, topics.name
FROM topics INNER JOIN assessments
ON JSON_CONTAINS(JSON_EXTRACT(topic_list, '$[*]'), CONVERT(topics.id, JSON)) = 1
WHERE assessments.id = 1
demo on dbfiddle.uk
Note: A faster solution can be an additional table with the mapping between topics and assessments instead of saving the topics list in the assessment record itself.

SUMmarize 3 columns from 2 different table in SQL

I have 3 database table. The one I store sell data like invoice number, due date, issue date. The second table I have the sell record id stored from previous table, there I store the ammount the person have payed (something like the customer payed only a half of the total price). In the third table I store the products and every row has the sell record id for backreference, in this table I have quantity and subtotal (quantity * price).
What I need is select all the record from first table and based on the sell_id I have to summarize how much they payed from the second table, summarize how many products one purchased and how much does it cost.
I need a little guidance to the right solution because I am stuck.
So far I came to this uggly sollution:
SELECT
(SELECT SUM(payment.sellpayment_amount) FROM es_sellpayment payment WHERE sell.sell_id = payment.sell_id) AS payed,
(SELECT SUM(product.sellproduct_quantity) FROM es_sellproduct product WHERE sell.sell_id = product.sell_id) AS quantity,
(SELECT SUM(product.sellproduct_total) FROM es_sellproduct product WHERE sell.sell_id = product.sell_id) AS total
FROM es_sell sell
It works I get the correct result, but I am unsure/unaware how this will impact later on the performance of the website. I tried the classic left join which gave me wrong results. I am also thinking storing permanently these 3 values on the main table; but this would be the lest preferable method because I had to make at lest 2 seperate SQL queries to retrieve the records.
CREATE TABLE `es_sell` (
`sell_id` int(11) NOT NULL,
`sell_invoice` int(11) NOT NULL,
`sell_note` text COLLATE utf8mb4_unicode_ci NOT NULL,
`sell_deliver` datetime NOT NULL,
`sell_issued` datetime NOT NULL,
`sell_due` datetime NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `es_sellproduct` (
`sellproduct_id` int(11) NOT NULL,
`product_id` int(11) NOT NULL,
`sellproduct_quantity` int(11) NOT NULL,
`sellproduct_price` int(11) NOT NULL,
`pricetype_id` int(11) NOT NULL,
`sellproduct_total` decimal(11,3) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `es_sellpayment` (
`sellpayment_id` int(11) NOT NULL,
`sellpayment_amount` decimal(11,3) NOT NULL,
`sell_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Use sub-queries to aggregate the associated tables to ensure that you're only ever joining one row to one row...
SELECT
sell.sell_id,
payment.payed,
product.quantity,
product.total
FROM
es_sell sell
LEFT JOIN
(
SELECT
sell_id,
SUM(sellpayment_amount) AS payed
FROM
es_sellpayment payment
GROUP BY
sell_id
)
payment
ON payment.sell_id = sell.sell_id
LEFT JOIN
(
SELECT
sell_id,
SUM(sellproduct_quantity) AS quantity,
SUM(sellproduct_total ) AS total
FROM
es_sellproduct
GROUP BY
sell_id
)
product
ON product.sell_id = sell.sell_id
If sell_id isn't unique in the sell table, you can aggregate again in the outer query.

How to fetch a record containing something if Count(*)=0

I have a set of tables containing staff memebers' secondary specialities in different formats. Actually, only few primary specialities allow a person to have a secondary speciality.
I make a UNION view, combining the secondary specialities in this way and it is working fine:
SELECT tpl.id, tpl.speciality, 20 as gr FROM tpath_list tpl
UNION ALL
SELECT tlt.id, tlt.lab_type as speciality, 11 as gr FROM tlab_types tlt
UNION ALL
SELECT til.id, til.speciality, 10 as gr FROM tinstrumental_list til
Field gr contains the index field of the primary specialities (10, 11 and 20) allowing to have a secondary speciality. All other specialities (with id 1, 2, 3 etc.) do not have secondary ones.
I receive something like this...
I can now fetch data from the created view using WHERE gr=:n.
How can I modify the view so that fetching data from it using WHERE gr=:n (1, 2, 3 etc.) clause will give me a single record id=1 speciality="Not specified" gr=n
Appended on comments of the community:
I need to fetch all the records if :n IN(10,11,20) that are present in the tables and presented by the view. For example, 3 records listed in the picture for :n=20. Only if :n NOT IN(10,11,20) I need an additional (single) record absenting from the tables (and the view). Please, note that records with gr IN(10,11,20) have got the id=1 and speciality='Not specified'.
Appended on request of Jon Tofte-Hansen
Here is an unfiltered view output (without WHERE clause).
If I fetch the view with WHERE gr=20 I get the following (that is what I want):
If with WHERE gr=10 - this set (that is what I want):
If I fetch it with WHERE gr=1 then I woud like to receive a sigle record that is absent in any of the tables used for view creation. I wish to get this output but do not know how to:
Here is the structure of the tables if it might help:
DROP TABLE IF EXISTS `tspecialities_list`;
CREATE TABLE `tspecialities_list` (
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`speciality` varchar(30) NOT NULL DEFAULT '',
PRIMARY KEY (`id`,`speciality`),
KEY `i_by_speciality` (`speciality`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tlab_types`;
CREATE TABLE `tlab_types` (
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`lab_type` varchar(255) DEFAULT NULL,
`deleted` smallint(5) unsigned DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tpath_list`;
CREATE TABLE `tpath_list` (
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`speciality` varchar(30) NOT NULL DEFAULT '',
PRIMARY KEY (`id`,`speciality`),
KEY `i_by_speciality` (`speciality`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tinstrumental_list`;
CREATE TABLE `tinstrumental_list` (
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`speciality` varchar(30) NOT NULL DEFAULT '',
PRIMARY KEY (`id`,`speciality`),
KEY `i_by_speciality` (`speciality`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
It is not entirely clear what you want, but this could be it. Add an extra union that produce all the id's not present in the three tables with secondary specialities:
SELECT tpl.id, tpl.speciality, 20 as gr FROM tpath_list tpl
UNION ALL
SELECT tlt.id, tlt.lab_type as speciality, 11 as gr FROM tlab_types tlt
UNION ALL
SELECT til.id, til.speciality, 10 as gr FROM tinstrumental_list til
UNION ALL
select tsl.id, 'Not specified', tsl.id as gr from tspecialities_list tsl
where tsl.id not in
(SELECT tpl.id FROM tpath_list tpl
UNION
SELECT tlt.id FROM tlab_types tlt
UNION
SELECT til.id FROM tinstrumental_list til)
It seems like you are just asking for the following:
SELECT *
FROM viewname
WHERE speciality='Not specified' and gr=:n
Is there a reason this isn't what you want?

Can I improve my movie selecting SQL query

I've created a database to store movies data. My tables are the following:
movies:
CREATE TABLE IF NOT EXISTS `movies` (
`movieId` int(11) NOT NULL AUTO_INCREMENT,
`imdbId` varchar(255) DEFAULT NULL,
`imdbRating` float DEFAULT NULL,
`movieTitle` varchar(255) NOT NULL,
`movieLength` varchar(255) NOT NULL,
`imdbRatingCount` varchar(255) NOT NULL,
`poster` varchar(255) NOT NULL,
`year` varchar(255) NOT NULL,
PRIMARY KEY (`movieId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
I have a table in which i store movie actors:
CREATE TABLE IF NOT EXISTS `actors` (
`actorId` int(10) NOT NULL AUTO_INCREMENT,
`actorName` varchar(255) NOT NULL,
PRIMARY KEY (`actorId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
And one other in which i store the relation between the movies and actors: (movieActor)
CREATE TABLE IF NOT EXISTS `movieActor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`movieId` int(10) NOT NULL,
`actorId` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Now when i want to select a list of movies in which are the selected actors my query is:
SELECT *
FROM movies m inner join
(SELECT movieId FROM movieActor WHERE actorId IN(1,2,3) GROUP BY movieId having count(*) = 3) ma ON m.movieId = ma.movieId
WHERE imdbRating IS NOT NULL ORDER BY imdbRating DESC
This is working perfectly, but i don't know that this is the optimal table structure and query to accomplish this. Are there any better table structure to store data or query the list?
First of all, use indexes on your tables. In my opinion it should be useful to have 3 indexes on movieActor. MovieId - ActorID - MovieIdActorId.
Second try tu use foreign keys. These help to identify the best execution plan for your dbs.
Third try to avoid generating temp tables in your execution plan of your query. Subselects often creates temp tables which are used when the database has to temporarily save something in the RAM. To check this, write EXPLAIN in front of goer query.
I would write it like this:
SELECT m.*, movieActor
FROM movies m inner join
movieActor ma ON m.movieId = ma.movieId
WHERE imdbRating IS NOT NULL
and actorId IN(1,2,3)
GROUP BY movieId
having count(*) = 3)
ORDER BY imdbRating DESC
(Not tested)
Just try to optimize it with the EXPLAIN keyword. It also can help you to create the right indexes.

SQL select entries in other table linked by foreign keys

I have redesigned my database structure to use PRIMARY and FOREIGN KEYs to link the entries in my 3 tables together, and I am having problems trying to write queries to select data in one table given data in a another table. Here is an example of my 3 CREATE TABLE statements:
CREATE TABLE IF NOT EXISTS players (
id INT(10) NOT NULL AUTO_INCREMENT,
username VARCHAR(16) NOT NULL,
uuid VARCHAR(200) NOT NULL DEFAULT 0,
joined TIMESTAMP DEFAULT 0,
last_seen TIMESTAMP DEFAULT 0,
PRIMARY KEY (id)
);
/* ^
One |
To
| One
v
*/
CREATE TABLE IF NOT EXISTS accounts (
id INT(10) NOT NULL AUTO_INCREMENT,
account_id INT(10) NOT NULL,
pass_hash VARCHAR(200) NOT NULL,
pass_salt VARCHAR(200) NOT NULL,
created BIGINT DEFAULT 0,
last_log_on BIGINT DEFAULT 0,
PRIMARY KEY (id),
FOREIGN KEY (account_id) REFERENCES players(id) ON DELETE CASCADE
) ENGINE=InnoDB;
/* ^
One |
To
| Many
v
*/
CREATE TABLE IF NOT EXISTS purchases (
id INT(10) NOT NULL AUTO_INCREMENT,
account_id INT(10) NOT NULL,
status VARCHAR(20) NOT NULL,
item INT NOT NULL,
price DOUBLE DEFAULT 0,
description VARCHAR(200) NOT NULL,
buyer_name VARCHAR(200) NOT NULL,
buyer_email VARCHAR(200) NOT NULL,
transaction_id VARCHAR(200) NOT NULL,
payment_type VARCHAR(20) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (account_id) REFERENCES accounts(account_id) ON DELETE CASCADE
) ENGINE=InnoDB;
Say for example, I want to select all the usernames of users who purchased anything greater than $30. All the usernames are stored in the players table, which is linked to the accounts table and that is linked to the purchases table. Is this this the best way to design this relational database? If so, how would I run queries similar to the above example?
I was able to get get all of a users purchase history given their username, but I did it with 2 sub-queries... Getting that data should be easier than that!
Here is the SELECT query I ran to get all of a players purchase data:
SELECT *
FROM purchases
WHERE account_id = (SELECT id FROM accounts WHERE account_id = (SELECT id FROM players WHERE username = 'username'));
Also, when I try to make references to the other tables using something like 'players.username', I get an error saying that the column doesn't exist...
I appreciate any help! Thanks!
Your design is ok in my opinion. The relation between players and account is one-to-many and not one-to-one since this way, you can have two tuples referencing a single player.
I would write the query you need as:
SELECT DISTINCT p.id, p.username
FROM players p INNER JOIN accounts a ON (p.id = a.account_id)
INNER JOIN purchases pc ON (a.id = pc.account_id)
WHERE (pc.price > 30);
As Sam suggested, I added DISTINCT to avoid repeating id and username in case a user have multiple purchases.
Note the id is here to avoid confusion among repeated usernames.