Get Last non-null cell in a row mysql - mysql

I have a table in following structure:
id performance_id employee_comments manager_comments
1 23 NULL Well done.
2 46 NULL Improve Speed
3 46 Trying to improve NULL
4 46 NULL Put more effort
In above structure, at a time, only one comments exist, either employee_comments or manager_comments, not both simultaneously.
I want to fetch last non-empty employee comments and manager_comments in a single row.
I need mysql query to display output as:
performance_id employee_comments manager_comments
23 NULL Well done.
46 Trying to improve Put more effort

You can get the largest id for each of the employee comments and manager comments, per performance_id this way:
SELECT performance_id,
MAX(CASE WHEN employee_comments IS NOT NULL THEN id END) AS emp_id,
MAX(CASE WHEN manager_comments IS NOT NULL THEN id END) AS mgr_id
FROM MyTable
GROUP BY performance_id
The above only returns one row per performance_id, with two id numbers. These are the "last" entries, as determined by greater id numbers.
Output:
+----------------+--------+--------+
| performance_id | emp_id | mgr_id |
+----------------+--------+--------+
| 23 | NULL | 1 |
| 46 | 3 | 4 |
+----------------+--------+--------+
With that result, you can join it back to the table to retrieve other columns:
SELECT t.performance_id,
t.employee_comments,
t.manager_comments
FROM (
SELECT performance_id,
MAX(CASE WHEN employee_comments IS NOT NULL THEN id END) AS emp_id,
MAX(CASE WHEN manager_comments IS NOT NULL THEN id END) AS mgr_id
FROM MyTable
GROUP BY performance_id
) AS x
JOIN MyTable AS t ON t.id IN (emp_id, mgr_id)
Output:
+----------------+-------------------+------------------+
| performance_id | employee_comments | manager_comments |
+----------------+-------------------+------------------+
| 23 | NULL | Well done. |
| 46 | Trying to improve | NULL |
| 46 | NULL | Put more effort |
+----------------+-------------------+------------------+
The above return up to two rows per performance_id. You can use another GROUP BY to force them onto one row:
SELECT t.performance_id,
MAX(t.employee_comments) AS employee_comments,
MAX(t.manager_comments) AS manager_comments
FROM (
SELECT performance_id,
MAX(CASE WHEN employee_comments IS NOT NULL THEN id END) AS emp_id,
MAX(CASE WHEN manager_comments IS NOT NULL THEN id END) AS mgr_id
FROM MyTable
GROUP BY performance_id
) AS x
JOIN MyTable AS t ON t.id IN (emp_id, mgr_id)
GROUP BY performance_id
Output:
+----------------+-------------------+------------------+
| performance_id | employee_comments | manager_comments |
+----------------+-------------------+------------------+
| 23 | NULL | Well done. |
| 46 | Trying to improve | Put more effort |
+----------------+-------------------+------------------+

try this query
SELECT t.id,t.performance_id,t2.employee_comments,t3.manager_comments FROM test1 t join test1 t2 on t2.employee_comments IS NOT NULL and t.manager_comments IS NULL
join test1 t3 on t3.manager_comments IS NOT NULL and t.id < t3.id
group by t.performance_id

This can be done by a nested query in MySQL. We will have to do a union and group by performance_id
SELECT performance_id ,MAX(employee_comments)
employee_comments,MAX(manager_comments) manager_comments
FROM(
SELECT performance_id,employee_comments,manager_comments FROM MyTable
WHERE id IN (SELECT * FROM
(SELECT id FROM MyTable WHERE manager_comments IS NOT NULL ORDER BY id DESC)AS t)
UNION
SELECT performance_id,employee_comments,manager_comments FROM MyTable
WHERE id IN (SELECT * FROM
(SELECT id FROM MyTable WHERE employee_comments IS NOT NULL ORDER BY id DESC) AS p)) g GROUP BY performance_id;
The above query should work for fetching the last non-empty employee comments and manager_comments
I have tried and tested it...

Related

MySQL: Get most recent record satisfying certain conditions

