MySql, after DELETE update a counter to contain no gaps - mysql

I have a mySql table like this (simplified)
Id*| Text | Pos (integer)
-----------
A | foo | 0
B | bar | 1
C | baz | 2
D | qux | 3
Now, after I delete a row, I want to update the Pos value on the remaining rows so that no "holes" or gaps are to be found.
For example if I row with Id='C' is deleted, the remaing table should be:
Id*| Text | Pos (integer)
-----------
A | foo | 0
B | bar | 1
D | qux | 2
Is this possible in a single query?
UPDATE
Based on the accepted answer this was the solution to my problem:
START TRANSACTION;
SELECT #A:=pos FROM table_name WHERE Id= 'C';
DELETE FROM table_name WHERE Id = 'C';
UPDATE table_name SET Pos = Pos - 1 WHERE Pos > #A;
COMMIT;

You can achieve this by creating a AFTER DELETE TRIGGER on table,
or by using transactions:
START TRANSACTION;
SELECT Pos
INTO #var_pos
FROM table_name
WHERE id = 'C';
DELETE
FROM table_name
WHERE id = 'C';
UPDATE table_name
SET Pos = Pos - 1
WHERE Pos > #var_pos;
COMMIT;

I think this should work, (I haven't tested it)
you can run this statement after any delete
update t set t.Pos=a.iterator
from tablename t
join(
SELECT #i:=#i+1 AS iterator, t.id
FROM tablename t,(SELECT #i:=0) r)a
on a.id=t.id

Related

How to increment an id based on a field having a certain value going row by row

I'm importing data where groups of rows need to be given an id but there is nothing unique and common to them in the incoming data. What there is is a known indicator of the first row of a group and that the data is in order so we can step through row by row setting an id and then increment that id whenever this indicator is found. I've done this however it's incredibly slow, so is there a better way to do this in mysql or am i better off perhaps pre-processing the text data going line by line to add the id.
Example of data coming in, I need to increment an id whenever we see "NEW"
id,linetype,number,text
1,NEW,1234,sometext
2,CONTINUE,2412,anytext
3,CONTINUE,1,hello
4,NEW,2333,bla bla
5,CONTINUE,333,hello
6,NEW,1234,anything
So i'll end up with
id,linetype,number,text,group_id
1,NEW,1234,sometext,1
2,CONTINUE,2412,anytext,1
3,CONTINUE,1,hello,1
4,NEW,2333,bla bla,2
5,CONTINUE,333,hello,2
6,NEW,1234,anything,3
I've tried a stored procedure where i go row by row updating as i go, but it's super slow.
select count(*) from mytable into n;
set i=1;
while i<=n do
select linetype into l_linetype from mytable where id = i;
if l_linetype = "NEW" then
set l_id = l_id + 1;
end if;
update mytable set group_id = l_id where id = i;
end while;
No errors, it's just something that i could go line by line reading and writing the text file and do in a second while in mysql it's taking 100 seconds, it'd be nice if there was a way within mysql to do this reasonably fast so separate pre-processing was not needed.
In absence of MySQL 8+ (non availability of Windowing functions), you can use a Correlated Subquery instead:
EDIT: As pointed out by #Paul in comments,
SELECT t1.*,
(SELECT COUNT(*)
FROM your_table t2
WHERE t2.id <= t1.id
AND t2.linetype = 'NEW'
) group_id
FROM your_table t1
Above query can be more performant, if we define the following composite index (linetype, id). The order of columns is important, because we have a Range condition on id.
Previously:
SELECT t1.*,
(SELECT SUM(t2.linetype = 'NEW')
FROM your_table t2
WHERE t2.id <= t1.id
) group_id
FROM your_table t1
Above query requires indexing on id.
Another approach using User-defined Variables (Session variables) would be:
SELECT
t1.*,
#g := IF(t1.linetype = 'NEW', #g + 1, #g) AS group_id
FROM your_table t1
CROSS JOIN (SELECT #g := 0) vars
ORDER BY t1.id
It is like a looping technique, where we use Session Variables whose previous value is accessible during next row's calculation during SELECT. So, we initialize the variable #g to 0, and then compute it row by row. If we can encounter a row with NEW linetype, we increment it, else use the previous row's value. You can also check https://stackoverflow.com/a/53465139/2469308 for more discussion and caveats to take care of while using this approach.
For MySql 8.0+ you can use SUM() window function:
select *,
sum(linetype = 'NEW') over (order by id) group_id
from tablename
See the demo.
For previous versions you can simulate this functionality with the use of a variable:
set #group_id := 0;
select *,
#group_id := #group_id + (linetype = 'NEW') group_id
from tablename
order by id
See the demo.
Results:
| id | linetype | number | text | group_id |
| --- | -------- | ------ | -------- | -------- |
| 1 | NEW | 1234 | sometext | 1 |
| 2 | CONTINUE | 2412 | anytext | 1 |
| 3 | CONTINUE | 1 | hello | 1 |
| 4 | NEW | 2333 | bla bla | 2 |
| 5 | CONTINUE | 333 | hello | 2 |
| 6 | NEW | 1234 | anything | 3 |

How to use Column with JSON string with IN Operator in SQL?

I have one column with JSON data, i want to use that column with my query, but i was try so many thing but can't please guide me for that How to use Column with JSON string with IN Operator in SQL ?
i have following table and query.
table1
------------
id | ids |
1 | [1,2] |
2 | [3,4] |
3 | [5] |
4 | [] |
5 | [1,5,6]|
-------------
table2
------------
id | name |
1 | raj |
2 | mohan |
3 | test |
4 | name1 |
5 | hello |
-------------
SELECT * FROM `table1` t1
LEFT JOIN table2 t2 ON ( t2.id IN (replace(replace(t1.ids,"[",""),"]","")) )
WHERE p.id = 2
You could use JSON_CONTAINS:
SELECT *
FROM `table1` t1
LEFT JOIN `table2` t2
ON JSON_CONTAINS(t1.ids, t2.id)
WHERE t1.id = 2;
DBFiddle Demo
Your IN() query might not work correctly because mysql return '1,2' for id 1, and you need something like 1,2, Check following example
select
replace(replace(t1.ids,'[',''),']','') as id
from table11 t1 where t1.id=1;
Above query return result as string which interpret like '1,2' not 1,2
And because of this your query might not return your expected output.
You can try some other approach, as follow
SOLUTION
Create 1 function which will convert your json string to another table and then you can use that table into your in() query
Try following function (Reference)
DROP FUNCTION IF EXISTS hello;
DELIMITER //
CREATE FUNCTION hello (_list CHAR(20))
RETURNS INT
BEGIN
DECLARE _next TEXT DEFAULT NULL;
DECLARE _nextlen INT DEFAULT NULL;
DECLARE _value TEXT DEFAULT NULL;
iterator:
LOOP
IF LENGTH(TRIM(_list)) = 0 OR _list IS NULL THEN
LEAVE iterator;
END IF;
SET _next = SUBSTRING_INDEX(_list,',',1);
SET _nextlen = LENGTH(_next);
SET _value = TRIM(_next);
INSERT INTO t11 (id) VALUES (_next);
SET _list = INSERT(_list,1,_nextlen + 1,'');
END LOOP;
RETURN 1;
END //
DELIMITER ;
Create one table
CREATE TABLE t11(id CHAR(1));
Then execute following queries.
DELETE FROM t11;
SELECT hello(REPLACE(REPLACE(t1.ids,'[',''),']','')) FROM table11 t1;
SELECT t2.* FROM table22 t2 WHERE t2.id IN(SELECT id FROM t11);
I hope this will solve your problem

How can I add a sequencial number without cursor in mysql?

I have a table that can have some valid duplicated values so I need an additional column with the sequence number of appearance of said duplicate for future use.
A sample might be
ROW | COLUMN_A | COLUMN_B | COLUMN_C | SEQ_NUM <= Want this column
1 A B 1 1
2 A B 1 2
3 A B 2 1
4 A B 2 2
5 A B 2 3
The values are supposed to be unique like (COLUMN_A, COLUMB_B, COLUMN_C), but I cannot use a unique index because I need those duplicated values as well, I just need to keep track of the order of apparition. So I added a column SEQ_NUM to keep track of those repetitions.
And i fill it like this:
begin
declare done boolean default false;
declare _A varchar(1);
declare _B varchar(1);
declare _C integer unsigned;
declare cur cursor for
select COLUMN_A , COLUMN_B , COLUMN_C
from tmp_horario
group by COLUMN_A , COLUMN_B , COLUMN_C
having count(*) > 1; -- Here I loop throught the repeated values
declare continue handler for not found set done := true;
open cur;
loop_dup: loop
fetch cur into _A, _B, _C;
if done then
leave loop_dup;
end if;
set #_seq = 0; -- I initialize my sequence in 0 to start
update tmp_table h
set h.SEQ_NUM = (#_seq := #_seq + 1) -- Set the next sequential to the repeated values
where h.COLUMN_A = _A
and h.COLUMN_B = _B
and h.COLUMN_C = _C;
end loop loop_dup;
close cur;
end;
Note: The table has way more columns making the cursor (fetch into) a bigger pain.
As you can see that works like charm except that it takes my store from 20 s to 80 s which I find a little disappointing (already checked indexes and they are being properly used), I believe the problem lies in the use of the cursor.
My question then is: Is there a way of setting that famous sequential number in a single query without the cursor?.
Assuming you want this to happen when you insert a value to the table you could do this as such:
INSERT INTO tmp_horario(COLUMN_A, COLUMN_B, COLUMN_C, SEQ_NUM)
VALUE(A_VAL, B_VAL, C_VAL, (IFNULL((
SELECT MAX(SEQ_NUM)
FROM tmp_horario AS a
WHERE a.COLUMN_A = A_VAL AND a.COLUMN_B = B_VAL AND a.COLUMN_C = C_VAL), 0)+1));
The basic premise is you look for rows with the same values, get the maximum sequential value if one exists, and then add one to that for the new value. If no match is found then set the insert value to one. The IFNULL statement is really all you need to get the SEQ_NUM, should you need to adapt this query.
Yes pretty much like your cursor
DROP TABLE IF EXISTS T;
CREATE TABLE T(ROW INT, COLUMN_A VARCHAR(1), COLUMN_B VARCHAR(1), COLUMN_C VARCHAR(1), SEQ_NUM INT);
INSERT INTO T VALUES
(1 , 'A' , 'B' , 1,NULL),
(2 , 'A' , 'B' , 1,NULL),
(3 , 'A' , 'B' , 2,NULL),
(4 , 'A' , 'B' , 2,NULL),
(5 , 'A' , 'B' , 2,NULL);
UPDATE T
JOIN (
SELECT T.ROW,
IF(CONCAT(T.COLUMN_A,T.COLUMN_B,T.COLUMN_C) <> #P , #RN:=1,#RN:=#RN+1) RN,
#P:=CONCAT(T.COLUMN_A,T.COLUMN_B,T.COLUMN_C) P
FROM T , (SELECT #RN:=0,#P:=0) R
ORDER BY ROW
) S ON S.ROW = T.ROW
SET SEQ_NUM = S.RN
WHERE 1 = 1
MariaDB [sandbox]> SELECT * FROM T;
+------+----------+----------+----------+---------+
| ROW | COLUMN_A | COLUMN_B | COLUMN_C | SEQ_NUM |
+------+----------+----------+----------+---------+
| 1 | A | B | 1 | 1 |
| 2 | A | B | 1 | 2 |
| 3 | A | B | 2 | 1 |
| 4 | A | B | 2 | 2 |
| 5 | A | B | 2 | 3 |
+------+----------+----------+----------+---------+
5 rows in set (0.00 sec)

Update MySQL record twice using 'CASE'

I am working on an accountant software , some times the invoice contain the same item id twice (every one with its unique serial number) .. so after selling the two items I need its quantity equals quantity-1
I am using this statement for updating some records
UPDATE `table1` SET `quantity` = CASE
WHEN id = 1 THEN quantity-1
WHEN id = 1 THEN quantity-1
END
WHERE id in (1)
after updating using this statement ,the value of quantity ignores the second statement
How can I solve this ?
EDIT
The answer
Thank You ... I found the trick by myself
UPDATE `table1` SET `quantity` = CASE
WHEN id = (1*(1/1)) THEN quantity-1
WHEN id = (1*(2/2)) THEN quantity-1
WHEN id = (1*(3/3)) THEN quantity-1
END
WHERE id in (1)
the record will be updated 3 times by the same query
why I need This ?
because my software accepts that the user can add the same (Product ID) many times in the same invoice so I need the query to update the quantity many times in the same statement for one (Product ID)
I think this might actually be what you want. I have id=1 in my table 3 times, so mcount is set equal to 3. The row for ID=2 was not updated because of the where condition.
update table1
join (select id, count(*) as ct from table1 group by id) as ct_tbl
set mcount = ct_tbl.ct
where table1.id=1
mysql> select * from table1;
+------+--------+
| id | mcount |
+------+--------+
| 1 | 3 |
| 1 | 3 |
| 1 | 3 |
| 2 | 0 |
+------+--------+
4 rows in set (0.00 sec)
I don't want to hijack the other answer because it's totally correct but I think it needs more explanation.
UPDATE `table1` SET `mcount` = CASE
WHEN id = 1 THEN 1
WHEN id = 1 THEN 2
END
WHERE id in (1)
that is the equivalent of... (psuedo)
in the table table1
where the id = 1
if id = 1 then set mcount = 1
if id = 1 then set mcount = 2
Because of the where statement, we already know that we will only select rows where id=1. Those if statements will just overwrite each other. I really don't know what you're after but normally...
the following will add one to mcount
UPDATE `table1`
SET `mcount` = mcount + 1;
WHERE id in (1)
the following will toggle mcount values
UPDATE `table1`
SET `mcount` = CASE WHEN mcount = 1 THEN 2
WHEN mcount = 2 THEN 1 END
WHERE id in (1)
the following will toggle mcount values
UPDATE `table1`
SET `mcount` = CASE WHEN other_field = 'Y' THEN 1
WHEN other_field = 'N' THEN 2 END
WHERE id in (1)
working with multiple ID values...
UPDATE `table1`
SET `mcount` = CASE WHEN id = 1 THEN 1
WHEN id = 2 THEN 2
WHEN id = 3 THEN 3 END
WHERE id in (1,2,3)
which is the same as...
UPDATE `table1`
SET `mcount` = id
WHERE id in (1,2,3)
now the real question is... if ID=1 then what should mcount be?
also....
set #row:=0;
update table1
set mcount = #row:=#row+1
where id = 1;
mysql> select * from table1;
+------+--------+
| id | mcount |
+------+--------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 0 |
+------+--------+
4 rows in set (0.00 sec)
Your statement has the same condition for each case, always will return 1 (first statement), the second statement will be ignored because it is the same condition.
Thank You ... I found the trick by myself
UPDATE `table1` SET `quantity` = CASE
WHEN id = (1*(1/1)) THEN quantity-1
WHEN id = (1*(2/2)) THEN quantity-1
WHEN id = (1*(3/3)) THEN quantity-1
END
WHERE id in (1)
the record will be updated 3 times by the same query

Return rows with maximum date less than each value in a set of dates in SQL

Consider the following table:
CREATE TABLE foo (
id INT PRIMARY KEY,
effective_date DATETIME NOT NULL UNIQUE
)
Given a set of dates D, how do you fetch all rows from foo whose effective_date is the greatest value less than each date in D in a single query?
For simplicity, assume that each date will have exactly one matching row.
Suppose foo has the following rows.
---------------------
| id |effective_date|
---------------------
| 0 | 2013-01-07|
---------------------
| 1 | 2013-02-03|
---------------------
| 2 | 2013-04-19|
---------------------
| 3 | 2013-04-20|
---------------------
| 4 | 2013-05-11|
---------------------
| 5 | 2013-06-30|
---------------------
| 6 | 2013-12-08|
---------------------
If you were given D = {2013-02-20, 2013-06-30, 2013-12-19}, the query should return the following:
---------------------
| id |effective_date|
---------------------
| 1 | 2013-02-03|
| 4 | 2013-05-11|
| 6 | 2013-12-08|
If D had only one element, say D = {2013-06-30}, you could just do:
SELECT *
FROM foo
WHERE effective_date = SELECT MAX(effective_date) FROM foo WHERE effective_date < 2013-06-30
How do you generalize this query when the size of D is greater than 1, assuming D will be specified in an IN clause?
Actually, your problem is - that you have a list of values, which will be treated in MySQL as row - and not as a set - in most cases. That is - one of possible solutions is to generate your set properly in application so it will look like:
SELECT '2013-02-20'
UNION ALL
SELECT '2013-06-30'
UNION ALL
SELECT '2013-12-19'
-and then use produced set inside JOIN. Also, that will be great, if MySQL could accept static list in ANY subqueries - like for IN keyword, but it can't. ANY also expects rows set, not list (which will be treated as row with N columns, where N is count of items in your list).
Fortunately, in your particular case your issue has important restriction: there could be no more items in list, than rows in your foo table (it makes no sense otherwise). So you can dynamically build that list, and then use it like:
SELECT
foo.*,
final.period
FROM
(SELECT
period,
MAX(foo.effective_date) AS max_date
FROM
(SELECT
period
FROM
(SELECT
ELT(#i:=#i+1, '2013-02-20', '2013-06-30', '2013-12-19') AS period
FROM
foo
CROSS JOIN (SELECT #i:=0) AS init) AS dates
WHERE period IS NOT NULL) AS list
LEFT JOIN foo
ON foo.effective_date<list.period
GROUP BY period) AS final
LEFT JOIN foo
ON final.max_date=foo.effective_date
-your list will be automatically iterated via ELT(), so you can pass it directly to query without any additional restructuring. Note, that this method, however, will iterate through all foo records to produce row set, so it will work - but doing the stuff in application may be more useful in terms of performance.
The demo for your table can be found here.
perhaps this can help :
SELECT *
FROM foo
WHERE effective_date IN
(
(SELECT MAX(effective_date) FROM foo WHERE effective_date < '2013-02-20'),
(SELECT MAX(effective_date) FROM foo WHERE effective_date < '2013-06-30'),
(SELECT MAX(effective_date) FROM foo WHERE effective_date < '2013-12-19')
)
result :
---------------------
| id |effective_date|
---------------------
| 1 | 2013-02-03| -- different
| 4 | 2013-05-11|
| 6 | 2013-12-08|
UPDATE - 06 December
create procedure :
DELIMITER $$
USE `test`$$ /*change database name*/
DROP PROCEDURE IF EXISTS `myList`$$
CREATE PROCEDURE `myList`(ilist VARCHAR(100))
BEGIN
/*var*/
/*DECLARE ilist VARCHAR(100) DEFAULT '2013-02-20,2013-06-30,2013-12-19';*/
DECLARE delimeter VARCHAR(10) DEFAULT ',';
DECLARE pos INT DEFAULT 0;
DECLARE item VARCHAR(100) DEFAULT '';
/*drop temporary table*/
DROP TABLE IF EXISTS tmpList;
/*loop*/
loop_item: LOOP
SET pos = pos + 1;
/*split*/
SET item =
REPLACE(
SUBSTRING(SUBSTRING_INDEX(ilist, delimeter, pos),
LENGTH(SUBSTRING_INDEX(ilist, delimeter, pos -1)) + 1),
delimeter, '');
/*break*/
IF item = '' THEN
LEAVE loop_item;
ELSE
/*create temporary table*/
CREATE TEMPORARY TABLE IF NOT EXISTS tmpList AS (
SELECT item AS sdate
);
END IF;
END LOOP loop_item;
/*view*/
SELECT * FROM tmpList;
END$$
DELIMITER ;
call procedure :
CALL myList('2013-02-20,2013-06-30,2013-12-19');
query :
SELECT
*,
(SELECT MAX(effective_date) FROM foo WHERE effective_date < sdate) AS effective_date
FROM tmpList
result :
------------------------------
| sdate |effective_date|
------------------------------
| 2013-02-20 | 2013-02-03 |
| 2013-06-30 | 2013-05-11 |
| 2013-12-19 | 2013-12-08 |
The bad way first (without ordered analytical functions, or rank/row_number)
sel tmp.min_effective_date, for_id.id
from
(
Sel crossed.effective_date,max(SRC.effective_date) as min_effective_date
from
foo as src
cross join
foo as crossed
where
src.effective_date <cross.effective_date
and crossed.effective_date in
(given dates here)
group by 1
) tmp inner join foo as for_id on
tmp.effective_date =for_id.effective_date
Next, with row_number
SEL TGT.id, TGT.effective_date
(Sel id, effective_date, row_number() over(order by effective_date asc) as ordered
) SRC
INNER JOIN
(Sel id, effective_date, row_number() over(order by effective_date asc) as ordered ) TGT
on
src.ordered+1=TGT.ordered
where src.effective_date in (given dates)
with ordered analytical functions:
sel f.id, tmp.eff
foo as f inner join
(SEL ID, max(effective_date) over(order by effective_date asc ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) as eff
from foo
) TMP
on f.id = tmp.id
where f.effective_date in (given dates)
and tmp.eff is not null
the queries above assume id needs to be selected, and the ids in the source don't follow the same sequence (eg ascending) as the dates. Otherwise, you can straight away use the ordered analytical function.