COUNT with IF FROM DISTINCT values MySQL - mysql

I am trying to calculate the how many users are female and how many male, after doing DISTINCT on the UserID
SELECT
COUNT(IF("Gender" = 'female', 1, NULL)) as Ufemale,
COUNT(IF("Gender" = 'male', 1, NULL)) as Umale
FROM (SELECT DISTINCT UserID FROM user_stats where Year='2019' and Account='P') as UID
If I execute the
SELECT DISTINCT UserID FROM user_stats where Year='2019' and Account='P'
It returns the unique UserID. However, if I combine it with count gender part, it returns Zero.
Here is how the values looks like
UserID | Gender
-----------------------------
2018359084885123 | male
1925823664195671 | female
2033134076795519 |
2122445674469149 | female
2315129265210413 | female
2018359084885123 | male
2122445674469149 | female
And the aim is to show
Ufemale | Umale
-------------------
3 | 1

Try this:
Tables
drop table if exists test;
create table test (userid bigint, gender char(6));
insert into test values
(2018359084885123,'male')
,(1925823664195671,'female')
,(2033134076795519, null)
,(2122445674469149,'female')
,(2315129265210413,'female')
,(2018359084885123,'male')
,(2122445674469149,'female');
Query
select
sum(case when gender = 'female' then 1 else 0 end) as ufemale,
sum(case when gender = 'male' then 1 else 0 end) as umale
from
(select distinct userid, gender from test) x
Result
ufemale umale
3 1
Example: https://rextester.com/JLQ19855

There is no Gender column in the results of the subquery:
SELECT DISTINCT UserID FROM user_stats where Year='2019' and Account='P'
as you may think.
What you are doing is comparing 2 strings: "Gender" and 'female' which are obviously not equal so this:
IF("Gender" = 'female', 1, NULL)
returns NULL and COUNT(NULL) returns 0.
Where is the Gender column?
Is it in user_stats table or in another table like Users?
If it is in user_stats then you need to write the query like this:
select
sum(t.gender = 'female') ufemale,
sum(t.gender = 'male') umale
from (
select distinct userid, gender
from user_stats
where Year='2019' and Account='P'
) t
If it is in the users table then you will need a join first:
select
sum(u.gender = 'female') ufemale,
sum(u.gender = 'male') umale
from (
select distinct userid, gender
from user_stats
where Year='2019' and Account='P'
) t inner join users u
on u.userid = t.userid

Related

SQL Select with Conditional Fields (Subquerys maybe?)

I'm trying to set up an SQL View that returns all details of a contact. Name and Last name are saved in a Table "Person", the contact info is saved in "contact" and the type of contact info (email, phone 1, phone2) is saved in "contact_types".
I want to return all the information in 1 row but I can't really figure it out. So far my best result is with:
SELECT
Person.ID, Person.Title, Person.Firstname, Person.Lastname,
( SELECT MAX(ContactInfo.InfoText) FROM ContactInfo WHERE ContactInfo.ContactTypID = '1' AND ContactInfo.PersonID = Person.ID ) AS Phone_Business,
( SELECT MAX(ContactInfo.InfoText) FROM ContactInfo WHERE ContactInfo.ContactTypID = '2' AND ContactInfo.PersonID = Person.ID ) AS Phone_Private,
( SELECT MAX(ContactInfo.InfoText) FROM ContactInfo WHERE ContactInfo.ContactTypID = '3' AND ContactInfo.PersonID = Person.ID ) AS Phone_Mobile,
( SELECT MAX(ContactInfo.InfoText) FROM ContactInfo WHERE ContactInfo.ContactTypID = '5' AND ContactInfo.PersonID = Person.ID ) AS Email
FROM Person
This statement results in duplicate outputs - 4 identical rows, even with MAX(). It is apparently one row per subquery. How can I only receive 1 row per ID?
I'm quite new to SQL, any suggestions would be helpful!
Edit:
Sample Data:
Table Person:
ID
Title
Firstname
Lastname
1
Mr.
Tom
Selleck
2
Mr.
Fred
Miller
Table ContactInfo
PersonID
InfoText
ContactTypeID
1
tom.selleck#gmail.com
5
2
+1 12345 678
1
1
+1 98765 432
2
Table ContactTypeID
ID
InfoText
1
phone_business
2
phone_private
5
email
Expected Result:
ID
Title
Firstname
Lastname
Phone_Business
Phone_Private
Phone_Mobile
Email
1
Mr.
Tom
Selleck
NULL
+1 98765 432
NULL
tom.selleck#gmail.com
2
Mr.
Fred
Miller
+1 12345 678
NULL
NULL
NULL
It works so far, but I'd get each row 4 times.
You can do it like this:
SELECT P.ID, P.Title, P.Firstname, P.Lastname,
MAX(CASE WHEN C.ContactTypeID = '1' THEN C.InfoText END) AS Phone_Business,
MAX(CASE WHEN C.ContactTypeID = '2' THEN C.InfoText END) AS Phone_Private,
MAX(CASE WHEN C.ContactTypeID = '3' THEN C.InfoText END) AS Phone_Mobile,
MAX(CASE WHEN C.ContactTypeID = '5' THEN C.InfoText END) AS Email
FROM Person P
LEFT JOIN ContactInfo C
ON P.ID=C.PersonID
GROUP BY P.ID, P.Title, P.Firstname, P.Lastname;
Just a single LEFT JOIN between Person table and ContactInfo. The Person table here acts as a reference table. Then use MAX() with CASE expression (also possible with GROUP_CONCAT()) in SELECT.
Here's a demo fiddle

