MySQL GROUP BY while keeping certain rows by column content - mysql

There is table with duplicated rows. See rows 1 and 2:
id full_name email status active
1 John Doe john#mail.com ok 1
2 John Doe john#mail.com null 1
3 Ricky Duke rick#mail.com null 1
4 Jane Doe jane#mail.com block 1
I need to select distinct rows, not randomly - one distinct row, but the one that has a 'status' NOT NULL.
My query is:
SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND (status = 'ok' OR status IS NULL)
GROUP BY email
That query selects distinct rows randomly, without prioritizing 'status' field.
How can i prioritize selection of distinct rows, that has a 'status' NOT NULL, and select ones with NULL only in case there is no rows with 'ok' status is present?

You can use row_number():
select s.*
from (select s.*,
row_number() over (partition by email order by (status is not null) desc) as seqnum
from subscribers s
where active = 1
) s
where seqnum = 1;

You could filter with a correlated subquery that does conditional ordering, and gives a lowest priority to null statuses:
select t.*
from mytable t
where t.id = (
select id
from mytable t1
where
t1.full_name = t.full_name
and t1.email = t.email
and t1.active = t.active
order by status is null, status
limit 1
)
This defines duplicats as records that have the same full_name, email and active. You might want to adapt that to your actual definition of duplicates.
Demo on DB Fiddle:
id | full_name | email | status | active
-: | :--------- | :------------ | :----- | :-----
1 | John Doe | john#mail.com | ok | 1
3 | Ricky Duke | rick#mail.com | null | 1
4 | Jane Doe | jane#mail.com | block | 1

(SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND status IS NOT NULL
GROUP BY email)
UNION ALL
(SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND status IS NULL AND
email not in (SELECT distinct email
FROM `subscribers`
WHERE active = 1 AND status IS NOT NULL)
GROUP BY email);

Related

Limit query results based on value of multiple columns

I am using MySQL 5.6 and I have a table structure like below
| user_id | email_1 | email_2 | email_3 |
| 1 | abc#test.com | | |
| 2 | xyz#test.com | | joe#test.com |
| 3 | | test#test.com | bob#joh.com |
| 4 | | | x#y.com |
I want to fetch the first n email addresses from this table.
For example, if I want to fetch the first 5 then only the first 3 rows should return.
This makes certain assumptions about the uniqueness of data, that might not be true...
SELECT DISTINCT x.* FROM my_table x
JOIN
(SELECT user_id, 1 email_id,email_1 email FROM my_table WHERE email_1 IS NOT NULL
UNION ALL
SELECT user_id, 2 email_id,email_2 email FROM my_table WHERE email_2 IS NOT NULL
UNION ALL
SELECT user_id, 3 email_id,email_3 email FROM my_table WHERE email_3 IS NOT NULL
ORDER BY user_id, email_id LIMIT 5
) y
ON y.user_id = x.user_id
AND CASE WHEN y.email_id = 1 THEN y.email = x.email_1
WHEN y.email_id = 2 THEN y.email = x.email_2
WHEN y.email_id = 3 THEN y.email = x.email_3
END;
You want to return as many rows as necessary to get five emails. So you need a running total of the email count.
select user_id, email_1, email_2, email_3
from
(
select
user_id, email_1, email_2, email_3,
coalesce(
sum((email_1 is not null) + (email_2 is not null) + (email_3 is not null))
over (order by user_id rows between unbounded preceding and 1 preceding)
, 0) as cnt_prev
from mytable
) counted
where cnt_prev < 5 -- take the row if the previous row has not reached the count of 5
order by user_id;
You need a current MySQL version for SUM OVER to work.
The counting of the emails uses a MySQL feature: true equals 1 and false equals 0 in MySQL. Thus (email_1 is not null) + (email_2 is not null) + (email_3 is not null) counts the emails in the row.
Demo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=ac415e71733699547196ae01cb1caf13

select column value by custom priority

