How to Join the subquery using JSON document within MySQL? - mysql

The Mysql version is 8.0.18-commercial. The primary key of the table is id.
I have written the following query which displays hostname and details columns
select hostname, details from table t1;
hostname: abc123
details:
[
{
"Msg": "Job Running",
"currentTask": "IN_PROGRESS",
"activityDate": "2020-07-20 16:25:15"
},
{
"Msg": "Job failed",
"currentTask": "IN_PROGRESS",
"activityDate": "2020-07-20 16:35:24"
}
]
I want the Msg value only from the element having most recent activityDate
My desired output is displaying hostname alongwith Msg of the element with latest date :
hostname Msg
abc123 Job failed
I have written following query and it is running successfully but not displaying anything at all. Morever, it is taking 17secs to execute.
select hostname,
(select Msg
from (
select x.*, row_number() over(partition by t.id order by x.activityDate) rn
from table1 t
cross join json_table(
t.audits,
'$[*]' columns(
Msg varchar(50) path '$.Msg',
activityDate datetime path '$.activityDate'
)
) x
) t
where rn = 1) AS Msg
from table1;

You need to fix the JSON's format by removing the commas at the end
of the lines starting with "activityDate" keys
A conversion function such as STR_TO_DATE() should be applied to
the derived activityDate columns in order to get date ordered(not
characterwise) results.
A subquery is not needed through putting ROW_NUMBER() analytic
function next to the ORDER BY Clause( with descending order ), and adding a LIMIT 1 Clause
at the end of the query
So, you can rewrite the query as
SELECT t1.hostname,
j.Msg
FROM t1
CROSS JOIN
JSON_TABLE(details, '$[*]'
COLUMNS (
Msg VARCHAR(100) PATH '$.Msg',
activityDate VARCHAR(100) PATH '$.activityDate'
)
) j
ORDER BY ROW_NUMBER()
OVER ( -- PARTITION BY id
ORDER BY STR_TO_DATE(j.activityDate, '%Y-%m-%d %H:%i:%S') DESC)
LIMIT 1
Demo
Update :
For the case of having several id values, you may consider using the ROW_NUMBER() function within a subquery and filter out the values returning equal to 1 in the main query :
SELECT id, Msg
FROM
(
SELECT t1.*, j.Msg,
ROW_NUMBER()
OVER (PARTITION BY id
ORDER BY STR_TO_DATE(j.activityDate, '%Y-%m-%d %H:%i:%S') DESC) AS rn
FROM t1
CROSS JOIN
JSON_TABLE(details, '$[*]'
COLUMNS (
Msg VARCHAR(100) PATH '$.Msg',
activityDate VARCHAR(100) PATH '$.activityDate'
)
) j
) q
WHERE rn= 1
Demo
One another method uses ROW_NUMBER() function together with LIMIT clause contains Correlated Subquery, and works for records with multiple id values :
SELECT t.id,
( SELECT j.Msg
FROM t1
CROSS JOIN
JSON_TABLE(details, '$[*]'
COLUMNS (
Msg VARCHAR(100) PATH '$.Msg',
activityDate VARCHAR(100) PATH '$.activityDate'
)
) j
WHERE t1.id = t.id
ORDER BY ROW_NUMBER()
OVER (ORDER BY STR_TO_DATE(j.activityDate, '%Y-%m-%d %H:%i:%S') DESC)
LIMIT 1 ) AS Msg
FROM t1 AS t
Demo

Maybe I'm old school, but the date field should be stored as a separate field either in additional to the JSON, to allow for easy queries.
Is the ID auto increment, and is the data inserted in timestamp order? If yes, then you can run a query like this to give you the last row for each hostname:
SELECT id, hostname, details
FROM table t1
WHERE NOT EXISTS (SELECT 1 FROM table t2 WHERE t2.hostname = t1.hostname AND t2.id > t1.id) ;

Related

mysql query to group record and generate custom columns

