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`
Related
I have two tables.
I am a total newbie to SQL. Using mysql at the moment.
I have the following setup for a school-related db:
Table A contains students records.
Student's id, password,name, lastname and so on.
Table B contains class attendancy records.
Each record goes like this: date, student id, grade
I need to gather all the student info of students that attended classes in a certain date range.
Now, the stupid way would be
first I SELECT all classes from Table B with DATE IN BETWEEN the range
then for each class, I get the student id and SELECT * FROM students WHERE id = student id
What I can't wrap my mind around is the smart way.
How to do this in one query only.
I am failing at understanding the concepts of JOIN, UNION and so on...
my best guess so far is
SELECT students.id, students.name, students.lastname
FROM students, classes
WHERE classes.date BETWEEN 20140101 AND 20150101
AND
classes.studentid = students.id
but is this the appropriate way for this case?
Dont add the join statement in the where clause. Do it like this:
SELECT s.id, s.name, s.lastname,c.date,c.grade
FROM classes c
inner join students s
on c.studentid=s.id
WHERE c.date BETWEEN '01/01/2014' AND '01/01/2015'
This sounds like an assignment so I will attempt to describe the problem and give a hint to the solution.
An example of a union would be;
SELECT students.name, students.lastname
FROM students
WHERE students.lastname IS NOT NULL
UNION
SELECT students.name, 'N/A'
FROM students
WHERE students.lastname IS NULL;
+--------------+--------------+
| name | lastname |
+--------------+--------------+
| John | Doe | <- First two rows came from first query
| Jill | Smith |
| Bill | N/A | <- This came from the second query
+--------------+--------------+
The usual use case for a union is to display the same columns, but munge the data in a different way - otherwise you can usually achieve similar results through a WHERE clause.
An example of a join would be;
SELECT authors.id, authors.name, books.title
FROM authors LEFT JOIN books ON authors.id = books.authors_id
+--------------+--------------+------------------+
| id | name | title |
+--------------+--------------+------------------+
| 1 | Mark Twain | Huckleberry Fin. |
| 2 | Terry Prat.. | Good Omens |
+--------------+--------------+------------------+
^ First two columns from ^ Third column appended
from authors table from books table linked
by "author id"
Think of a join as appending columns to your results, a union is appending rows with the same columns.
In your situation we can rule out a union as you don't want to append more student rows, you want class and student information side by side.
I have an SQL table setup similar to this:
name | subject |
-------+----------+
Harry | Painting |
Sandra | Soccer |
Sandra | English |
How can I write a select statement that merges the rows if they have multiple subject, so it would output a result like this:
name | subject 1 | subject 2 |
-------+------------+------------+
Harry | Painting | |
Sandra | Soccer | English |
You shouldn't. The best approach is to join the tables so you have:
Harry, Painting
Sandra, Soccer
Sandra, English
And then process these rows in your scripting language (PHP etc) to turn it into the hierarchical data structure you desire
In your example above, what would happen when there's 3 subjects per person, 10, 100, etc.
SQL only really works with two dimensional data. For three dimensions you either need to pre/post process as i've suggested, or move to something like NoSQL mongoDB that deals with structured objects instead of table rows.
Since you mentioned that the maximum number of subjects is only 2, you can therefore, generate a sequential number for each name and used that to pivot the columns.
SELECT Name,
MAX(CASE WHEN rn = 1 THEN Subject END) Subject1,
MAX(CASE WHEN rn = 2 THEN Subject END) Subject2
FROM
(
SELECT A.name,
A.subject,
(
SELECT COUNT(*)
FROM tableName c
WHERE c.name = a.name AND
c.subject <= a.subject) AS rn
FROM TableName a
) aa
GROUP BY Name
SQLFiddle Demo
The above is an SQL way.
You need a PIVOT routine. Serach for this in the engine of your choice.
Some RDBMSs have this built in. There is an alternative using a CASE statement in the SELECT clause, for which there are many blog posts out there.
You can accomplish this in one statement using a combination of substring_index() and group_concat() like this (SQLfiddle)
SELECT DISTINCT s.name,
substring_index(p.subject_list, ',', 1) AS "subject_1",
IF(instr(p.subject_list, ','),
substring_index(p.subject_list, ',', -1),
NULL
) AS "subject_2"
FROM subjects s
JOIN (SELECT name, GROUP_CONCAT(subject) AS "subject_list"
FROM subjects
GROUP BY name
) p on p.name = s.name
;
I have a table like
Name Spouse
---------------
John Smitha
Bob Neetha
Neetha Bob
Mona Jack
Smitha John
Jack Mona
and I want results as below using joins in MySQL.
Name Spouse
---------------
John Smitha
Bob Neetha
Mona Jack
(i.e. the couple should be selected only once)
Assuming that you have a IsPrimary field.
You could easily achieve this with the following query.
SELECT P.NAME, S.NAME As SpouseName
FROM PEOPLE P
LEFT JOIN PEOPLE S ON P.Spouse = S.Spouse -- You should have a PK/FK relasionship here (SouseId) and not join on a name string
WHERE P.IsPrimary = 1
a way to do it like that
SELECT `Name`, `Spouse`
FROM Table1
WHERE `Name` <= `Spouse`
demo
For the sake of simplicity, let's say I have tables users and interests
users
id | name
---------
1 | amy
2 | brian
3 | carole
interests
uid | interest
--------------
1 | apples
3 | catfish
3 | cobwebs
3 | cryogenics
What I want to get back is output that looks something like
name | interests
----------------
amy | apples
brian |
carole| catfish, cobwebs, cryogenics
Where interests could be a string consisting of the concatenation of all relevant values with some delimiter, or a vector of discrete values. I'm interested in dumping this to a file, rather than putting it in a table or doing any kind of further SQL stuff with it. Doing
SELECT name, (SELECT interest from interests where uid=id) as interests from users;
Is giving me the error I mentioned in the title. Is this just not possible in the SQL paradigm? I know I can dump a join of these tables to a file, and then aggregate the values I need using a python script or something, but this feels inelegant.
try this
SELECT name , group_concat(interest) as interests from interests
LEFT JOIN users on users.id = interests.uid
GROUP BY name
DEMO HERE
update:
if you want spaces do this group_concat(interest SEPARATOR ', ')
SELECT u.name, i.interest
FROM users AS u, interests AS i
WHERE u.uid = i.id
Try a join instead, this might work.
You might be getting errors because of the ordering of your select and from statements, or do you need to have a comma after name. Like:
SELECT name, ( *then your select statement here ...* )
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.