Update multiple columns with GROUP_CONCAT not working as expected - mysql

Help wanted fellas! I have a stored precedure that updates the columns of a calendar-style table from another one. The records of the second one should be stored in the calendar grouped according its date and individual, however I cannot achieve it in a single UPDATE sentence; I had to make these ugly series of column-by-column updates. If you have any idea, it will be appreciated. Code is below:
DECLARE thisYearMonth VARCHAR(7);
SET thisYearMonth = '2016-09';
UPDATE calendar CAL
INNER JOIN (
SELECT checkinout.userid,
checkinout.checktime,
GROUP_CONCAT(checkinout.checktime) ORDER BY checkinout.id SEPARATOR ' | ') AS checktime
FROM checkinout
INNER JOIN calendar ON calendar.userid = checkinout.userid
WHERE checkinout.userid = calendar.userid
AND DATE(checktime) = CONCAT(thisYearMonth, '-', '01')
GROUP BY userid
) CHK ON CHK.userid = CAL.userid
SET CAL.day1 = CHK.checkstatus;
UPDATE calendar CAL
INNER JOIN (
SELECT checkinout.userid,
checkinout.checktime,
GROUP_CONCAT(checkinout.checktime ORDER BY checkinout.id SEPARATOR ' | ') AS checktime
FROM checkinout
INNER JOIN calendar ON calendar.userid = checkinout.userid
WHERE checkinout.userid = calendar.userid
AND DATE(checktime) = CONCAT(thisYearMonth, '-', '02')
GROUP BY userid
) CHK ON CHK.userid = CAL.userid
SET CAL.day2 = CHK.checkstatus;
And so on until day n. The question is how do I make it work like just a single UPDATE that does not repeats in all remaning calendar rows the same value as the row "day1", i.e. day1 gets 10:00, day2 gets 10:00 and so on. Here's the code that generates this behavior:
UPDATE calendar CAL
INNER JOIN (
SELECT checkinout.userid,
checkinout.checktime,
GROUP_CONCAT(checkinout.checktime) ORDER BY checkinout.id SEPARATOR ' | ') AS checktime
FROM checkinout
INNER JOIN calendar ON calendar.userid = checkinout.userid
WHERE checkinout.userid = calendar.userid
AND DATE(checktime) = CONCAT(thisYearMonth, '-', '01')
GROUP BY userid
) CHK ON CHK.userid = CAL.userid
SET CAL.day1 = CHK.checkstatus,
CAL.day2 = CHK.checkstatus,
CAL.day3 = CHK.checkstatus
...;
EDIT: According to Gordon Linoff this can be achieved using conditional aggregation, however it only brings one record per row and not all of the records that correspond to every userid on each day.
Like this:
Results using conditional aggregation
Expected result:
Result updating row by row individually
I'm posting this sample data in the code below for you to verify my results
DROP TABLE IF EXISTS tmp_checkinout;
CREATE TEMPORARY TABLE tmp_checkinout
(`id` int, `userid` int, `checktime` datetime, `checkstatus` varchar(8));
INSERT INTO tmp_checkinout
(`id`, `userid`, `checktime`, `checkstatus`)
VALUES
(1 , 3, '2016-8-1 07:49:23', 'SHRP'),
(2 , 2, '2016-8-1 08:01:40', 'SHRP'),
(3 , 1, '2016-8-1 08:49:07', 'SHRP'),
(4 , 5, '2016-8-1 09:14:19', 'LATE'),
(5 , 3, '2016-8-2 07:48:06', 'SHRP'),
(6 , 2, '2016-8-2 08:04:03', 'SHRP'),
(7 , 4, '2016-8-2 08:04:12', 'SHRP'),
(8 , 1, '2016-8-2 08:49:07', 'SHRP'),
(9 , 5, '2016-8-2 09:10:31', 'TOLR'),
(10, 3, '2016-8-2 10:40:16', 'EXTN'),
(11, 3, '2016-8-3 07:48:32', 'SHRP'),
(12, 2, '2016-8-3 08:05:34', 'SHRP'),
(13, 4, '2016-8-3 08:12:23', 'LATE'),
(14, 5, '2016-8-3 09:08:52', 'TOLR'),
(15, 1, '2016-8-3 10:23:41', 'EXTN'),
(16, 3, '2016-8-4 07:49:15', 'SHRP'),
(17, 2, '2016-8-4 08:05:39', 'SHRP'),
(18, 1, '2016-8-4 08:36:44', 'SHRP'),
(19, 4, '2016-8-4 08:07:22', 'TOLR'),
(20, 5, '2016-8-4 09:22:34', 'LATE'),
(21, 3, '2016-8-5 07:51:42', 'SHRP'),
(22, 4, '2016-8-5 08:11:33', 'LATE'),
(23, 2, '2016-8-5 08:16:54', 'LATE'),
(24, 1, '2016-8-5 08:53:20', 'SHRP'),
(25, 5, '2016-8-5 09:26:02', 'LATE'),
(26, 3, '2016-8-2 10:52:44', 'EXTN')
;
DROP TABLE IF EXISTS tmp_calendar;
CREATE TEMPORARY TABLE tmp_calendar
(`userid` int, `day1` varchar(40), `day2` varchar(40), `day3` varchar(40), `day4` varchar(40), `day5` varchar(40));
INSERT INTO tmp_calendar
(`userid`)
VALUES (1), (2), (3), (4), (5);
Using Gordon Linoff's answer, this is the code that generates the results as described in picture 1:
UPDATE tmp_calendar AS cal
INNER JOIN
(SELECT id, userid,
CASE WHEN DAY(checktime) = 1 THEN CONCAT('ID:', id, ', ', checkstatus, ', ', checktime) ELSE NULL END AS timeandstatus_01,
CASE WHEN DAY(checktime) = 2 THEN CONCAT('ID:', id, ', ', checkstatus, ', ', checktime) ELSE NULL END AS timeandstatus_02,
CASE WHEN DAY(checktime) = 3 THEN CONCAT('ID:', id, ', ', checkstatus, ', ', checktime) ELSE NULL END AS timeandstatus_03,
CASE WHEN DAY(checktime) = 4 THEN CONCAT('ID:', id, ', ', checkstatus, ', ', checktime) ELSE NULL END AS timeandstatus_04,
CASE WHEN DAY(checktime) = 5 THEN CONCAT('ID:', id, ', ', checkstatus, ', ', checktime) ELSE NULL END AS timeandstatus_05
FROM tmp_checkinout
WHERE DATE_FORMAT(checktime, '%Y-%m') = '2016-08'
ORDER BY id ASC) AS chk ON chk.userid = cal.userid
SET cal.day1 = chk.timeandstatus_01,
cal.day2 = chk.timeandstatus_02,
cal.day3 = chk.timeandstatus_03,
cal.day4 = chk.timeandstatus_04,
cal.day5 = chk.timeandstatus_05
WHERE cal.userid = chk.userid;
Thanks for any pointer :-)