So i have a table named "log" with the following columns,
id, endpoint ,response ,group
SAMPLE DATA.
1. endpoint1 ,{"last_name":"data here"} ,1234
2. endpoint2 ,{"first_name":"data here"} ,1234
3. endpoint3 ,{"dob":"12-21-2301"} ,1234
what I want to achieve is to write a query that can generate a record grouped by the "
group" column and the final output should be something like this.
{"last_name","data here","first_name":"data here","dob":"12-21-2301"}
for each record with each key been a column.
Thank you
WITH RECURSIVE
cte1 AS ( SELECT response,
`group`,
ROW_NUMBER() OVER (PARTITION BY `group`) rn
FROM log ),
cte2 AS ( SELECT response,
`group`,
rn
FROM cte1
WHERE rn = 1
UNION ALL
SELECT JSON_MERGE_PRESERVE(cte1.response, cte2.response),
cte1.`group`,
cte1.rn
FROM cte2
JOIN cte1 USING (`group`)
WHERE cte2.rn + 1 = cte1.rn )
SELECT DISTINCT
FIRST_VALUE(response) OVER (PARTITION BY `group` ORDER BY rn DESC) responses,
`group`
FROM cte2;
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=913b1923d7d5dbc7e42baeefb6e6ec86

SQL Query about percentage selection

I am trying to write a query for a condition:
If >=80 percent (4 or more rows as 4/5*100=80%) of the top 5 recent rows(by Date Column), for a KEY have Value =A or =B, then change the flag from fail to pass for the entire KEY.
Here is the input and output sample:
I have highlighted recent rows with green colour in the sample.
Can someone help me in this?
I tried till finding the top 5 recent rows by the foll code:
select * from(
select *, row_number() over (partition by "KEY") as 'RN' FROM (
select * from tb1
order by date desc))
where "RN"<=5
Couldnt figure what to be done after this
Test this:
WITH
-- enumerate rows per key group
cte1 AS ( SELECT *,
ROW_NUMBER() OVER (PARTITION BY `key` ORDER BY `date` DESC) rn
FROM sourcetable ),
-- take 5 recent rows only, check there are at least 4 rows with A/B
cte2 AS ( SELECT `key`
FROM cte1
WHERE rn <= 5
GROUP BY `key`
HAVING ( SUM(`value` = 'A') >= 4
OR SUM(`value` = 'B') >= 4 )
-- AND SUM(rn = 5) )
-- update rows with found key values
UPDATE sourcetable
JOIN cte2 USING (`key`)
SET flag = 'PASS';
5.7 version – Ayn76
Convert CTEs to subqueries. Emulate ROW_NUMBER() using user-defined variable.

Get the distinct rows with oldest date

I have a table like this:
MyTable:
id: pk
numero: varchar
data_modif: timestamp
...
I have multiple records with same value in numero and I need to return each distinct numero record with oldest data_modif. How can I do this?
This sounds like aggregation:
select numero, min(data_modif)
from mytable
group by numero;
If you want the entire row, then window functions are one method:
select t.*
from (select t.*,
row_number() over (partition by numero order by data_modif asc) as seqnum
from mytable t
) t
where seqnum = 1;
EDIT:
In an old version of MySQL, you woudl use:
select t.*
from t
where t.data_modif = (select min(t2.data_modif)
from t t2
where t2.numero = t.numero
);

Mysql - Accumulatively count the total on a row by row basis

