Mysql SELECT in SELECT - mysql

I have a table like this :
+--------+---------------------------------------+
| CandId | Speak |
+--------+---------------------------------------+
| 1 | English |
| 1 | Spanish |
| 2 | English |
| 2 | Spanish |
| 3 | Dutch |
| 3 | English |
| 4 | Dutch |
| 4 | Spanish |
| 4 | German |
+--------+---------------------------------------+
I'm trying to make a query that would for instance get the CandId of people that speaks English and Spanish (not or Spanish).
So in the specific case the query would show 1 and 2.
It's surely very easy but I even can't imagine how to do this.
Many thanks by advance for your help.

SELECT CandId
FROM Candidate
WHERE Speak in ('English','Spanish')
GROUP BY CandId
HAVING COUNT(CandId) = 2
It returns all CandId for which there are 2 records which match 'English' or 'Spanish'.
If you search for more languages also change in the HAVING clause the value of 2 in the number of languages.
UPDATED (thanks to spencer7593 for the valid comment):
if the combination CandId+Speak is not unique, one must use:
SELECT CandId
FROM Candidate
WHERE Speak in ('English','Spanish')
GROUP BY CandId
HAVING COUNT(DISTINCT Speak) = 2

Aggregation with a having clause is a very general way to answer these "set-within-a-set" questions. For both English and Spanish:
select candid
from t
group by candid
having sum(language = 'English') > 0 and
sum(language = 'Spanish') > 0;
If you don't want Spanish:
having sum(language = 'English') > 0 and
sum(language = 'Spanish') = 0;
Nuahatl and Amdo but not Mandarin:
having sum(language = 'Nuahatl') > 0 and
sum(language = 'Amdo') > 0 and
sum(language = 'Mandarin') = 0;

The column name CandId suggests that you have a Cand table containing the people. You can then select from that table and look up the languages with IN or EXISTS:
select *
from cand
where candid in (select candid from cand_languages where speak = 'English')
and candid in (select candid from cand_languages where speak = 'Spanish')
order by candid;
With a large table, you would provide an index to get the lookup fast:
create index idx on cand_languages(speak, candid);

You can join the table on itself using the CandId:
SELECT
DISTINCT t1.CandId
FROM
`table` t1
JOIN `table` t2 ON t2.CandId = t1.CandId
WHERE
t1.Speak = 'English'
AND t2.Speak = 'Spanish';
See http://sqlfiddle.com/#!9/97635c/3 for a working example

You can use CASE+SUM to count language for every candidate and then select only having count=2:
select CandId
from
(
select CandId,
sum( case speak
when 'English' then 1
when 'Spanish' then 1
else 0
end) countlang
from Table
group by CandId
)x
where countlang=2

Related

How to get multiple counts with multiple joins using one SQL query?