Mysql 5.7
I have a table with 3 columns
ID -> Primary Key
STATUS -> User Status
MASTERID -> Foreign Key
I need a select query which will return a Status of the user using below priority
Verified -> 1 has the highest priority
Reset -> 2 the second priority
Created -> 3 has the least priority
ID STATUS MASTERID
1 Created 1
2 Verified 1
3 Reset 1
4 Created 1
select query should return Verified
ID STATUS MASTERID
1 Reset 1
2 Reset 1
select query should return Reset as there is only one distinct status present
ID STATUS MASTERID
1 Reset 1
2 Created 1
3 Verified 1
select query should return Verified as it has the highest priority
above records are an example of one user, likewise I need to fetch the status of each distinct user
Use order by with field():
select t.*
from (select t.*,
row_number() over (partition by masterid order by field(status, 'Verified', 'Reset', 'Created') as seqnum
from t
) t
where seqnum = 1;
If you only want the result for one master id:
select t.*
from t
where masterid = 1
order by field(status, 'Verified', 'Reset', 'Created')
limit 1;
Of course, for the values that you have specified, you could replace field() with status desc. However, this offers the more general solution.
EDIT:
In MySQL 5.7, you can use:
select t.*
from t
where t.id = (select t2.id
from t t2
where t2.masterid = t.masterid
order by field(status, 'Verified', 'Reset', 'Created')
limit 1
);
I am assuming that MASTERID is the user and I have taken the liberty of creating some sample data. But really the OP (and that is you) should be the one creating the sample tables and data on a website such as db-fiddle.com if you are seriously looking for someone to answer your questions in the future. See Why should I provide a Minimal Reproducible Example for a very simple SQL query?.
SELECT MASTERID,
CASE
WHEN RANKING = 1 THEN 'Created'
WHEN RANKING = 2 THEN 'Reset'
WHEN RANKING = 3 THEN 'Verified'
END as STATUS
FROM (
SELECT MASTERID, MAX(FIELD(STATUS, 'Created', 'Reset', 'Verified')) as RANKING
FROM mytable
GROUP bY MASTERID
) SQ;
| MASTERID | STATUS |
| -------- | -------- |
| 1 | Verified |
| 2 | Reset |
| 3 | Verified |
View on DB Fiddle
If you want to also have an associated ID with the returned results, then:
SELECT * FROM mytable t1
WHERE FIELD(STATUS, 'Created', 'Reset', 'Verified') = (
SELECT MAX(FIELD(STATUS, 'Created', 'Reset', 'Verified')) FROM mytable t2
WHERE t2.MASTERID = t1.MASTERID
) AND t1.ID = (
SELECT MIN(t2.ID) FROM mytable t2
WHERE t2.MASTERID = t1.MASTERID AND t2.STATUS = t1.STATUS
)
| ID | STATUS | MASTERID |
| --- | -------- | -------- |
| 2 | Verified | 1 |
| 5 | Reset | 2 |
| 9 | Verified | 3 |
View on DB Fiddle

Last message in correspondence

I have the following table of messages (sid = sending correspondent ID, rid = receiving correspondent ID, mdate = message date, mtext = message text) representing a correspondence among parties:
sid|rid| mdate | mtext
---+---+------------+----------
1 | 2 | 01-08-2014 | message1 <-- 1st m. in corresp. between id=1 and id=2
2 | 1 | 02-08-2014 | message2 <-- 2nd m. in corresp. between id=1 and id=2
1 | 2 | 04-08-2014 | message3 <-- last m. in corrensp. between id=1 and id=2
2 | 3 | 02-08-2014 | message4 <-- not id=1 correspondence at all
1 | 3 | 03-08-2014 | message5 <-- 1st m. in corrensp. between id=1 and id=3
3 | 1 | 04-08-2014 | message6 <-- 2nd m. in corrensp. between id=1 and id=3
3 | 1 | 05-08-2014 | message7 <-- last m. in corrensp. between id=1 and id=3
5 | 1 | 03-08-2014 | message8 <-- last m. in corrensp. between id=1 and id=5
requested MySQL query should return for one correspondent (being sender or receiver) only correspondence with last message (sent or received) with other parties. So from previous table of messages this query for correspondent with id=1 should return last correspondence messages (last sent or received):
sid|rid| mdate | mtext
---+---+------------+----------
1 | 2 | 04-08-2014 | message3
3 | 1 | 05-08-2014 | message7
5 | 1 | 03-08-2014 | message8
How to make such a query for MySQL?
group by sid if rid=1 or rid if sid=1 to find max date, then join:
select a.*
from messages a
join (
select if(sid=1, rid, sid) id, max(mdate) d
from messages
where sid = 1 or rid = 1
group by id) b on ((a.sid=1 and a.rid=b.id) or (a.sid=b.id and a.rid=1)) and a.mdate = b.d;
demo
Assuming that there are no messages with the exact same timestamp between two correspondents, you can use a filtering join:
select *
from messages m
join (
select case when sid > rid then sid else rid end r1
, case when sid <= rid then sid else rid end r2
, max(mdate) as max_mdate
from messages
where 1 in (sid, rid)
group by
r1
, r2
) as filter
on m.sid in (filter.r1, filter.r2)
and m.rid in (filter.r1, filter.r2)
and m.mdate = filter.max_mdate
Example on SQL Fiddle.
Does the following join work for you:
SELECT m.*
FROM messages m
INNER JOIN
(SELECT
MAX(
CASE
WHEN sid = 1 THEN mdate
ELSE NULL
END
) max_sdate,
MAX(
CASE
WHEN rid = 1 THEN mdate
ELSE NULL
END
) max_rdate
FROM messages
WHERE sid = 1 or rid = 1
) max_dates
ON (m.sid = 1 AND mdate = max_dates.max_sdate) OR (m.rid = 1 AND mdate = max_dates.max_rdate);
SQL Fiddle demo

MySQL - select row by user_id based on column value of other rows with the same user_id

I have a complicated request, so I have simplified it down, and hope I explain it well.
I have a table of Subscriptions:
ID | Timestamp | User Id | Status
-----+---------------------+---------+---------
1 | 2013-06-14 16:39:23 | 1 | inactive
2 | 2013-11-20 10:18:17 | 1 | active
3 | 2013-06-14 16:39:23 | 2 | inactive
4 | 2014-03-01 17:18:26 | 3 | active
I want to Query this table to find subscriptions which have a status of inactive, and who do NOT also have an active subscription.
For example, this should return Subscription ID 3, but not Subscription ID 1 - Because the User of ID 1 has an active subscription (being Subscription ID 2).
following query should do the trick:
select a.id from
table as a
inner join
(
select user_id, max(id)
from table
group by user_id
) as b
on (a.id=b.id and a.status='inactive')
Try the following and see if that works
SELECT *
FROM Table t
WHERE t.Status = 'inactive'
AND NOT EXISTS(SELECT 1 FROM Table t2
WHERE t2.user_id = t.user_id
AND t2.status <> 'inactive')

MySQL Query to get the record for a specific date

I have the following table named staff_status with structure and records:
----------------------------------------------------
| id (INT) | status (VARCHAR) | status_date (DATE) |
----------------------------------------------------
| 1 | Working | 2009-05-03 |
| 2 | Working | 2009-07-21 |
| 1 | Leave | 2010-02-01 |
| 1 | Working | 2010-02-15 |
----------------------------------------------------
Now I want to query this to get the status of the staff on a specific date. Example: status of id = 1 on 2010-02-10 should return Leave while on 2010-03-01 should return Working
What I have tried without success:
SELECT t1.status FROM staff_status t1 INNER JOIN (SELECT * FROM staff_status WHERE id = 1 AND status_date < '2010-02-10') t2 ON (t1.id = t2.id AND t1.status_date < t2.status_date);
You could try something like
SELECT s.*
FROM staff_status s INNER JOIN
(
SELECT id,
MAX(status_date) status_date
FROM staff_status
WHERE status_date < '2010-02-10'
AND id = 1
) m ON s.id = m.id
AND s.status_date = m.status_date
Additionaly you could try an ORDER BY status_date DESC LIMIT 1
from 13.2.8. SELECT Syntax
Something like
SELECT *
FROM staff_status
WHERE id = 1
AND status_date < '2010-02-10'
ORDER BY status_date DESC
LIMIT 1
First, you'll need the MAX() of the dates per id:
SELECT id, MAX(status_date)
FROM staff_status
WHERE status_date < "2010-02-10" GROUP BY id
...but MySQL doesn't guarantee that the status will be from the row of the MAX(status_date) (in fact, this is almost never the case). So you'll have to take the information you found above, and pull out those records from the original table, matching on id and status_date:
SELECT id, status
FROM staff_status
WHERE
(id, status_date)
IN
(
SELECT id, MAX(status_date)
FROM staff_status
WHERE status_date < "2010-02-10" GROUP BY id
);
This generates a list of ids and statuses for the most recent date found before 2010-02-10:
+------+---------+
| id | status |
+------+---------+
| 2 | Working |
| 1 | Leave |
+------+---------+
2 rows in set (0.01 sec)
Surely simply:
SELECT status FROM staff_status WHERE status_date = '2010-02-10'
Would return you "leave"?
try this:
select status
from staff_status
where status_date<='2010-03-01'
and id=1
order by status_date desc
limit 1
try this:
SELECT IFNULL((SELECT status
FROM staff_status
WHERE id = 1 AND
status_date = '2010-02-10'),
"Leave") AS status;