MySQL get ids of the range between two conditions - mysql

Let's say I've a table
+----+------------+
| id | condition |
+----+------------+
| 1 | open |
+----+------------+
| 2 | content |
+----+------------+
| 3 | content |
+----+------------+
| 4 | close |
+----+------------+
| 5 | nocontentx |
+----+------------+
| 6 | nocontenty |
+----+------------+
| 7 | open |
+----+------------+
| 8 | content |
+----+------------+
| 9 | close |
+----+------------+
| 10 | nocontentz |
+----+------------+
| 11 | open |
+----+------------+
| 12 | content |
+----+------------+
and want to get a new table where I get the IDs (the first and the last) of the values between "close" and "open". Note that the values between this two conditions are dynamic (I can't search by "nocontent"whatever)
Such as I get this table:
+----+----------+--------+
| id | start_id | end_id |
+----+----------+--------+
| 1 | 5 | 6 |
+----+----------+--------+
| 2 | 10 | 10 |
+----+----------+--------+
Thanks in advance!
http://sqlfiddle.com/#!2/c255c8/2

You can do this using a correlated subquery:
select (#rn := #rn + 1) as id,
id as startid,
(select id
from atable a2
where a2.id > a.id and
a2.condition = 'close'
order by a2.id asc
limit 1
) as end_id
from atable a cross join
(select #rn := 0) vars
where a.condition = 'open';
The working SQL Fiddle is here.
Note this returns the third open as well. If you don't want it, then add having end_id is not null to the end of the query.
EDIT:
If you know the ids are sequential, you can just add and subtract 1 from the above query:
select (#rn := #rn + 1) as id,
id+1 as startid,
(select id
from atable a2
where a2.id > a.id and
a2.condition = 'open'
order by a2.id asc
limit 1
) - 1 as end_id
from atable a cross join
(select #rn := 0) vars
where a.condition = 'close';
You can also do this in a different way, which is by counting the number of open and closes before any given row and using this as a group identifier. The way your data is structured, every other group is what you are looking for:
select grp, min(id), max(id)
from (select t.*,
(select sum(t2.condition in ('open', 'close'))
from t t2
where t2.id <= t.id
) as grp
from t
) t
where t.condition not in ('open', 'close') and
grp % 2 = 0
group by grp;

Related

SQL partition by with original order

Here's the original MySQL table:
+----+-----+
| Id | Num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
+----+-----+
When I use select Id, Num, row_number() over(partition by Num) from t, MySQL automatically disrupts the order of the Num column. However, I want to keep Num column order unchanged.
Specifically, the ideal output should be like:
+----+-----+-----+
| Id | Num | row |
+----+-----+-----+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
| 5 | 1 | 1 |
| 6 | 2 | 1 |
| 7 | 2 | 2 |
+----+-----+-----+
How to write this MySQL query?
This is a gaps-and-islands problem. I would recommend using the difference between row numbers to identify the groups.
If id is always incrementing without gaps:
select id, num,
row_number() over(partition by num, id - rn order by id) rn
from (
select t.*, row_number() over(partition by num order by id) rn
from mytable t
) t
order by id
Otherwise, we can generate our own incrementing id with another row_number():
select id, num,
row_number() over(partition by num, rn1 - rn2 order by id) rn
from (
select t.*,
row_number() over(order by id) rn1,
row_number() over(partition by num order by id) rn2
from mytable t
) t
order by id
Demo on DB Fiddle - for your sample data, both queries yield:
id | num | rn
-: | --: | -:
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 2 | 1
5 | 1 | 1
6 | 2 | 1
7 | 2 | 2
You can do this by writing your own row_number to have greater control over its partitioning.
set #prev_num = null;
set #row_number = 0;
select
id,
-- Reset row_number to 1 whenever num changes, else increment it.
#row_number := case
when #prev_num = num then
#row_number + 1
else
1
end as `row_number`,
-- Emulate lag(). This must come after the row_number.
#prev_num := num as num
from foo
order by id;
Same idea as the solution proposed by Schwern. Just another style of syntax in MySQL which I find very simplistic and easy to use.
Select
id
, num
, value
from
(select
T.id,
T.num,
if( #lastnum = T.num, #Value := #Value + 1,#Value := 1) as Value,
#lastnum := T.num as num2
from
mytable T,
( select #lastnum := 0,
#Value := 1 ) SQLVars
order by
T.id) T;
DB fiddle link - https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=e04692841d091ccd54ee3435a409c67a

How to track previous row status count

I want to calculate count of order status changes within different states.
My Orderstatus table:
| id |ordr_id| status |
|----|-------|------------|
| 1 | 1 | pending |
| 2 | 1 | processing |
| 3 | 1 | complete |
| 4 | 2 | pending |
| 5 | 2 | cancelled |
| 6 | 3 | processing |
| 7 | 3 | complete |
| 8 | 4 | pending |
| 9 | 4 | processing |
Output I want:
| state | count |
|----------------------|-------|
| pending->processing | 2 |
| processing->complete | 2 |
| pending->cancelled | 1 |
Currently I'm fetching the results by SELECT order_id,GROUP_CONCAT(status) as track FROM table group by order_id and then process the data in php to get the output. But is that possible in query itself ?
Use lag():
select prev_status, status, count(*)
from (select t.*,
lag(status) over (partition by order_id order by status) as prev_status
from t
) t
group by prev_status, status;
LAG() is available in MySQL starting with version 8.
Note that you can filter out the first status for each order by putting where prev_status is not null in the outer query.
Your version is not quite correct, because it does not enforce the ordering. It should be:
SELECT order_id,
GROUP_CONCAT(status ORDER BY id) as track
EDIT:
In earlier versions of MySQL, you can use a correlated subquery:
select prev_status, status, count(*)
from (select t.*,
(select t2.status
from t t2
where t2.order_id = t.order_id and t2.id < t.id
order by t2.id desc
limit 1
) as prev_status
from t
) t
group by prev_status, status;
If id column ensure the sequence of records, you can use self join to achieve your requirement as below-
SELECT A.Status +'>'+ B.Status, COUNT(*)
FROM OrderStatus A
INNER JOIN OrderStatus B
ON A.id = B.id -1
WHERE B.Status IS NOT NULL
GROUP BY A.Status +'>'+ B.Status
With a join of the 3 status change types to the grouping of the table that you already did:
select c.changetype, count(*) counter
from (
select 'pending->processing' changetype union all
select 'processing->complete' union all
select 'pending->cancelled'
) c inner join (
select
group_concat(status order by id separator '->') changestatus
from tablename
group by ordr_id
) t on concat('->', t.changestatus, '->') like concat('%->', changetype, '->%')
group by c.changetype
See the demo.
Results:
> changetype | counter
> :------------------- | ------:
> pending->cancelled | 1
> pending->processing | 2
> processing->complete | 2
...or just a simple join...
SELECT CONCAT(a.status,'->',b.status) action
, COUNT(*) total
FROM my_table a
JOIN my_table b
ON b.ordr_id = a.ordr_id
AND b.id = a.id + 1
GROUP
BY action;
+----------------------+-------+
| action | total |
+----------------------+-------+
| pending->cancelled | 1 |
| pending->processing | 2 |
| processing->complete | 2 |
+----------------------+-------+
Note that this relies on the fact that ids are contiguous.

SQL - Select same values in column then rename them

Is it possible in SQL to select values in a column then rename the duplicate ones? (assuming maximum of one possible duplicate only)
Let's say I have a table..
| id | name | 0or1_id |
| 0 | Eddy | 0 |
| 1 | Allan | 0 |
| 2 | Eddy | 1 |
| 3 | Allan | 1 |
What query can I do to make it like this?
| id | name | 0or1_id |
| 0 | Eddy | 0 |
| 1 | Allan | 0 |
| 2 | Eddy-copy | 1 |
| 3 | Allan-copy | 1 |
Assuming you want to actually change the data, use update:
update t join
(select name, count(*) as cnt, min(id) as minid
from t
group by name
having cnt > 1
) tt
on t.name = tt.name and t.id <> tt.minid
set name = concat(name, '-copy');
If you only want a select, then the logic is quite similar.
This will work in SQL Server..
select id , name ,0or1_id from (
select id , name ,0or1_id ,row_number() over (partition by name order by id ) as rnm
from table)z1
where rnm =1
union
select id , name || '- Copy' as new_name ,0or1_id from (
select id , name ,0or1_id ,row_number() over (partition by name order by id ) as rnm
from table)z2
where rnm > 2
http://sqlfiddle.com/#!9/3ebaf/1
UPDATE mytable t
INNER JOIN mytable t1
ON t.name = t1.name
AND t.id>t1.id
SET t.name = CONCAT(t.name,'-copy');

MySQL - Check if consecutive columns are the same and display only those rows

I have an events table that contains IDs (id) and dates (eventDate) corresponding to those IDs (id and eventDate are not the only columns in the table).
SQLFiddle here.
+--------+----+---------------------+
| row_id | id | eventDate |
+--------+----+---------------------+
| 1 | 1 | 2014-02-27 23:19:41 |
| 2 | 1 | 2014-02-27 23:21:41 |
| 3 | 1 | 2014-02-27 23:21:41 |
| 4 | 2 | 2014-02-27 23:23:08 |
| 5 | 2 | 2014-02-27 23:25:08 |
| 6 | 2 | 2014-02-27 23:25:08 |
| 9 | 3 | 2014-02-28 15:36:55 |
| 8 | 3 | 2014-02-28 15:36:55 |
| 7 | 3 | 2014-02-28 15:34:55 |
| 10 | 4 | 2014-02-28 19:31:31 |
| 11 | 4 | 2014-02-28 19:33:31 |
| 12 | 4 | 2014-02-28 19:33:31 |
| 13 | 5 | 2014-02-28 19:33:34 |
| 14 | 5 | 2014-02-28 19:33:33 |
| 15 | 5 | 2014-02-28 19:31:33 |
| 16 | 6 | 2014-03-04 22:40:21 |
| 17 | 6 | 2014-03-04 22:38:21 |
| 18 | 6 | 2014-03-04 22:40:21 |
| 19 | 7 | 2014-03-04 23:08:37 |
| 20 | 7 | 2014-03-04 23:08:38 |
+--------+----+---------------------+
I want to select only those rows from the table, where consecutive event dates are the same for the same ID.
Thus, I would like to see only these entries -
+----+---------------------+
| id | eventDate |
+----+---------------------+
| 1 | 2014-02-27 23:21:41 |
| 1 | 2014-02-27 23:21:41 |
| 2 | 2014-02-27 23:25:08 |
| 2 | 2014-02-27 23:25:08 |
| 3 | 2014-02-28 15:36:55 |
| 3 | 2014-02-28 15:36:55 |
| 4 | 2014-02-28 19:33:31 |
| 4 | 2014-02-28 19:33:31 |
Note that there is no
| 6 | 2014-03-04 22:40:21 |
| 6 | 2014-03-04 22:40:21 |
in the above result, because they're not consecutive.
I know I can store the output of the SQL query in a file and then use a unix tool to do this, but I want to know if this is achievable directly through SQL.
Should be able to accomplish this leveraging a group by although my mySql is a bit rusty.
SELECT t.*
FROM (
SELECT
id,
eventDate,
COUNT(0) AS numRows
FROM tabl
GROUP BY id, DATE(eventDate)
HAVING COUNT(0) > 1
ORDER BY eventDate
) t
Then you just join this correlated sub query back to the original table if you need additional columns.
select id,eventDate from your_tableName where eventDate in (select eventDate from your_tableName group by id,eventDate having count(eventDate) > 1);
select ta.id, ta.eventDate from
(
select row_id as ra, t1.id, t1.eventDate
from events t1
) as ta
join
(
select row_id as rb, t2.id, t2.eventDate
from events t2
) as tb
on rb = ra+1 and ta.id = tb.id and ta.eventDate = tb.eventDate
I have found a way to match the eventDate of the next row but the only drawback is that it will return the number of consecutive dates - 1 rows. But in your code you can just loop 1 extra time.
SET #inc = 0;
SET #innerInc = 1;
SELECT t1.id, t1.eventDate
FROM (
SELECT id, eventDate, (#inc := #inc + 1) as increment FROM temp
) t1
WHERE t1.eventDate = (
SELECT t2.eventDate FROM (
SELECT eventDate, (#innerInc := #innerInc + 1) as increment FROM temp
) t2
WHERE t2.increment = t1.increment
);
Here is the SQLFiddle for this: Here
This should be able to do it with a single table scan (no subqueries,joins,etc..)
SELECT t.id,t.eventDate
FROM (
SELECT
IF(id = #prevID AND eventDate = #prevDate, #counter, #counter := #counter+1) as c,
#prevID := id as id,
#prevDate := eventDate as eventDate
FROM events e
JOIN (SELECT #counter := 0, #prevID := NULL, #prevDate := NULL) as stuff
WHERE 1 #or some where condition for events
ORDER BY row_id ASC
) as t
GROUP BY t.c
If it's not specifically that you need the positionally consecutive entry, but rather that if you grouped by eventDate and found entries with the same eventDate then you'd want those records, then the following:
select *
from Table a
join (select eventDate, count(*)
from Table
group by eventDate
having count(*) > 1) b
on (a.eventDate = b.eventDate)
The arbitrary dependence on the position of the data suggests that there is some other property you're not sharing, and it is by that property that the records are retrieved and ordered. If such a property determines the record's position, then it's precisely by ordering or grouping with that property that you can efficiently solve this.
After throwing away my self-join I think you're going to have to generate row_numbers for each sub_query:
select #rn1 := #rn1+1 as ra, t1.id, t1.eventDate
from events t1
join (select #rn1 := 0) r;
and then join that to
select #rn2 := #rn2+1 as rb, t2.id, t2.eventDate
from events t2
join (SELECT #rn2 := 0) r;
so final answer:
select ta.id, ta.eventDate from
(
select #rn1 := #rn1+1 as ra, t1.id, t1.eventDate
from events t1
join (select #rn1 := 0) r
) as ta
join
(
select #rn2 := #rn2+1 as rb, t2.id, t2.eventDate
from events t2
join (SELECT #rn2 := 0) r
) as tb
on rb = ra+1 and ta.id = tb.id and ta.eventDate = tb.eventDate
Results:
1 February, 27 2014 23:21:41+0000
2 February, 27 2014 23:25:08+0000
3 February, 28 2014 15:36:55+0000
4 February, 28 2014 19:33:31+0000

Mysql ranking with upper and lower rankers

I want to get range ranking from a table using mysql query.
the table is like this,
+------------+------+
| first_name | age |
+------------+------+
| Kathy | 2 |
| Jane | 1 |
| Nick | 3 |
| Bob | 5 |
| Anne | 4 |
| Jack | 6 |
| Bill | 8 |
| Steve | 7 |
+------------+------+
and I want to get Jack's ranking with 2 lower and upper rankers.
+------------+------+
| Anne | 4 |
| Bob | 5 |
| Jack | 6 |
| Steve | 7 |
| Bill | 8 |
+------------+------+
Any idea how to write this query for a MySQL database?
Here is a very ugly way to do this by applying a rownumber to the records. By applying the rownumber to the records, then you you will be able to return rows if the ages are not consecutive (Demo with Non-consecutive ages):
select age, first_name
from
(
select t1.age, t1.first_name, #rownum:=#rownum+1 AS rownum
from yourtable t1, (SELECT #rownum:=0) r
order by t1.age
) x
where rownum >= (select rownum
from
(
select t.age,
t.first_name,
#rownum:=#rownum+1 AS rownum
from yourtable t, (SELECT #rownum:=0) r
order by t.age
) x
where first_name = 'jack') - 2
and rownum <= (select rownum
from
(
select t.age,
t.first_name,
#rownum:=#rownum+1 AS rownum
from yourtable t, (SELECT #rownum:=0) r
order by t.age
) x
where first_name = 'jack') + 2;
see SQL Fiddle with Demo
Working Example:
SELECT * FROM yourtable WHERE id BETWEEN ((SELECT id FROM yourtable WHERE name = 'Jack') - 2) AND ((SELECT id FROM yourtable WHERE name = 'Jack') + 2);
SQL Fiddle Demo