I think you need conditional aggregation in the subquery:
UPDATE calendar CAL INNER JOIN
(SELECT ch.userid, ch.checktime,
GROUP_CONCAT(CASE WHEN DAY(checktime) = 01 THEN ch.checktime END ORDER BY ch.id SEPARATOR ' | ') AS checktime_01,
GROUP_CONCAT(CASE WHEN DAY(checktime) = 02 THEN ch.checktime END ORDER BY ch.id SEPARATOR ' | ') AS checktime_02,
. . .
FROM checkinout ch INNER JOIN
calendar ca
ON ca.userid = ch.userid
WHERE date_format(checktime, '%Y-%m') = thisYearMonth
GROUP BY userid
) CHK
ON CHK.userid = CAL.userid
SET CAL.day1 = CHK.checkstatus_01,
CAL.day2 = CHK.checkstatus_02;

Related

MYSQL 5.6 get latest data of each user

My Database table is as shown below. I need to get latest mark of each student. Latest entry is the row with maximum udate and maximum oder. (The oder will be incremented by one on each entry with same date)
In my example, I have two students Mujeeb, Zakariya and two subjects ENGLISH, MATHS. I need to get latest mark of each student for each subject. My expectd result is as follows
My sample data is
DROP TABLE IF EXISTS `students`;
CREATE TABLE IF NOT EXISTS `students` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`udate` date NOT NULL,
`oder` int(11) NOT NULL,
`name` varchar(20) NOT NULL,
`Subject` varchar(20) NOT NULL,
`mark` int(11) NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=13 DEFAULT CHARSET=latin1;
INSERT INTO `students` (`uid`, `udate`, `oder`, `name`, `Subject`, `mark`) VALUES
(1, '2021-08-01', 1, 'Mujeeb', 'ENGLISH', 10),
(2, '2021-08-01', 1, 'Zakariya', 'ENGLISH', 20),
(3, '2021-08-10', 2, 'Mujeeb', 'ENGLISH', 50),
(4, '2021-08-11', 2, 'Zakariya', 'ENGLISH', 60),
(5, '2021-08-02', 1, 'Mujeeb', 'ENGLISH', 100),
(6, '2021-08-03', 1, 'Zakariya', 'ENGLISH', 110),
(7, '2021-08-10', 1, 'Mujeeb', 'ENGLISH', 500),
(8, '2021-08-11', 1, 'Zakariya', 'ENGLISH', 600),
(9, '2021-08-01', 2, 'Mujeeb', 'MATHS', 100),
(10, '2021-08-01', 2, 'Zakariya', 'MATHS', 75),
(11, '2021-08-10', 3, 'Mujeeb', 'MATHS', 50),
(12, '2021-08-11', 3, 'Zakariya', 'MATHS', 60);
Use NOT EXISTS:
SELECT s1.*
FROM students s1
WHERE NOT EXISTS (
SELECT 1
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
AND (s2.udate > s1.udate OR (s2.udate = s1.udate AND s2.oder > s1.oder))
);
Or with a correlated subquery in the WHERE clause:
SELECT s1.*
FROM students s1
WHERE s1.uid = (
SELECT s2.uid
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
ORDER BY s2.udate DESC, s2.oder DESC LIMIT 1
);
See the demo.
As ROW_NUMBER() function doesn't work at lower version of MySQL, So alternate way of row_number() is used for this solution.
-- MySQL (v5.6)
SELECT p.uid, p.udate, p.oder, p.name, p.Subject, p.mark
FROM (SELECT #row_no := IF((#prev_val = t.name && #prev_val1 = t.Subject), #row_no + 1, 1) AS row_number
, #prev_val := t.name AS name
, #prev_val1 := t.Subject AS Subject
, t.mark
, t.oder
, t.uid
, t.udate
FROM students t,
(SELECT #row_no := 0) x,
(SELECT #prev_val := '') y,
(SELECT #prev_val1 := '') z
ORDER BY t.name, t.Subject, t.udate DESC, t.oder DESC ) p
WHERE p.row_number = 1
ORDER BY p.name, p.Subject;
Please check the url http://sqlfiddle.com/#!9/b5befe/18

How can I rewrite this to remove select statement joins?

As it is now each time I am selecting from Bill in my actual database I am searching over 1 million rows. There has to be a better way to write this query, but I haven't been able to figure it out. There has to be some way I can write this that doesn't join using the select statements.
If you notice I have some form of this join three seperate times. What should I do to remove this join?
join
(
select b.year
, sum(bli.amount) amt
from Bill b
join BillLine bli
on bli.bill_id = b.id
where bli.payment = 0
and bli.refund = 0
and bli.type = 3
group
by b.year
) org
on org.year = b.year
select b.Year,
Round(case when b.year = (select collectoryear -1 from Entity) then 0 else beg.amt end,2) Beginning,
Round(case when b.year >= (select collectoryear -1 from Entity) then ifnull(org.amt,0) else 0 end,2) Additions,
Round(case when b.year = (select collectoryear -1 from Entity) then 0 else beg.amt end + case when b.year >= (select collectoryear -1 from Entity) then ifnull(org.amt,0) else 0 end - ending.amt ,2) Collections,
Round(ending.amt,2) Ending
from Bill b
left join Levy l on l.year = b.year
join Entity e on b.year = e.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.payment = 0 and bli.refund = 0 and bli.type = 3 group by b.year) org on org.year = b.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.type = 2 group by b.year ) beg on beg.year = b.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.type = 2 group by b.year ) ending on ending.year = b.year
where b.year > (select collectoryear -11 from Entity)
and b.year < (select collectoryear from Entity)
group by b.year
order by Year desc ;
Here is the db-fiddle databases
create table Bill
( id varchar(20) PRIMARY KEY
, year varchar(20) FOREIGN KEY
);
Insert into Bill (id, year) values
(1, 2020),
(2, 2020),
(3, 2020),
(4, 2019),
(5, 2019),
(6, 2018),
(7, 2017),
(8, 2016),
(9, 2015),
(10, 2013);
create table BillLine
( id varchar(20) PRIMARY KEY
, bill_id varchar(20) FOREIGN KEY
, amount varchar(20)
, refund varchar(20)
, payment varchar(20)
, type varchar(20)
);
Insert into BillLine (id, bill_id, amount, refund, payment, type) values
(1, 10, 100.00, 0, 0, 3),
(2, 9, 250.00, 1, 1, 5),
(3, 8, 102.00, 0, 0, 3),
(4, 7, 85.00, 1, 1, 5),
(5, 6, 20.00, 0, 0, 3),
(6, 5, 43.75, 0, 1, 2),
(7, 4, 22.22, 0, 0, 3),
(8, 3, 125.25, 0, 1, 2),
(9, 2, 77.70, 0, 0, 3),
(10, 1, 100.75, 1, 1, 5);
create table Entity
( id varchar(20) PRIMARY KEY
, collectoryear varchar(20)
, year varchar(20)
);
Insert into Entity (id, collectoryear, year) values (1, 2021, 2020);
create table Levy
( id varchar(20) PRIMARY KEY, county varchar(20), year varchar(20)
);
Insert into Levy (id, county, year) values (1, null, null);