I'm trying in MySql to count the number of users created each day and then get an accumulative figure on a row by row basis. I have followed other suggestions on here, but I cannot seem to get the accumulation to be correct.
The problem is that it keeps counting from the base number of 200 and not taking account of previous rows.
Where was I would expect it to return
My Sql is as follows;
SELECT day(created_at), count(*), (#something := #something+count(*)) as value
FROM myTable
CROSS JOIN (SELECT #something := 200) r
GROUP BY day(created_at);
To create the table and populate it you can use;
CREATE TABLE myTable (
id INT AUTO_INCREMENT,
created_at DATETIME,
PRIMARY KEY (id)
);
INSERT INTO myTable (created_at)
VALUES ('2018-04-01'),
('2018-04-01'),
('2018-04-01'),
('2018-04-01'),
('2018-04-02'),
('2018-04-02'),
('2018-04-02'),
('2018-04-03'),
('2018-04-03');
You can view this on SqlFiddle.
Use a subquery:
SELECT day, cnt, (#s := #s + cnt)
FROM (SELECT day(created_at) as day, count(*) as cnt
FROM myTable
GROUP BY day(created_at)
) d CROSS JOIN
(SELECT #s := 0) r;
GROUP BY and variables have not worked together for a long time. In more recent versions, ORDER BY also needs a subquery.

How many different ways are there to get the second row in a SQL search?

Let's say I was looking for the second most highest record.
Sample Table:
CREATE TABLE `my_table` (
`id` int(2) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`value` int(10),
PRIMARY KEY (`id`)
);
INSERT INTO `my_table` (`id`, `name`, `value`) VALUES (NULL, 'foo', '200'), (NULL, 'bar', '100'), (NULL, 'baz', '0'), (NULL, 'quux', '300');
The second highest value is foo. How many ways can you get this result?
The obvious example is:
SELECT name FROM my_table ORDER BY value DESC LIMIT 1 OFFSET 1;
Can you think of other examples?
I was trying this one, but LIMIT & IN/ALL/ANY/SOME subquery is not supported.
SELECT name FROM my_table WHERE value IN (
SELECT MIN(value) FROM my_table ORDER BY value DESC LIMIT 1
) LIMIT 1;
Eduardo's solution in standard SQL
select *
from (
select id,
name,
value,
row_number() over (order by value) as rn
from my_table t
) t
where rn = 1 -- can pick any row using this
This works on any modern DBMS except MySQL. This solution is usually faster than solutions using sub-selects. It also can easily return the 2nd, 3rd, ... row (again this is achievable with Eduardo's solution as well).
It can also be adjusted to count by groups (adding a partition by) so the "greatest-n-per-group" problem can be solved with the same pattern.
Here is a SQLFiddle to play around with: http://sqlfiddle.com/#!12/286d0/1
This only works for exactly the second highest:
SELECT * FROM my_table two
WHERE EXISTS (
SELECT * FROM my_table one
WHERE one.value > two.value
AND NOT EXISTS (
SELECT * FROM my_table zero
WHERE zero.value > one.value
)
)
LIMIT 1
;
This one emulates a window function rank() for platforms that don't have them. It can also be adapted for ranks <> 2 by altering one constant:
SELECT one.*
-- , 1+COALESCE(agg.rnk,0) AS rnk
FROM my_table one
LEFT JOIN (
SELECT one.id , COUNT(*) AS rnk
FROM my_table one
JOIN my_table cnt ON cnt.value > one.value
GROUP BY one.id
) agg ON agg.id = one.id
WHERE agg.rnk=1 -- the aggregate starts counting at zero
;
Both solutions need functional self-joins (I don't know if mysql allows them, IIRC it only disallows them if the table is the target for updates or deletes)
The below one does not need window functions, but uses a recursive query to enumerate the rankings:
WITH RECURSIVE agg AS (
SELECT one.id
, one.value
, 1 AS rnk
FROM my_table one
WHERE NOT EXISTS (
SELECT * FROM my_table zero
WHERE zero.value > one.value
)
UNION ALL
SELECT two.id
, two.value
, agg.rnk+1 AS rnk
FROM my_table two
JOIN agg ON two.value < agg.value
WHERE NOT EXISTS (
SELECT * FROM my_table nx
WHERE nx.value > two.value
AND nx.value < agg.value
)
)
SELECT * FROM agg
WHERE rnk = 2
;
(the recursive query will not work in mysql, obviously)
You can use inline initialization like this:
select * from (
select id,
name,
value,
#curRank := #curRank + 1 AS rank
from my_table t, (SELECT #curRank := 0) r
order by value desc
) tb
where tb.rank = 2
SELECT name
FROM my_table
WHERE value < (SELECT max(value) FROM my_table)
ORDER BY value DESC
LIMIT 1
SELECT name
FROM my_table
WHERE value = (
SELECT min(r.value)
FROM (
SELECT name, value
FROM my_table
ORDER BY value DESC
LIMIT 2
) r
)
LIMIT 1