Mysql - How to create view representing dynamic pivot

I have two database tables customers which contains data about customers with the scheme like that:
mysql> SELECT * FROM customers;
customer_id created_at partner_id
1 "2019-08-20 09:17:58" cats
2 "2019-09-12 11:46:37" dogs
and customers_facts which keeps the customers facts in a form of fact_name and corresponding fact_value.
mysql> SELECT * FROM customers_facts;
customer_id fact_name fact_value
1, name Milton
1 city Milan
2 surname Bloom
2 name Orlando
I want to create a pivot table which in each row will have a customer and it's facts each as a separate column. Something like this:
mysql> SELECT * FROM pivot_table;
customer_id created_at partner_id name city surname
1 "2019-08-20 09:17:58" cats Milton Milan
2 "2019-09-12 11:46:37" dogs Orlando Bloom
I've found a script that allows me to create such table:
SET #sql = '';
SELECT
#sql := CONCAT(#sql,if(#sql='','',', '),temp.output)
FROM
(
SELECT
DISTINCT
CONCAT(
'MAX(IF(cf.fact_name = ''',
fact_name,
''', cf.fact_value, NULL)) AS ''',
fact_name,
''''
) as output
FROM
customers_facts
) as temp;
SET #sql = CONCAT('SELECT c.customer_id, c.created_at, c.partner_id, ', #sql, '
FROM customers c
LEFT JOIN customers_facts AS cf
ON cf.customer_id = c.customer_id
GROUP BY c.customer_id, c.created_at, c.partner_id');
but I have an issue of how to make it so:
a) I will be able to query the pivot table
b) When I add a new entry / update an old one in one of those two original tables the pivot table will be updated
How to solve ? Is it possible ?
Consider the following:
DROP TABLE IF EXISTS customers;
CREATE TABLE customers
(customer_id SERIAL PRIMARY KEY
,created_at DATETIME NOT NULL
,partner_id INT NOT NULL
);
INSERT INTO customers VALUES
(1,"2019-08-20 09:17:58",108),
(2,"2019-09-12 11:46:37",110);
DROP TABLE IF EXISTS customers_facts ;
CREATE TABLE customers_facts
(customer_id INT NOT NULL
,fact_name VARCHAR(20) NOT NULL
,fact_value VARCHaR(20) NOT NULL
,PRIMARY KEY(customer_id,fact_name)
);
INSERT INTO customers_facts VALUES
(1,'name','Milton'),
(1,'city','Milan'),
(2,'surname','Bloom'),
(2,'name','Orlando');
Now we can create a VIEW in the manner you describe...
DROP VIEW IF EXISTS my_pivot;
CREATE VIEW my_pivot AS
SELECT c.customer_id
, c.created_at
, c.partner_id
, MAX(CASE WHEN fact_name = 'name' THEN fact_value END) name
, MAX(CASE WHEN fact_name = 'surname' THEN fact_value END) surname
, MAX(CASE WHEN fact_name = 'city' THEN fact_value END) city
FROM customers c
LEFT
JOIN customers_facts f
ON f.customer_id = c.customer_id
GROUP
BY c.customer_id;
We can interrogate this VIEW with a simple query - e.g. SELECT customer_id FROM my_pivot WHERE name = 'Milton', however, this cannot use an index, so it's not very efficient.
Also, because of the way in which we created the VIEW, it cannot be updated...
UPDATE my_pivot SET name = 'Leonardo' WHERE customer_id = 1;
ERROR 1288 (HY000): The target table my_pivot of the UPDATE is not updatable
However, had we created the VIEW slightly differently, then it could be updated...
DROP VIEW IF EXISTS my_pivot;
CREATE VIEW my_pivot AS
SELECT c.customer_id
, c.created_at
, c.partner_id
, name.fact_value name
, surname.fact_value surname
, city.fact_value city
FROM customers c
LEFT
JOIN customers_facts name
ON name.customer_id = c.customer_id
AND name.fact_name = 'name'
LEFT
JOIN customers_facts surname
ON surname.customer_id = c.customer_id
AND surname.fact_name = 'surname'
LEFT
JOIN customers_facts city
ON city.customer_id = c.customer_id
AND city.fact_name = 'city';
UPDATE my_pivot SET name = 'Leonardo' WHERE customer_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
SELECT * FROM customers_facts;
+-------------+-----------+------------+
| customer_id | fact_name | fact_value |
+-------------+-----------+------------+
| 1 | city | Milan |
| 1 | name | Leonardo |
| 2 | name | Orlando |
| 2 | surname | Bloom |
+-------------+-----------+------------+
...but this still cannot use an index.
EDIT: To answer the question asked in comments below your question, you can do...
SELECT customer_id
FROM customers_facts
WHERE
( fact_name,fact_value ) IN (('name','Orlando'),('surname','Bloom'))
GROUP
BY customer_id
HAVING COUNT(*) = 2;
...although I think MySQL can't use an index in this instance, so the longhand version might be better...
SELECT customer_id
FROM customers_facts
WHERE
( fact_name = 'name'
AND fact_value = 'Orlando'
)
OR
( fact_name = 'surname'
AND fact_value = 'Bloom'
)
GROUP
BY customer_id HAVING COUNT(*) = 2;

MySQL how to count from GROUP BY

date | userid | companyid
12.8.14 | 1 | 1
12.8.14 | 2 | 2
12.8.14 | 3 | 1
I had a table like above. It is easy to count how many company 1 from table with normal query.
My question is : if my query is select * from table where companyid = '1' group by date, how can i get mysql_num_row equal to 2 for company 1 and userid is 1 and 3?
select * from table where companyid = '1' group by date will only return me
mysql_num_row equal 1 and result 12.8.14 | 1 | 1
You can nest a query to get the sum of company one entries and then join that nested query to an outer query:
SELECT ABB2.*, ABB1.mysql_num_row
FROM `table` AS ABB2
JOIN
(SELECT companyid, COUNT(userid) AS mysql_num_row
FROM `table`
GROUP BY companyid) AS ABB1 ON ABB1.companyid = ABB2.companyid
WHERE ABB2.companyid = 1;
Example
try like this also
select *,(select count(*) from table1 where companyid=a.companyid) as count
from t as a where companyid=1
You wanted:
select date,companyid,count(*)
from table
where userid = 1
group by date,companyid

How to find if a list/set is contained within another list

I have a list of product IDs and I want to find out which orders contain all those products. Orders table is structured like this:
order_id | product_id
----------------------
1 | 222
1 | 555
2 | 333
Obviously I can do it with some looping in PHP but I was wondering if there is an elegant way to do it purely in mysql.
My ideal fantasy query would be something like:
SELECT order_id
FROM orders
WHERE (222,555) IN GROUP_CONCAT(product_id)
GROUP BY order_id
Is there any hope or should I go read Tolkien? :) Also, out of curiosity, if not possible in mysql, is there any other database that has this functionality?
You were close
SELECT order_id
FROM orders
WHERE product_id in (222,555)
GROUP BY order_id
HAVING COUNT(DISTINCT product_id) = 2
Regarding your "out of curiosity" question in relational algebra this is achieved simply with division. AFAIK no RDBMS has implemented any extension that makes this as simple in SQL.
I have a preference for doing set comparisons only in the having clause:
select order_id
from orders
group by order_id
having sum(case when product_id = 222 then 1 else 0 end) > 0 and
sum(case when product_id = 555 then 1 else 0 end) > 0
What this is saying is: get me all orders where the order has at least one product 222 and at least one product 555.
I prefer this for two reasons. The first is generalizability. You can arrange more complicated conditions, such as 222 or 555 (just by changing the "and" to and "or"). Or, 333 and 555 or 222 without 555.
Second, when you create the query, you only have to put the condition in one place, in the having clause.
Assuming your database is properly normalized, i.e. there's no duplicate Product on a given Order
Mysqlism:
select order_id
from orders
group by order_id
having sum(product_id in (222,555)) = 2
Standard SQL:
select order_id
from orders
group by order_id
having sum(case when product_id in (222,555) then 1 end) = 2
If it has duplicates:
CREATE TABLE tbl
(`order_id` int, `product_id` int)
;
INSERT INTO tbl
(`order_id`, `product_id`)
VALUES
(1, 222),
(1, 555),
(2, 333),
(1, 555)
;
Do this then:
select order_id
from tbl
group by order_id
having count(distinct case when product_id in (222,555) then product_id end) = 2
Live test: http://www.sqlfiddle.com/#!2/fa1ad/5
CREATE TABLE orders
( order_id INTEGER NOT NULL
, product_id INTEGER NOT NULL
);
INSERT INTO orders(order_id,product_id) VALUES
(1, 222 ) , (1, 555 ) , (2, 333 )
, (3, 222 ) , (3, 555 ) , (3, 333 ); -- order#3 has all the products
CREATE TABLE products AS (SELECT DISTINCT product_id FROM orders);
SELECT *
FROM orders o1
--
-- There should not exist a product
-- that is not part of our order.
--
WHERE NOT EXISTS (
SELECT *
FROM products pr
WHERE 1=1
-- extra clause: only want producs from a literal list
AND pr.product_id IN (222,555,333)
-- ... that is not part of our order...
AND NOT EXISTS ( SELECT *
FROM orders o2
WHERE o2.product_id = pr.product_id
AND o2.order_id = o1.order_id
)
);
Result:
order_id | product_id
----------+------------
3 | 222
3 | 555
3 | 333
(3 rows)

SQL: Where NOT IN query

So in the database, there's a table named roles_users. This holds all the roles that the users have. Here, there's two columns: user_id, role_id.
A normal user, with no extra roles has 1 row in this table. This row has role_id 1.
A admin user, has 2 rows in this table. One with role_id 1, and one row with role_id 2
Like this:
user_id role_id
88 1
88 2
99 1 // Only one row with that user_id, so he's a user
Now im trying to count how many users/admin/sellers/partners that exists.
Sellers have 3 rows, one with role_id 1, role_id 2 and role_id 3.
Partner has role_id 1, role_id 4
So i tried this:
SELECT user_id FROM roles_users WHERE role_id IN (1) // MEMBERS ONLY
SELECT user_id FROM roles_users WHERE role_id IN (1,2) // ADMIN
SELECT user_id FROM roles_users WHERE role_id IN (1,2,3) // SELLER
SELECT user_id FROM roles_users WHERE role_id IN (1,4) // PARTNERS
But these queries does not work properly. They give me a count that is way over than its supposed to be. And i believe this is because that it does not EXCLUDE any rows,
I mean at the query when it should look for role_id 1, for members, it includes partners,admin,sellers too because it only check for if it theres any with the row role_id 1 and thats it.
So how can i do this right? So when it looks after members, it should also make sure that the user_id does not have any more rows with other role_ids like 2,3,4
group_concat is what you want:
select
case roles
when '1' then 'members'
when '1,2' then 'admins'
when '1,2,3' then 'sellers'
when '1,4' then 'partners'
else 'uh??'
end role,
count(user_id) nr_users from (
select user_id, group_concat(role_id order by role_id separator ',') roles
from roles_users
group by user_id
)
group by role
order by role;
And by the way, you could store roles more efficiently using a bitmask. Advantage: only one column per user id. Disadvantage: harder to build queries...
This will give you all combinations of privileges in the table with their count
SELECT
COUNT(*) AS num
GROUP_CONCAT(role_id ORDER BY role_id) AS privilegeset
FROM roles_users
GROUP BY user_id
Concerning sellers and partners:
As I understand it, sellers are the only ones that can have role_id = 3 and partners are the only ones that can have role_id = 4, correct?
If yes, finding sellers and partners is quite easy:
SELECT user_id FROM roles_users WHERE role_id = 3 // SELLER
SELECT user_id FROM roles_users WHERE role_id = 4 // PARTNERS
Not the most elegant, but a start
SELECT user_id
,CASE WHEN (role1 > 0 AND 0 = role2 AND 0 = role3 AND 0 = role4) THEN 'MEMBER'
WHEN (role1 > 0 AND role2 > 0 AND 0 = role3 AND 0 = role4) THEN 'ADMIN'
WHEN (role1 > 0 AND role2 > 0 AND role3 > 0 AND 0 = role4) THEN 'SELLER'
WHEN (role1 > 0 AND 0 = role2 AND 0 = role3 AND role4 > 0) THEN 'PARTNER'
ELSE 'Something else'
END AS type
FROM (
SELECT user_id
,SUM( CASE role_id WHEN 1 THEN 1 ELSE 0 END) AS role1
,SUM( CASE role_id WHEN 2 THEN 1 ELSE 0 END) AS role2
,SUM( CASE role_id WHEN 3 THEN 1 ELSE 0 END) AS role3
,SUM( CASE role_id WHEN 4 THEN 1 ELSE 0 END) AS role4
FROM roles_users
GROUP BY user_id
) x
EDIT: I guess this is answering the wrong question, it is showing the type of each user.
Here's a solution using INTERSECT and MINUS. Sadly these are not supported by MySQL yet but maybe someone will find this useful nonetheless.
-- Find users with Role 1
SELECT user_id FROM
(SELECT user_id, role_id FROM roles_users WHERE role_id = 1)
MINUS SELECT user_id FROM roles_users WHERE role_id NOT IN (1)
-- Find users with Roles 1 and 2
SELECT user_id FROM
(SELECT user_id, role_id FROM roles_users WHERE role_id = 1
INTERSECT SELECT user_id, role_id FROM roles_users WHERE role_id = 2)
MINUS SELECT user_id FROM roles_users WHERE role_id NOT IN (1,2)
-- Find users with Roles 1, 2, 3
SELECT user_id FROM
(SELECT user_id, role_id FROM roles_users WHERE role_id = 1
INTERSECT SELECT user_id, role_id FROM roles_users WHERE role_id = 2
INTERSECT SELECT user_id, role_id FROM roles_users WHERE role_id = 3)
MINUS SELECT user_id FROM roles_users WHERE role_id NOT IN (1,2,3)
-- Find users with Roles 1, 4
SELECT user_id FROM
(SELECT user_id, role_id FROM roles_users WHERE role_id = 1
INTERSECT SELECT user_id, role_id FROM roles_users WHERE role_id = 4)
MINUS SELECT user_id FROM roles_users WHERE role_id NOT IN (1,4)
Here's a join-based solution that hopefully works on any SQL.
SELECT user_id FROM roles_users RU0
LEFT JOIN roles_users RU1 ON RU0.user_id = RU1.user_id AND RU1.role_id = 1
LEFT JOIN roles_users RU2 ON RU0.user_id = RU2.user_id AND RU2.role_id = 2
LEFT JOIN roles_users RU3 ON RU0.user_id = RU3.user_id AND RU3.role_id = 3
LEFT JOIN roles_users RU4 ON RU0.user_id = RU4.user_id AND RU4.role_id = 4
WHERE RU1.user_id IS NOT NULL -- should have role 1
AND RU2.user_id IS NULL -- should NOT have role 2
AND RU3.user_id IS NULL -- should NOT have role 3
AND RU4.user_id IS NOT NULL -- should have role 4
Just vary the "IS NULL" and "IS NOT NULL" in the where clause to change which roles you want the user to have or not have.