how to make a apriori table with dynamicaly mysql

i have 3 tables like this
create table order_match
(
id int(10) PRIMARY KEY not null,
order_status_id int(10) not null
);
create table order_match_detail
(
id int(10) PRIMARY KEY not null,
order_match_id int(10) not null,
product_id int(10) NOT NULL
);
create table product
(
id int(10) PRIMARY KEY not null,
name varchar(255) not null
);
Insert into order_match (id, order_status_id)
select 1, 6 union all
select 2, 7 union all
select 3, 6 union all
select 4, 6;
Insert into order_match_detail (id, order_match_id, product_id)
select 1, 1, 147 union all
select 2, 2, 148 union all
select 3, 3, 147 union all
select 4, 4, 149 union all
select 5, 4, 147;
Insert into product (id, name)
select 147, 'orange' union all
select 148, 'carrot' union all
select 149, 'Apple';
with order_match.id = order_match_detail.order_match_id
and order_match_detail.product_id = product.id
i want to make the data where order_status_id not in 7 then it's success transaction and from that success transaction, if the transaction buy apple, then the column of apple contain 1 else if not buying, then 0 and this is my expected results, i want to make this data to analyze in
id (in order_match) | Orange | Carrot | Apple
1 1 0 0
3 1 0 0
4 1 0 1
with that problem i can solve it with this query
select om.id,
count(DISTINCT case when omd.product_id = 147 THEN 1 END) Orange,
count(DISTINCT case when omd.product_id = 148 THEN 1 END) Carrot,
count(DISTINCT case when omd.product_id = 149 THEN 1 END) Apple
from order_match om
left join order_match_detail omd
on om.id = omd.order_match_id
where om.order_status_id in (4, 5, 6, 8)
group by om.id
the real problem is, in my real database, it's contain 1550 product_id, how to make it automatically so no need to input manual the product_id untill 1550 product_id
this is the fiddle https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=c0eb7fe1b012ab1c909d37e325a8d434
i've tried the new queries like this and it still wrong
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'count(case when product.name = ''',
product.name,
''' then 1 end) AS ',
replace(product.name, ' ', '')
)
) INTO #sql
from product;
SET #sql = CONCAT('SELECT omd.order_match_id, ', #sql, ' from order_match_detail omd
left join order_match om
on omd.order_match_id = om.id
left join product p
on omd.product_id = p.id
where om.order_status_id in (4, 5, 6, 8)
group by omd.order_match_id');
PREPARE stmt FROM #sql;
EXECUTE stmt;
your query is almost correct you were missing on alias instead of p you should use 'product' in second query, it would work
SET group_concat_max_len = 1000000;
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'count(case when product.name = ''',
product.name,
''' then 1 end) AS ',
'"',replace(product.name, ' ', ''),'"'
)
) INTO #sql
from product;
SET #sql = CONCAT('SELECT omd.order_match_id as id, ', #sql, ' from order_match_detail omd
left join order_match om
on omd.order_match_id = om.id
left join product product
on omd.product_id = product.id
where om.order_status_id in (4, 5, 6, 8)
group by omd.order_match_id');
PREPARE stmt FROM #sql;
EXECUTE stmt;
You can remove DISTINCT from GROUP_CONCAT(DISTINCT if you are sure that product id's is having unique names for each id's
Then i would get the exact result as you mentioned in question else just you will see, Apple column first and Orange column last, overall result is as expected.

mysql sum top 5 scores per player and put in ranking

I want to make a ranking with the best 5 scores of the players summed and put in a ranking. Players have more or less then 5 scores.
For now I have the following query:
set #count:=0, #player:='';
SELECT SEIZOEN
, WEDSTRIJDTYPE
, ATLEET_ID
, GROEP_NAAM
, GROEP_OMS
, SUM(PUNTEN_WC) as WC_RESULT
FROM
( SELECT ATLEET_ID
, PUNTEN_WC
, SEIZOEN
, WEDSTRIJDTYPE
, GROEP_NAAM
, GROEP_OMS
FROM
( SELECT PUNTEN_WC
, SEIZOEN
, WEDSTRIJDTYPE
, GROEP_NAAM
, GROEP_OMS
, #count := if (#player != ATLEET_ID,0,#count + 1) as count
, #player := ATLEET_ID as ATLEET_ID
FROM
( SELECT ATLEET_ID
, PUNTEN_WC
, k.SEIZOEN
, k.WEDSTRIJDTYPE
, g.GROEP_NAAM
, g.GROEP_OMS
FROM RESULTATEN r
LEFT
JOIN KALENDER k
ON r.KALENDER_ID = k.KALENDER_ID
JOIN GROEP_SNB g
ON r.GROEP_NAAM = g.GROEP_NAAM
JOIN SKIER s
ON r.ATLEET_ID = s.SKIER_ID
WHERE k.WEDSTRIJDTYPE = 'Dutch Cup snowboard '
AND k.SEIZOEN = '2016-2017'
order
by ATLEET_ID
, PUNTEN_WC DESC
) x
) y
where count < 6
) z
GROUP
BY ATLEET_ID
ORDER
BY GROEP_NAAM
, WC_RESULT DESC
LIMIT 0,2000;
The problem is this query doesn't take the five best scores of each player.
When I run the most inner query it's sorts the scores fine.
SELECT ATLEET_ID
, PUNTEN_WC
, k.SEIZOEN
, k.WEDSTRIJDTYPE
, g.GROEP_NAAM
, g.GROEP_OMS
FROM RESULTATEN r
LEFT
JOIN KALENDER k
ON r.KALENDER_ID = k.KALENDER_ID
JOIN GROEP_SNB g
ON r.GROEP_NAAM = g.GROEP_NAAM
JOIN SKIER s
ON r.ATLEET_ID = s.SKIER_ID
WHERE k.WEDSTRIJDTYPE = 'Dutch Cup snowboard '
AND k.SEIZOEN = '2016-2017'
order
by ATLEET_ID
, PUNTEN_WC DESC
I've put a count on the records so I can limit it to best of 5. But then the trouble starts. With the second query the highscores are still ordered correctly but the count-field is not 0 to 5?
So when I put the third query in it stops when count-field is 5, but I want a maximum of 5 scores per player.
You can sum the top N values for each person with this query:
Creating a simple test table and populating it -
CREATE TABLE Scores
(`id` int, `playerName` varchar(16), `score` int)
;
INSERT INTO Scores
(`id`, `playerName`, `score`)
VALUES
(1, 'Joe', 5),
(2, 'Joe', 5),
(3, 'Joe', 5),
(4, 'Joe', 1),
(5, 'Joe', 10),
(6, 'Joe', 10),
(7, 'Joe', 15),
(8, 'Bob', 5),
(9, 'Bob', 5),
(10, 'Bob', 2),
(11, 'Bob', 10),
(12, 'Bob', 3),
(13, 'Bob', 10),
(14, 'Bob', 20)
;
The query:
SET #score_rank := 0;
SET #current_player = '';
SET #topN = 5;
Select playerName, SUM(score)
From (Select playerName, score,
#score_rank := IF(#current_player = playerName, (#score_rank + 1), 1) AS score_rank,
#current_player := playerName
From Scores
Order By playerName, score DESC) sorted
Where score_rank <= #topN
Group By playerName;
The inner query assigns values to two variables - #current_player and #score_rank. If the #current_player value matches playerName, it increments #score_rank, otherwise it sets the #score_rank to 1. By doing this, we can grab only the top 5 for each player. The outer query then sums those top 5 scores. You can change the value of #topN if you want to sum a different set (like top 10).
Results with the above sample table:
playerName SUM(score)
---------- ----------
Bob 50
Joe 45
See it here: http://rextester.com/CLO11640

Topological sorting in sql

I am resolving dependency between some objects in a table.
I have to do something with objects in order their dependency.
For example, the first object doesn't depend on any object. The second and third ones depends on first one and so on. I have to use topological sorting.
Could someone show the sample of implementation so sorting in t-sql.
I have a table:
create table dependency
(
DependencyId PK
,ObjectId
,ObjectName
,DependsOnObjectId
)
I want to get
ObjectId
ObjectName
SortOrder
Thank you.
It seams, it works:
declare #step_no int
declare #dependency table
(
DependencyId int
,ObjectId int
,ObjectName varchar(100)
,DependsOnObjectId int
,[rank] int NULL
,degree int NULL
);
insert into #dependency values (5, 5, 'Obj 5', 2, NULL, NULL)
insert into #dependency values (6, 6, 'Obj 6', 7, NULL, NULL)
insert into #dependency values (2, 2, 'Obj 2', 1, NULL, NULL)
insert into #dependency values (3, 3, 'Obj 3', 1, NULL, NULL)
insert into #dependency values (1, 1, 'Obj 1', 1, NULL, NULL)
insert into #dependency values (4, 4, 'Obj 4', 2, NULL, NULL)
insert into #dependency values (7, 7, 'Obj 7', 2, NULL, NULL)
update #dependency set rank = 0
-- computing the degree of the nodes
update d set d.degree =
(
select count(*) from #dependency t
where t.DependsOnObjectId = d.ObjectId
and t.ObjectId <> t.DependsOnObjectId
)
from #dependency d
set #step_no = 1
while 1 = 1
begin
update #dependency set rank = #step_no where degree = 0
if (##rowcount = 0) break
update #dependency set degree = NULL where rank = #step_no
update d set degree = (
select count(*) from #dependency t
where t.DependsOnObjectId = d.ObjectId and t.ObjectId != t.DependsOnObjectId
and t.ObjectId in (select tt.ObjectId from #dependency tt where tt.rank = 0))
from #dependency d
where d.degree is not null
set #step_no = #step_no + 1
end
select * from #dependency order by rank
You have a simple tree structure with only one path to each ObjectId so labeling based off number of DependsOnObjectId links traversed gives only one answer and a good enough answer to process the right stuff first. This is easy to do with a common table expression and has the benefit of easy portability:
with dependency_levels as
(
select ObjectId, ObjectName, 0 as links_traversed
from dependency where DependsOnObjectId is null
union all
select ObjectId, ObjectName, links_traversed+1
from dependecy
join dependency_levels on dependency.DependsOnObjectId = dependency_levels.ObjectId
)
select ObjectId, ObjectName, links_traversed
from dependency_levels
order by links_traversed