I was inspired by this post. But what I'm going to solve is more complex.
In the table below we have three columns, id,rating,created, call it test_table,
+----+--------+----------------------+
| id | rating | created |
+----+--------+----------------------+
| 1 | NULL | 2011-12-14 09:25:21 |
| 1 | 2 | 2011-12-14 09:26:21 |
| 1 | 1 | 2011-12-14 09:27:21 |
| 2 | NULL | 2011-12-14 09:25:21 |
| 2 | 2 | 2011-12-14 09:26:21 |
| 2 | 3 | 2011-12-14 09:27:21 |
| 2 | NULL | 2011-12-14 09:28:21 |
| 3 | NULL | 2011-12-14 09:25:21 |
| 3 | NULL | 2011-12-14 09:26:21 |
| 3 | NULL | 2011-12-14 09:27:21 |
| 3 | NULL | 2011-12-14 09:28:21 |
+----+--------+----------------------+
I want to write a query which selects the most recent rating but not null for every id. If all of the ratings are null for a specific id, we select the most recent rating. The desired result is as follows:
+----+--------+----------------------+
| id | rating | created |
+----+--------+----------------------+
| 1 | 1 | 2011-12-14 09:27:21 |
| 2 | 3 | 2011-12-14 09:27:21 |
| 3 | NULL | 2011-12-14 09:28:21 |
+----+--------+----------------------+
The following gets the creation date:
select t.id,
coalesce(max(case when rating is not null then creation_date end),
creation_date
) as creation_date
from t
group by t.id;
You can then do this as:
select t.*
from t
where (id, creation_date) in (select t.id,
coalesce(max(case when rating is not null then creation_date end),
creation_date
) as creation_date
from t
group by t.id
);
One possible answer is this. Create a list of max(create) date per id and id having all NULL rating.
select t1.*
from myTable t1
join (
select id, max(created) as created
from myTable
where rating is not NULL
group by id
UNION ALL
select id, max(created) as created
from myTable t3
where rating is NULL
group by id
having count(*) = (select count(*) from myTable t4 where t4.id=t3.id)
) t2
where t1.id=t2.id
and t1.created=t2.created
order by t1.id;
select a.* from #test a join (select id, max(created) created
from #test
where rating is not null
group by id )b on a.id=b.id and a.created=b.created
union
select a.* from #test a join
(select id, max(created) created
from #test
where rating is null
and id not in
(select id from (select id, max(created) created
from #test
where rating is not null
group by id )d
group by id)
group by id )b on a.id=b.id and a.created=b.created
This query should work:
select a.id, a.rating, b.m from test_table a
join (
select id, max(created) as m from test_table
where rating is not null
group by id
) b on b.id = a.id and b.m = a.created
union
select a.id, a.rating, b.m from test_table a
join(
select id, max(created) as m from test_table a
where not exists
(select 1 from test_table b where a.id = b.id and b.rating is not null)
group by id
)b on b.id = a.id and b.m = a.created
You can get the created value in a correlated LIMIT 1 subquery:
select t.id, (
select created
from mytable t1
where t1.id = t.id
order by rating is null asc, created desc
limit 1
) as created
from (select distinct id from mytable) t
If you also need the rating column, you will need to join the result with the table again:
select t.*
from (
select t.id, (
select created
from mytable t1
where t1.id = t.id
order by rating is null asc, created desc
limit 1
) as created
from (select distinct id from mytable) t
) x
natural join mytable t
Demo: http://sqlfiddle.com/#!9/49e68c/8

Return rows that have one value but not another