There is an error in my query and I would like some help. I have three tables
Rooms{id,number,name,type(ECO/LUX),active(0/1)}
Men{passport,roomid,status(YOUTH/ADULT)}
Women{passport,roomid,status(YOUTH/ADULT)}
**In each room there can be more than one woman or man.
I want to count how many women and men have the same room with roomid in (1,2,3), status='ADULT', type='LUX' and active=1. Therefore I need a result like this:
+----+--------+-----------+----------+------------+
| id | number | name | CountMen | CountWomen |
+----+--------+-----------+----------+------------+
| 1 | 23 | 1st suite | 2 | 4 |
| 3 | 4 | 2nd suite | 1 | 2 |
+----+--------+-----------+----------+------------+
SELECT id,number,name,
sum(case when Men.status='ADULT' then 1 else 0 end) as CountMen,
sum(case when Women.status='ADULT' then 1 else 0 end) as CountWomen
FROM Rooms left join Men
on Rooms.id=Men.roomid
left join Women on Room.id=Women.roomid where
(type='LUX') and (active=true) and (id in (1,2,3))
group by id;
The problem is that I get sometimes wrong results in the counters.
In a left join, conditions on the second table need to be in the on clause. It would help if you qualified all column names in the query.
However your problem is because you are getting a Cartesian product between the gender tables. This is definitely a case where gender segregation is not a good thing. You should have just one table for people (and this doesn't even bring up other issues with defining binary genders).
SELECT r.id, r.number, r.name,
(SELECT COUNT(*)
FROM men m
WHERE m.status = 'ADULT' AND r.id = m.roomid
) as CountMen,
(SELECT COUNT(*)
FROM women w
WHERE w.status = 'ADULT' AND r.id = w.roomid
) as CountWomen
FROM Rooms r
WHERE r.type = 'LUX' AND r.active = true AND r.id IN (1, 2, 3);
However, you should fix your data model so you have people rather than segregated gender tables.

Mysql join table result as one row

I have two tables
prj
id | ptitle
1 | prj111
2 | prj222
prjflow
id | pid | paction | pactiontxt
1 | 1 | 1 | man1
2 | 1 | 1 | man2
3 | 1 | 2 | woman1
4 | 1 | 1 | man3
i want this output:
output
ptitle | men | women
prj111 | man1,men3 | woman1
i write this query:
SELECT prj.ptitle
, GROUP_CONCAT(pflow1.pactiontxt) men
, GROUP_CONCAT(pflow2.pactiontxt) women
FROM prj
JOIN prjflow pflow1
ON prj.id = pflow1.pid
AND pflow1.paction = 1
JOIN prjflow pflow2
ON prj.id = pflow2.pid
AND pflow2.paction = 2;
but output is:
ptitle | men | women
prj111 | man1,men3 | woman1,woman1
My Query when the number of rows of men and women have been equal, working properly
but
i want that works at any case.
thanks a lot
and excuse me for poor english writing
Just use conditional aggregation:
SELECT prj.ptitle,
GROUP_CONCAT(CASE WHEN prjflow.paction = 1 THEN prjflow.pactiontext END ORDER BY prjflow.id) as men,
GROUP_CONCAT(CASE WHEN prjflow.paction = 2 THEN prjflow.pactiontext END ORDER BY prjflow.id) as women
FROM prj JOIN
prjflow
ON prj.id = prjflow.pid
GROUP BY prj.ptitle;
This will also fix two potential problems with your query. The first is performance. If some of the titles have large numbers of men and women, then the query has to process a cartesian product. The second is semantic. If some titles have men but not women or women without men, then the two joins will filter them out.
Here is a SQL Fiddle demonstrating it.
Do note that the suggested output seems inconsistent with the input data. This produces the output:
ptitle | men | women
prj111 | man1,man2,men3 | woman1
I see no reasonable way to exclude man2 from the list, so I assume that is a typo.
The 'quick fix' would be to use distinct in the group_concat aggregation:
SELECT
prj.ptitle,
group_concat(distinct pflow1.pactiontxt) AS men,
group_concat(distinct pflow2.pactiontxt) AS women
FROM prj
JOIN prjflow AS pflow1 ON (prj.id = pflow1.pid AND pflow1.paction = 1)
JOIN prjflow AS pflow2 ON (prj.id = pflow2.pid AND pflow2.paction = 2)
GROUP BY
prj.ptitle -- Officially you'll need this as well, although MySQL is quite forgiving.
But the query still uses two joins, and will even fail if there a no men or no women for a particular project.
So to solve that, you can write the query with subqueries. This way, you won't need joins distinct or group by, and the query will work too when an item has no men or no women at all.
SELECT
prj.ptitle,
( SELECT group_concat(pflow1.pactiontxt) FROM prjflo pflow1
WHERE prj.id = pflow1.pid AND pflow1.paction = 1) AS men,
( SELECT group_concat(pflow2.pactiontxt) FROM prjflo pflow2
WHERE prj.id = pflow2.pid AND pflow2.paction = 2) AS women
FROM prj

avoid select with subselect

I have a table with people and language this person knows. For ex
Name Language
John Engl ish
Bill English
John German
Bill Japanese
Li Chinese
I want to select all people knowing English and German languages.
The simple way is to do it:
select name from persons p where
exists (select 1
from persons pp
where pp.name=p.name
and pp.language="English")
AND
exists (select 1 from persons pp
where pp.name=p.name
and pp.language="English")
Complexity of request is n^2;
But, what if I need to select all persons knowing English, German and Russian? I'll have complexity n^3. And so on..
Is there any faster way to do it?
You want the names of people that speak both English and Japanese; not the name of people that either English or Japanese, correct? If so, here's a way of doing it without any joins or subqueries:
select name, count(name)
from persons
where language in ('English', 'Japanese')
group by name
having count(name)=2
If you need to add more languages, just add the additional languages to the where clause, and increase the number in the last line to the number of languages that you have.
Try this:
select name from persons p where p.language in ('English', 'German', 'Russian');
Try this one select
Select name from (
Select name, GROUP_CONCAT(DISTINCT language
ORDER BY language ASC SEPARATOR ' ') as gr from persons group by name) as t
WHERE gr = 'English Russian';
But this one will work for exact matches. You can use INSTR mysql function for searching in more languages.
However, my main advice is to create another structue because you have got many-to-many relations.
Revise Table Structure to:
people
person_id | name
----------+------
1 | John
2 | Bill
3 | Li
languages
language_id | language
------------+---------
1 | English
2 | German
3 | Japanese
4 | Chinese
people_have_languages
person_id | language_id
----------+------------
1 | 1
2 | 1
1 | 2
2 | 3
3 | 4
Now you that you would have a normalized table structure, here would be your query:
SELECT
`people`.`name`
FROM
`people`
INNER JOIN `people_have_languages` ON (`people`.`person_id`=`people_have_languages`.`person_id`)
INNER JOIN `languages` ON (`people_have_languages`.`language_id`=`languages`.`language`)
WHERE
`language` IN ('English', 'German', 'Russian')
GROUP BY
`people`.`person_id`

MySql Impass - can't move forward

I currently have this sql statement that I wrote and it works but it's not quite what I want. I've been working on it for hours but can't seem to get any further.
select parent.id as parent_id, parent.subject as parent,s.id,s.subject from (select s.id, s.subject from subjects s where parent_id = 0) parent join subjects s on parent.id = s.parent_id order by parent.subject, s.subject
It's grabbing all the subjects and ordering correctly but I also want to return the parent subject (parent_id = 0) at the top of each grouping. This is because some parents may not have subjects underneath but I still need to return them. Also the ordering is off when I try to do it as I want the parent first then it's child subjects. Hope that makes sense but if not just ask.
Any help would be appreciated.
Thanks
Steve
You're talking about grouping sets of rows by their parent rows.
The only way I know how to do this in MySQL is using the GROUP_CONCAT() function which won't group the subjects by row, but rather create a grouped string.
Here's what you can do:
SELECT
a.id,
a.subject,
GROUP_CONCAT(CONCAT(b.id, ':::', b.subject) ORDER BY b.subject SEPARATOR '|||') AS subjectlist
FROM
subjects a
LEFT JOIN
subjects b ON a.id = b.parent_id
WHERE
a.parent_id = 0
GROUP BY
a.id,
a.subject
ORDER BY
a.subject
So this will give you a result set like:
id | subject | subjectlist
---------------------------------------------------------------------
2 | subj1 | 23:::childsubj1|||28:::childsubj4
3 | subj2 | 18:::childsubj8|||55:::childsubj16
4 | subj3 | NULL
Depending on what language you are using in your application, you may be able to "explode" the subjects string into arrays delimited first by ||| which separates each subject, then ::: which separates that subject's ID and name.
Obviously, the downside of this is you have to make sure that your child subject name does not contain either ||| or ::: or whichever delimiters you decide to use.
Edit: Experimentally, I came up with this alternative solution which may be closer to what you're looking for:
Try:
SELECT
c.subj,
c.id
FROM
(
SELECT
CONCAT('---> ', b.subject) AS subj,
b.id,
CONCAT(a.subject, b.subject) AS orderfactor
FROM
subjects a
INNER JOIN
subjects b ON a.id = b.parent_id
WHERE
a.parent_id = 0
UNION ALL
SELECT
subject AS subj,
id,
subject AS orderfactor
FROM
subjects
WHERE
parent_id = 0
) c
ORDER BY
c.orderfactor
This query should give you a result along the lines of:
subject | id |
----------------------------------------------------------
subj1 | 2 |
---> childsubj1 | 23 |
---> childsubj4 | 28 |
subj2 | 3 |
---> childsubj8 | 18 |
---> childsubj16 | 55 |
subj3 | 4 |
subj4 | 5 |
---> childsubj10 | 79 |

SQL - match records from one table to another table based on several columns

I have two tables:
tblhobby
+-------+-------+-------+-------+
| name |hobby1 |hobby2 |hobby3 |
+-------+-------+-------+-------+
| kris | ball | swim | dance |
| james | eat | sing | sleep |
| amy | swim | eat | watch |
+-------+-------+-------+-------+
tblavailable_hobby
+----------------+
| available_hobby|
+----------------+
| ball |
| dance |
| swim |
| eat |
| watch |
+----------------+
the sql query should take all the columns in tblhobby and match it with tblavailable_hobby. If all the hobbies match to the available_hobby, then the person is selected
the query should produce
+--------+
| name |
+--------+
| kris |
| amy |
+--------+
Please help
Thanks for the answers. I have inherited this database and not able to normalize it at the moment. however, I would like to add another twist to the question. Suppose:
+-------+-------+-------+-------+
| name |hobby1 |hobby2 |hobby3 |
+-------+-------+-------+-------+
| kris | ball | swim | dance |
| james | eat | sing | sleep |
| amy | swim | eat | watch |
| brad | ball | | dance |
+-------+-------+-------+-------+
I would like to get
+--------+
| name |
+--------+
| kris |
| amy |
| brad |
+--------+
how would i go about with it?
Poor DB design, but, assuming you have to live with it:
SELECT h.name
FROM tblhobby h
INNER JOIN tblavailable_hobby ah1
ON h.hobby1 = ah1.available_hobby
INNER JOIN tblavailable_hobby ah2
ON h.hobby2 = ah2.available_hobby
INNER JOIN tblavailable_hobby ah3
ON h.hobby3 = ah3.available_hobby
EDIT: Answering the twist proposed in the comments below.
SELECT h.name
FROM tblhobby h
LEFT JOIN tblavailable_hobby ah1
ON h.hobby1 = ah1.available_hobby
LEFT JOIN tblavailable_hobby ah2
ON h.hobby2 = ah2.available_hobby
LEFT JOIN tblavailable_hobby ah3
ON h.hobby3 = ah3.available_hobby
WHERE (h.hobby1 IS NULL OR ah1.available_hobby IS NOT NULL)
AND (h.hobby2 IS NULL OR ah2.available_hobby IS NOT NULL)
AND (h.hobby3 IS NULL OR ah3.available_hobby IS NOT NULL)
I know this doesn't answer your question directly, and others have pointed out that your table design is problematic. What it should look like is this:
Table: Person
Id Name
-------------
1 Kris
2 James
3 Amy
table: PersonHobby (Join table)
PersonId HobbyId
----------------
1 1 -- Kris likes to ball
1 2 -- " dance
1 3 -- " swim
2 4 -- James likes to eat
Table: Hobby
Id Name
--------------
1 Ball
2 Dance
3 Swim
4 Eat
etc.
This design uses the concept of a Join or Junction table that allows you make many-to-many relationships between data. In this case people and hobbies.
You then query the data like this:
SELECT *
FROM Person p
JOIN PersonHobby AS ph on p.Id = ph.PersonId
JOIN Hobby AS h on h.Id = ph.HobbyId
WHERE ... -- filter as you need to
The PersonHobbies table in my example takes a table of Persons and a table of Hobbies and enables relationships between Persons and Hobbies. I know this will probably look like more work to you... extra tables, extra columns. But trust us, this design will make your life much simpler in the near future. In fact, you're already feeling the pain of your design by trying to figure out a query which should be much simpler than it is against your current db.
I would like to produce a WHERE filter to match your requirements but I don't quite understand what you're after. Could you explain in some more detail?
You can use a query to transform your existing table into a "virtual table", which I think should be easier to work with. Save this SQL statement as qryHobbiesUnion.
SELECT [name] AS person, hobby1 AS hobby
FROM tblhobby
WHERE (((hobby1) Is Not Null))
UNION
SELECT [name], hobby2
FROM tblhobby
WHERE (((hobby2) Is Not Null))
UNION
SELECT [name], hobby3
FROM tblhobby
WHERE (((hobby3) Is Not Null));
I enclosed "name" in square brackets because it's a reserved word. And I aliased [name] as person to avoid problems with square brackets when using qryHobbiesUnion in a subquery later.
I assumed any "empty" values for hobby will be Null. If blanks could also be empty strings (""), change the WHERE clauses to a pattern like this:
WHERE Len(hobby1 & "") > 0
After you determine which version of the WHERE clause returns the correct rows, save the query and use it in another query.
SELECT sub.person
FROM
[SELECT qh.person, qh.hobby, ah.available_hobby
FROM
qryHobbiesUnion AS qh
LEFT JOIN tblavailable_hobby AS ah
ON qh.hobby = ah.available_hobby
]. AS sub
GROUP BY sub.person
HAVING (((Count(sub.hobby))=Count([sub].[available_hobby])));
Using your second set of sample data, that query returns the 3 person names you wanted: amy; brad; and kris.
If tblhobby contained a row for a person with all the hobby fields empty, this query would not include that person's name. That makes sense to me because it seems your intention is to identify the people whose hobby choices are all matched in tblavailable_hobby. So a person with no hobby selections has no matches. If you want different behavior, this will probably get uglier. :-)
Really you must learn more about relational databases. Your design isn't good. You should have table with people and a table with hobbies. Then you should have a table the relates the two tables by an ID.
Your tables should look likes this
TABLE: People
COLUMNS: PID (INT, Primary Key), NAME
TABLE: Hobbies
COLUMNS: HID (INT, Primary Key), Hobby
TABLE: PeoplesHobbies
COLUMNS: ID, PID, HID
THEN your query would look something like this
select * from people `p` inner join PeoplesHobbies `ph` on p.PID = ph.PID inner join on Hobbies `h` on ph.HID = h.HID where p.NAME = 'JOHN'
SELECT name
FROM tblhobby AS h
WHERE EXISTS
( SELECT *
FROM tblavailable_hobby AS ah1
WHERE h.hobby1 = ah1.available_hobby
)
AND EXISTS
( SELECT *
FROM tblavailable_hobby AS ah2
WHERE h.hobby2 = ah2.available_hobby
)
AND EXISTS
( SELECT *
FROM tblavailable_hobby AS ah3
WHERE h.hobby3 = ah3.available_hobby
)
Borrowing from Joe's answer:
SELECT h.name
FROM tblhobby h
LEFT JOIN tblavailable_hobby ah1
ON h.hobby1 = ah1.available_hobby
LEFT JOIN tblavailable_hobby ah2
ON h.hobby2 = ah2.available_hobby
LEFT JOIN tblavailable_hobby ah3
ON h.hobby3 = ah3.available_hobby
WHERE (h.hobby1 IS NULL OR ah1.available_hobby IS NOT NULL)
AND (h.hobby2 IS NULL OR ah2.available_hobby IS NOT NULL)
AND (h.hobby3 IS NULL OR ah3.available_hobby IS NOT NULL)
ypercube's answer can be similarly extended.