Find a class with exactly specific students - mysql

I'm no SQL expert, but I'm not a total amateur, yet this is a query on a single table with 2 fields that I don't know how to approach.
Suppose you have a table with class # & student #. How do you find the classes that have only exactly students x, y & z?
My real problem is more like a table of catalogs & item numbers, and how to find all the catalogs that have exactly (mo more or less) the specified items.
My only thought revolved around matching on GROUP_CONCAT, but there must b731e a more elegant way...
EDIT:
I misstated the problem, so I will provide table structure as well. The issue is more like products in boxes, where a box could contain more than one of a particular product, and you want to find boxes that have exactly the specified content. So the table, for example, is:
+------------+------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| box_id | bigint(20) | YES | | NULL | |
| product_id | bigint(20) | YES | MUL | NULL | |
+------------+------------+------+-----+---------+----------------+
I want to find all boxes that contain exactly 2 items of product ID 22, one of 17, and one of 55. No more, no less.

You could use a having clause:
select *
from YourTable
group by
class
having count(distinct student) = 3
and max(case when student = 'X' then 1 end) = 1
and max(case when student = 'Y' then 1 end) = 1
and max(case when student = 'Z' then 1 end) = 1

I do have an answer that works, but it is far from efficient OR elegant, so I present it for anyone looking for a sub-optimal but correct solution to this problem, and an enticement to anyone else to provide a better one.
SELECT box_id, GROUP_CONCAT( product_id
ORDER BY product_id DESC
SEPARATOR ',' ) AS contents
FROM box_product
GROUP BY box_id
HAVING contents = '17,22,22,55';

Related

Prioritize By Values Within Group

In MySQL, say I have the following table (called workers):
| id | specialty | status | name
| :- | :-------- | :--------- | :--- |
| 1 | Bricks | Unemployed | Joe
| 2 | Bricks | Employed | Eric
| 3 | Bricks | Contracted | Bob
| 4 | Tiles | Employed | Dylan
| 5 | Tiles | Contracted | James
In my query, say I want to find who is a prospective person for a new job. Thus, I would want to first find who is Unemployed, if no one is Unemployed, then who is only Contracted, and if no one is Contracted then at least who is Employed.
This would be GROUP BY specialty. The only methods I could figure out are either complex sub-queries or sets of UNIONs (or both). I also tried GROUP_CONCAT however this didn't work (or I didn't do it right). Googling this has not yielded any results.
Another idea is to assign a value to each category, and then do a group-wise max/min sub-query. I piloted this and it works, however seems quite messy and definitely not normalized:
SELECT
`id`,
`name`,
`status`,
-- I haven't been able to figure out how to get rid of MIN from the actual select
-- statement except by wrapping this in another sub-query, which I'm not keen on
MIN(`priority`) AS `priority`
FROM workers
INNER JOIN (
SELECT 'Unemployed' AS `status`, 0 AS `priority` FROM dual UNION
SELECT 'Contracted' AS `status`, 1 AS `priority` FROM dual UNION
SELECT 'Employed' AS `status`, 2 AS `priority` FROM dual
) AS priorities USING (`status`)
GROUP BY `specialty`;
I am looking for a more standard, efficient, normalized or versatile method of doing this.
Update:
An additional method I could be to use a CASE expression in the SELECT clause of the statement. This would be if I were to normalize the status column, through a foreign-key relationship or other related table:
New table called statuses
| id | status |
| :- | :------------- |
| 1 | Employed |
| 2 | Contracted |
| 3 | Unemployed |
| 4 | Not contracted |
Diffs: 'Not Contracted' is a new status and my workers table now stores the foreign key to the new statuses table.
Then my SQL would be:
SELECT
`id`,
`name`,
statuses.status,
MIN(`priority`) AS `priority`
FROM workers
INNER JOIN (
SELECT
`id`,
`status`,
CASE
-- currently uses text in `status`,
-- could also explicitly use `id`
WHEN `status` IN ('Unemployed', 'Not Contracted') THEN 0
WHEN `status` = 'Contracted' THEN 1
WHEN `status` = 'Employed' THEN 2
ELSE 3
END AS `priority`
FROM statuses
) AS statuses ON workers.status = statuses.id
GROUP BY `specialty`;
Note: You might think - why not put the priority in the statuses table? The reason why I am not doing that is because the priority changes depending on the data needed / the purpose of the report being generated.
Potentially this is a cleaner solution (for the times that the related data to prioritize against is in another table). Again, I am looking for a more standard, efficient, normalized or versatile method of doing this. Also, if there is more of a way this could be configurable to user input / variables.
The difficulty here mainly arises because you don't have an ordinal column which ranks the various status in some order. Absent that, we can introduce one using a CASE expression, similar to what your second query is trying to do:
SELECT w1.*
FROM workers w1
INNER JOIN
(
SELECT
specialty,
MIN(CASE status WHEN 'Unemployed' THEN 1
WHEN 'Contracted' THEN 2
ELSE 3 END) AS status_rnk
FROM workers
GROUP BY specialty
) w2
ON w1.specialty = w2.specialty AND
w2.status_rnk = CASE w1.status WHEN 'Unemployed' THEN 1
WHEN 'Contracted' THEN 2
ELSE 3 END;