For every same date (this is just one section of the table), I want to return the account numbers that made a purchase of A but DID NOT purchase B, and another query for vice-versa. So running the first query for A but no B should return 2 and 5. Running the vice-versa query for B but no A should give me 4. Thanks for the help. I'm assuming I would have to do a join of some sorts on the table but I'm stuck.
+----+----------------+---------------+----------+--+--+
| ID | Account Number | Purchase Type | Date | | |
+----+----------------+---------------+----------+--+--+
| 1 | 1 | A | 20140301 | | |
| 1 | 1 | A | 20140301 | | |
| 1 | 1 | B | 20140301 | | |
| 2 | 2 | A | 20140301 | | |
| 3 | 3 | A | 20140301 | | |
| 3 | 3 | B | 20140301 | | |
| 4 | 4 | B | 20140301 | | |
| 5 | 5 | A | 20140301 | | |
| 5 | 5 | A | 20140301 | | |
+----+----------------+---------------+----------+--+--+
Not sure if it is necessarily the best approach, but an inner select will work:
select distinct account_number
from purchases p
where purchase_type = "A" and account_number not in
(
select account_number
from purchases
where purchase_date = p.purchase_date and purchase_type = "B"
)
You first collect all ids that have purchase type "B" and then all ids with purchase type "A" that are not in the first collection.
(Assuming your table is purchases, ID is id int, Purchase Date is purchase_date char(1) and Date is purchase_date char(8), but you should be able to adapt the query to your actual columns.
Corresponding fiddle: http://sqlfiddle.com/#!9/edf73f/7/0)
One approach would be to use a full outer join where one or the other side is null; but mySQL doesn't support them. So to simulate: use a left join and then a union (or union all if you want to keep the fact that 1,1,A exists twice.) and we simply switch the criteria between the joins for the second SQL to union to handle both ways.
DEMO using SQL fiddle in comment: http://sqlfiddle.com/#!9/52c893/20/0
SELECT A.*
FROM purch A
LEFT JOIN purch B
on A.`Account Number` = B.`Account Number`
AND B.`Purchase Type` = 'B'
WHERE b.`Account Number` is null
AND A.`Purchase Type` = 'A'
UNION ALL
SELECT A.*
FROM purch A
LEFT JOIN purch B
on A.`Account Number` = B.`Account Number`
AND B.`Purchase Type` = 'A'
WHERE b.`Account Number` is null
AND A.`Purchase Type` = 'B'
You can use Exists on the same table:
select distinct AccountNumber , Date
from table1 outer_table
where PurchaseType = 'A' and not exists
(
select ID
from table1 inner_table
where
PurchaseType = 'B'
and inner_table.Date = outer_table.Date
and inner_table.AccountNumber = outer_table.AccountNumber
)
Fiddle: http://sqlfiddle.com/#!9/b84ecd/9
select id,sum(if(purchase_type='A',1,0)) as sumA,sum(if(purchase_type='B',1,0)) as sumB
from purchases
group by id
having sumA>0 and sumB=0
Fiddle: http://sqlfiddle.com/#!9/edf73f/16
and to get the two request in one:
select id,sum(if(purchase_type='A',1,0)) as sumA,sum(if(purchase_type='B',1,0)) as sumB
from purchases
group by id
having (sumA>0 and sumB=0) OR(sumA=0 and sumB>0 )
Fiddle: http://sqlfiddle.com/#!9/edf73f/18
You get a bit messed up here because MySQL's set arithmetic operations are incomplete. It has UNION and INTERSECT but not EXCEPT. If it had EXCEPT you could do
SELECT DISTINCT `Account Number` FROM purch WHERE `Purchase Type` = 'A'
EXCEPT /* MySQL fail! */
SELECT DISTINCT `Account Number` FROM purch WHERE `Purchase Type` = 'B'
and your problem would be solved.
So you can use the LEFT JOIN ... IS NULL query pattern. It's more verbose but works fine. (http://sqlfiddle.com/#!9/52c893/18/0)
SELECT suba.`Account Number`
FROM (
SELECT DISTINCT `Account Number`
FROM purch
WHERE `Purchase Type` = 'A'
) suba
LEFT JOIN (
SELECT DISTINCT `Account Number`
FROM purch
WHERE `Purchase Type` = 'B'
) subb ON suba.`Account Number` = subb.`Account Number`
WHERE subb.`Account Number` IS NULL
SELECT GROUP_CONCAT( DISTINCT A.account SEPARATOR ', ') AS "accounts"
FROM test A, test B
WHERE A.type='A'
AND A.id=B.id
AND A.date=B.date
AND A.date='2014-03-01'

SQL select and take data as column name

I have a database which store records like:
+----+---------------------+-------------+-----------------+
| id | user_name | status| date |
+----+---------------------+-------------+-----------------+
| 1 | A | Paid| 2016-10-11|
| 2 | B | Not Paid| 2016-10-12|
| 3 | C | Paid| 2016-10-12|
| 4 | A | Not Paid| 2016-10-13|
+----+---------------------+-------------+-----------------+
I wish to obtain the results like:
+----+---------------------+-------------+-----------------+-----------------+
| id | user_name | 2016-10-11| 2016-10-12 | 2016-10-13 |
+----+---------------------+-------------+-----------------+-----------------+
| 1 | A | Paid| NA| Not Paid|
| 2 | B | NA| Not Paid| NA|
| 3 | C | NA| Paid| Na|
+----+---------------------+-------------+-----------------+-----------------+
How can I query it to obtain the results like this?
PS: Poor English
FYI: I'm using mySQL as as DBMS and here is the create script:
CREATE TABLE `moneygame`.`pay_table`(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR(50),
`status` VARCHAR(50),
`p_date` DATE,
PRIMARY KEY (`id`)
);
If don't have a fixed number of dates then I wouldn't recommend doing what you are trying to do. Anyways, here's the solution to the problem.
create table p as select * from
(select 1 id, 'A' user_name, 'Paid' status, '2016-10-11' date union
select 2 id, 'B' user_name, 'Not Paid' status, '2016-10-12' date union
select 3 id, 'C' user_name, 'Paid' status, '2016-10-12' date union
select 4 id, 'A' user_name, 'Not Paid' status, '2016-10-13' date) t;
When dates are represented as columns
select user_name, ifnull(max(a),'NA') `2016-10-11`,
ifnull(max(b),'NA') `2016-10-12`, ifnull(max(c),'NA') `2016-10-13`
from (select user_name,
case when date = '2016-10-11' then status else null end a,
case when date = '2016-10-12' then status else null end b,
case when date = '2016-10-13' then status else null end c
from p group by user_name, date) s group by user_name;
When user names are represented as columns
Doing it this way should be optimal if you have a fixed number of users and an moving range of dates.
select date, ifnull(max(a),'NA') A,
ifnull(max(b),'NA') B, ifnull(max(c),'NA') C from
(select date,
case when user_name = 'A' then status else null end a,
case when user_name = 'B' then status else null end b,
case when user_name = 'C' then status else null end c
from p group by date, user_name) x group by date;
By the way if you still need to do dynamic columns, you should read this:
How to automate pivot tables in MySQL
MySQL pivot into dynamic number of columns
You can query like this
select user_name, max([2016-10-11]) as [2016-10-11], max([2016-10-12]) [2016-10-12],max([2016-10-13]) [2016-10-13] from #yourtable
pivot
(max(status) for date in ([2016-10-11],[2016-10-12],[2016-10-13])) p
group by user_name

MySQL show used index in query

For example I have created 3 index:
click_date - transaction table, daily_metric table
order_date - transaction table
I want to check does my query use index, I use EXPLAIN function and get this result:
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 668 | Using temporary; Using filesort |
| 2 | DERIVED | <derived3> | ALL | NULL | NULL | NULL | NULL | 645 | |
| 2 | DERIVED | <derived4> | ALL | NULL | NULL | NULL | NULL | 495 | |
| 4 | DERIVED | transaction | ALL | order_date | NULL | NULL | NULL | 291257 | Using where; Using temporary; Using filesort |
| 3 | DERIVED | daily_metric | range | click_date | click_date | 3 | NULL | 812188 | Using where; Using temporary; Using filesort |
| 5 | UNION | <derived7> | ALL | NULL | NULL | NULL | NULL | 495 | |
| 5 | UNION | <derived6> | ALL | NULL | NULL | NULL | NULL | 645 | Using where; Not exists |
| 7 | DERIVED | transaction | ALL | order_date | NULL | NULL | NULL | 291257 | Using where; Using temporary; Using filesort |
| 6 | DERIVED | daily_metric | range | click_date | click_date | 3 | NULL | 812188 | Using where; Using temporary; Using filesort |
| NULL | UNION RESULT | <union2,5> | ALL | NULL | NULL | NULL | NULL | NULL | |
+----+--------------+--------------+-------+---------------+------------+---------+------+--------+----------------------------------------------+
In EXPLAIN results I see, that index order_date of transaction table is not used, do I correct understand ?
Index click_date of daily_metric table was used correct ?
Please tell my how to understand from EXPLAIN result does my created index is used in query properly ?
My query:
SELECT
partner_id,
the_date,
SUM(clicks) as clicks,
SUM(total_count) as total_count,
SUM(count) as count,
SUM(total_sum) as total_sum,
SUM(received_sum) as received_sum,
SUM(partner_fee) as partner_fee
FROM (
SELECT
clicks.partner_id,
clicks.click_date as the_date,
clicks,
orders.total_count,
orders.count,
orders.total_sum,
orders.received_sum,
orders.partner_fee
FROM
(SELECT
partner_id, click_date, sum(clicks) as clicks
FROM
daily_metric WHERE DATE(click_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY partner_id , click_date) as clicks
LEFT JOIN
(SELECT
partner_id,
DATE(order_date) as order_dates,
SUM(order_sum) as total_sum,
SUM(customer_paid_sum) as received_sum,
SUM(partner_fee) as partner_fee,
count(*) as total_count,
count(CASE
WHEN status = 1 THEN 1
ELSE NULL
END) as count
FROM
transaction WHERE DATE(order_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY DATE(order_date) , partner_id) as orders ON orders.partner_id = clicks.partner_id AND clicks.click_date = orders.order_dates
UNION ALL SELECT
orders.partner_id,
orders.order_dates as the_date,
clicks,
orders.total_count,
orders.count,
orders.total_sum,
orders.received_sum,
orders.partner_fee
FROM
(SELECT
partner_id, click_date, sum(clicks) as clicks
FROM
daily_metric WHERE DATE(click_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY partner_id , click_date) as clicks
RIGHT JOIN
(SELECT
partner_id,
DATE(order_date) as order_dates,
SUM(order_sum) as total_sum,
SUM(customer_paid_sum) as received_sum,
SUM(partner_fee) as partner_fee,
count(*) as total_count,
count(CASE
WHEN status = 1 THEN 1
ELSE NULL
END) as count
FROM
transaction WHERE DATE(order_date) BETWEEN '2013-04-01' AND '2013-04-30'
GROUP BY DATE(order_date) , partner_id) as orders ON orders.partner_id = clicks.partner_id AND clicks.click_date = orders.order_dates
WHERE
clicks.partner_id is NULL
ORDER BY the_date DESC
) as t
GROUP BY the_date ORDER BY the_date DESC LIMIT 50 OFFSET 0
Although I can't explain what the EXPLAIN has dumped, I thought there must be an easier solution to what you have and came up with the following. I would suggest the following indexes to optimize your existing query for the WHERE date range and grouping by partner.
Additionally, when you have a query that uses a FUNCTION on a field, it doesn't take advantage of the index. Such as your DATE(order_date) and DATE(click_date). To allow the index to better be used, qualify the full date/time such as 12:00am (morning) up to 11:59pm. I would typically to this via
x >= someDate #12:00 and x < firstDayAfterRange.
in your example would be (notice less than May 1st which gets up to April 30th at 11:59:59pm)
click_date >= '2013-04-01' AND click_date < '2013-05-01'
Table Index
transaction (order_date, partner_id)
daily_metric (click_date, partner_id)
Now, an adjustment. Since your clicks table may have entries the transactions dont, and vice-versa, I would adjust this query to do a pre-query of all possible date/partners, then left-join to respective aggregate queries such as:
SELECT
AllParnters.Partner_ID,
AllParnters.the_Date,
coalesce( clicks.clicks, 0 ) Clicks,
coalesce( orders.total_count, 0 ) TotalCount,
coalesce( orders.count, 0 ) OrderCount,
coalesce( orders.total_sum, 0 ) OrderSum,
coalesce( orders.received_sum, 0 ) ReceivedSum,
coalesce( orders.partner_fee 0 ) PartnerFee
from
( select distinct
dm.partner_id,
DATE( dm.click_date ) as the_Date
FROM
daily_metric dm
WHERE
dm.click_date >= '2013-04-01' AND dm.click_date < '2013-05-01'
UNION
select
t.partner_id,
DATE(t.order_date) as the_Date
FROM
transaction t
WHERE
t.order_date >= '2013-04-01' AND t.order_date < '2013-05-01' ) AllParnters
LEFT JOIN
( SELECT
dm.partner_id,
DATE( dm.click_date ) sumDate,
sum( dm.clicks) as clicks
FROM
daily_metric dm
WHERE
dm.click_date >= '2013-04-01' AND dm.click_date < '2013-05-01'
GROUP BY
dm.partner_id,
DATE( dm.click_date ) ) as clicks
ON AllPartners.partner_id = clicks.partner_id
AND AllPartners.the_date = clicks.sumDate
LEFT JOIN
( SELECT
t.partner_id,
DATE(t.order_date) as sumDate,
SUM(t.order_sum) as total_sum,
SUM(t.customer_paid_sum) as received_sum,
SUM(t.partner_fee) as partner_fee,
count(*) as total_count,
count(CASE WHEN t.status = 1 THEN 1 ELSE NULL END) as COUNT
FROM
transaction t
WHERE
t.order_date >= '2013-04-01' AND t.order_date < '2013-05-01'
GROUP BY
t.partner_id,
DATE(t.order_date) ) as orders
ON AllPartners.partner_id = orders.partner_id
AND AllPartners.the_date = orders.sumDate
order by
AllPartners.the_date DESC
limit 50 offset 0
This way, the first query will be quick on the index to get all possible combinations from EITHER table. Then the left-join will AT MOST join to one row per set. If found, get the number, if not, I am applying COALESCE() so if null, defaults to zero.
CLARIFICATION.
Like you when building your pre-aggregate queries of "clicks" and "orders", the "AllPartners" is the ALIAS result of the select distinct of partners and dates within the date range you were interested in. The resulting columns of that where were "partner_id" and "the_date" respective to your next queries. So this is the basis of joining to the aggregates of "clicks" and "orders". So, since I have these two columns in the alias "AllParnters", I just grabbed those for the field list since they are LEFT-JOINed to the other aliases and may not exist in either/or the respective others.

Group values from different columns and Count

I have this table:
UNIQUE_ID | WINNER_ID | FINALIST_ID
________1 | ________1 | __________2
________2 | ________1 | __________3
________3 | ________3 | __________1
________4 | ________1 | __________2
And I need a list of all players (Winners and Finalists) and a COUNT of how many times they have got 1st or 2nd place.
In this case it would be:
PLAYER_ID | WINNER_TIMES | FINALIST_TIMES
________1 | ___________3 | _____________1
________2 | ___________0 | _____________2
________3 | ___________1 | _____________1
A similar question was already asked here (LINK), but I didn't understand the answer.
select coalesce(winner_id, finalist_id) as PLAYER_ID
, count(winner_id) as WINNER_TIMES
, count(finalist_id) as FINALIST_TIMES
from (
select winner_id
, null as finalist_id
from YourTable
union all
select null
, finalist_id
from YourTable
) as SubQueryAlias
group by
coalesce(winner_id, finalist_id)
Live example at SQL Fiddle.
Try this ::
Select
user_id as user,
winner_temp.count(1) as winning_count
finalist_temp.count(1) as runner_up_count
from
user_table
left join
(Select winner_id, count(1) from table group by winner_id) as winner_temp on (user_table.user_id = winner_temp.winner_id)
left join
(Select finalist_id, count(1) from table group by finalist_id) as finalist_temp on
(user_table.user_id = finalist_temp.finalist_id)