Left outer join with multiple matches, how to return a specific one?

I have a problem which I think might be solved with proper use of left outer join, but I'm unable to construct suitable query. OTOH, there may also be some other, more clever solution with SQL. In addition, this could easily be solved with some programming, but I want to avoid that and find as "clean" solution as possible.
Background: let's say I'm creating a website that lists some car brands and the user can select which ones he owns/has owned (I'm not really doing that, but this example illustrates the point). In addition, for the selected ones he can optionally enter some additional info about them, e.g. year and some free text like specific model, comments or whatever. In addition, the information entered is stored in a relational database (MySQL in my case) and the user can retrieve and change his answers later.
Let's say there are two database tables:
BRAND
------------
ID INT
NAME VARCHAR(50)
OWNED
------------
ID INT
BRAND_ID INT
OWNER_ID INT
YEAR INT
COMMENT VARCHAR(100)
(here BRAND_ID + OWNER_ID is an unique index, so there can be only one row, and thus one year & comment for each BRAND/OWNER combination)
The data in these tables may look something like this:
BRAND
--------------
ID | NAME
--------------
1 | Cadillac
2 | Chevrolet
3 | Dodge
4 | Ford
OWNED
-----------------------------------------
ID | BRAND_ID | OWNER_ID | YEAR | COMMENT
-----------------------------------------
1 | 1 | 1 | null | 70's Fleetwood
2 | 2 | 1 | 2000 | Crappy car
3 | 2 | 2 | null | I really liked it
4 | 4 | 2 | 1999 | null
Now, to facilitate easy creation of the web page, what I would like to do is to with one SELECT display all brands in table BRAND, and for each BRAND to know whether current user has owned it or not, and if he has, also list his year and comment (if any). In other words, something like this (assuming current user is 2):
NAME | OWNER_ID | YEAR | COMMENT
-------------------------------------
Cadillac | null | null | null
Chevrolet | 2 | null | I really liked it
Dodge | null | null | null
Ford | 2 | 1999 | null
I tried doing something like:
select NAME, OWNER_ID, YEAR, COMMENT from BRAND left join OWNED on
BRAND.ID = OWNED.BRAND_ID where OWNER_ID = 2 or OWNER_ID = null
but that fails because 1 owns a Cadillac and thus Cadillac is left from the result. OTOH if I omit the where clause, I will get two rows for Chevrolet, which is also not desirable.
So, if there is a clean solution with SQL (either with or without left outer join), I'd like to know how to do it?
I am guessing you want this:
select NAME, OWNER_ID, YEAR, COMMENT
from BRAND left join
OWNED o
on BRAND.ID = OWNED.BRAND_ID and OWNER_ID = 2 ;
Seems like what you might actually want is a list of the owners, followed by what they owned and the details. You can that by adjusting the owner id at the bottom of this one:
SELECT owned.owner_id, brand.id, brand.name, owned.year, owned.comment
FROM owned
INNER JOIN brand
ON owned.brand_id = brand.id
WHERE owned.owner_id = 2
Tested here: http://sqlfiddle.com/#!9/5b52ba

Match Multiple rows of same columns in Table - MySql

I have a table as follows.
+------------+-------------+---------+
| productid | attributeid | valueid |
+------------+-------------+---------+
| 1011052312 | 331100 | 1543697 |
| 1011052312 | 33113319 | 1534108 |
| 1011098009 | 33129 | 2655849 |
| 1011052380 | 331100 | 1543697 |
| 1011052380 | 33113319 | 1233908 |
+------------+-------------+---------+
Now I need to fetch only those productid who has very selected set of attribute value pair. Say for example I need to fetch a product who's attribute 331100 has value 1543697 and attribute 33113319 has value 1534108. Product 1011052312 satisfy this condition.
One thing to note is that I should avoid multiple joins because there can be a long list of attributes that I need to match. And for each attribute there can be any number of possible value.
It's a common problem, select all the products matching at least one attribute, group them, count how many attributes each product has and then use a HAVING clause to select only the products which have all the attributes. Something like this:
SELECT productid
FROM table
WHERE (attributeid=331100 and valueid=1543697)
OR (attributeid=33113319 and valueid=1534108)
GROUP BY productid
HAVING COUNT(*) = 2
Another way of saying the same thing - I can't remember if the EXPLAIN is the same or not, so this might be slower...
SELECT productid
FROM my_table
WHERE (attributeid,valueid) IN ((331100,1543697)
,(33113319,1534108)
)
GROUP
BY productid
HAVING COUNT(*) = 2;

Group only if condition is met

I have a table with fields TYPES, AMOUNT and COMMENT
I want to group by the TYPES field, but if the 'TYPES' value is "other" AND 'COMMENT' is not empty, I want those records to show up separately.
Example result:
+-----------+-----------+-----------+
| types | amount | comment |
+-----------+-----------+-----------+
| type1 | 27 | |
| type2 | 65 | |
| type3 | 45 | |
| other | 4 | blabla |
| other | 8 | something |
-------------------------------------
So instead of grouping the two "other" records, I want those records to show up separately (but only if they also have a comment)
If I understand correctly, you want all rows with a given type to grouped together unless the type is 'Other' and the comment is not NULL.
A close approximation is:
select types,
(case when types = 'Other' and comment is not null
then comment end) as comment,
sum(amount) as amount
from table t
group by types,
(case when types = 'Other' and comment is not null
then comment end);
The only issue is that rows with types = 'Other' and the same comment will be grouped together. To fix this correctly, you need a unique identifier on each row, which MySQL does not readily provide.
In your case I see two separate data sets. Try with union all as below
select types, amount,comment
from tab
where (types = 'other' and comment is not null)
or types <> 'other'
group by types
union all
select types, amount,comment
from tab
where types = 'other'
and comment is null

Select userids from mysql table if user has at least 3 fields filled

I have a mysql user table that holds user data like that:
userid | title | content
----------------------------------
1 | about | I am from ...
1 | location | Norway
1 | name | Mark
1 | website |
2 | about |
2 | location |
2 | name |
2 | website |
3 | ...
As you see the content is empty for userid 2, and also for many more users in the table.
My goal is to select only the userids that have at least 3 fields filled. All others should be ignored.
As my mysql knowledge is still weak I could not find a solution for this. I only found the opposite and just with count: Find the count of EMPTY or NULL columns in a MySQL table
What is the magic mysql query? Any help appreciated, thank you.
You would use aggregation and a having clause for this:
select u.userId
from users u
where content > '' and content is not null
group by u.userId
having count(*) >= 3;
I added the non-blank check as well as the null check. The null check is redundant, but it makes the intention clearer.