Updating sort keys after delete - mysql

I have a table which has a field sort_id. In this field there are numbers from 1 to n, that define the order of the data sets.
Now I want to delete some elements and afterwards I want to reorder the table. Therefore I need a query that "finds" the gaps and changes the sort_id field according to the modifications.
Sure, I could do something like this:
SELECT sort_id FROM table WHERE id = 5
Then save the sort_id and afterwards:
DELETE FROM table WHERE id = 5
UPDATE table SET sort_id = sort_id - 1 WHERE sort_id > {id from above}
But I'd like to do the reordering process in one step.

Mladen and Arvo have good ideas, but unfortunately in MySQL you can't SELECT and UPDATE the same table in the same statement (even in a subquery). This is a known limitation of MySQL.
Here's a solution that uses MySQL user variables:
SET #i := 0;
UPDATE mytable
SET sort_id = (#i := #i + 1)
ORDER BY sort_id;
For what it's worth, I wouldn't bother doing this anyway. If your sort_id is used only for sorting and not as a kind of "row number," then the rows are still in sorted order after you delete the row where id=6. The values don't necessarily have to be consecutive for sorting.

for sql server 2005:
this is how you get the new sequence:
SELECT row_number() over(order by sort_id) as RN
FROM table
updating the table means you should join that select to your update:
update t1
set sort_id = t2.RN
FROM table t1
join (SELECT row_number() over(order by sort_id) as RN FROM table) t2
on t1.UniqueId = t2.UniqueId

I don't know MySQL syntax variations and cannot test query live, but something like next should give you at least an idea:
update table t1
set sort_id = (select count * from table t2 where t2.sort_id <= t1.sort_id)

Related

Calculating time difference of every other row from a table

Note: The data for my question is on SQLFiddle right here where you
can query it.
How the table is created
I have data from a table and put into a temp table using the below logic but the BETWEEN start and end date time stamps are dynamically generated based on other logic in the stored proc, etc.
SET #RowNum = 0;
DROP TEMPORARY TABLE IF EXISTS temp;
CREATE TEMPORARY TABLE temp AS
SELECT #RowNum := #RowNum + 1 RowNum
, TimeStr
, Value
FROM mytable
WHERE TimeStr BETWEEN '2018-01-31 06:15:56' AND '2018-01-31 19:27:09'
AND iQuality = 3 ORDER BY TimeStr;
This gives me a temp table with the row number which increments up one number in order starting with the oldest based TimeStr records, so the oldest is the time of the first record or RowNum 1.
Temp Table
The Data
You can get to this temp table data and play with the queries here on the SQLFiddle I've created but I have a few things I tried there you'll see there which don't give me what I need though.
Attempt to Clarify Further
I need to get the time for each ON and OFF set based on the TimeStr values in each set and I can get this using the TIMEDIFF() function.
I'm having a hard time figuring out how to make it give me the result of each ON and OFF record. The records are always in order from oldest to newest and the row number always starts at 1 too.
I some how need to give give every two records with one after the other RowNum values wise a matching CycleNum starting at 1 and increment by one per each ON and OFF cycle or set.
I can use TIMEDIFF(MAX(TimeStr), MIN(TimeStr)) as duration but I'm not sure how to best get it to group every two RowNum records in order as explained to give each set a subsequent CycleNum value that increments.
Expected Output
The expected output show look like the below screen shot for all ON and OFF cycles or every two RowNum in groups and sequence.
Output Clarification
I need the output to include each ON and OFF cycle's start time, end time, and the duration for the time between the start and stop.
If you can guarantee two things:
That the row numbers are strictly sequential with no gaps.
That the on/off flag is always alternating.
Then you can do this with a relatively simple join. The code looks like:
SELECT (#rn := #rn + 1) as cycle, t.*, tnext.timestr,
timediff(tnext.timestr, t.timestr)
FROM temp t JOIN
temp tnext
ON t.rownum = tnext.rownum - 1 and
t.value = 1 and
tnext.value = 0 cross join
(SELECT #rn := 0) params;
If these conditions are not true, then more complex logic is needed.
Here is a simpler one :
SELECT
t1.TimeStr AS StartTime,
t2.TimeStr AS EndTime,
TIMEDIFF(t2.TimeStr, t1.TimeStr) AS Duration
FROM temp t1
INNER JOIN temp t2 ON t2.RowNum = t1.RowNum + 1
WHERE
t2.Value = 0
AND t1.Value = 1
A quick and dirty way to do it would be this:
SELECT
T1.TimeStr AS StartTime,
(SELECT T2.TimeStr FROM temp AS T2 WHERE T2.RowNum = T1.RowNum+1) AS StopTime,
TIMEDIFF((SELECT T2.TimeStr FROM temp AS T2 WHERE T2.RowNum = T1.RowNum+1),
T1.TimeStr) AS Duration
FROM temp AS T1
WHERE Value = 1;
Seems like there must be better ways to do this. Two subqueries will be slow.
You could do it in two steps:
CREATE TEMPORARY TABLE startstop AS
SELECT
T1.TimeStr AS StartTime,
(SELECT T2.TimeStr FROM temp AS T2 WHERE T2.RowNum = T1.RowNum+1) AS StopTime,
0 AS Duration
FROM temp AS T1
WHERE Value = 1;
UPDATE startstop SET Duration = StopTime - StartTime;
However I cannot test this in the Fiddle.

Sql Server default value average of x rows

I have the following trigger running on MySQL:
CREATE DEFINER=`root`#`%` TRIGGER `before_insert` BEFORE INSERT ON `table` FOR EACH ROW SET
new.AVG_COLUMN1 = (SELECT avg(COLUMN1) FROM (SELECT COLUMN1 from table ORDER BY DateTimeCol DESC LIMIT 20) as COLUMN1_A),
new.AVG_COLUMN2 = (SELECT avg(COLUMN2) FROM (SELECT COLUMN2 from table ORDER BY DateTimeCol DESC LIMIT 20) as COLUMN2_A),
new.AVG_COLUMN3 = (SELECT avg(COLUMN3) FROM (SELECT COLUMN3 from table ORDER BY DateTimeCol DESC LIMIT 20) as COLUMN3_A)
Basically my goal here is to set a automatic, default value in the AVG_COLUMNx column, based on the last 20 entries in COLUMNx, whenever a new row is inserted. This is working fine in MySQL using the mentioned trigger.
I am in the process of migrating my project to Sql Server Express from MS, and I'm trying to do the same there. Does anyone have any good pointers as to how I could accomplish this? Using triggers, computed columns, etc?
Thanks for any input!
The logic would be different in SQL Server because it would be using inserted rather than new. Basically:
update t
set AVG_ROW1 = tt.avg1,
AVG_ROW2 = tt.avg2,
AVG_ROW3 = tt.avg3
from table t join
inserted i
on i.pk = t.pk outer apply
(select avg(Row1) as avg1, avg(Row2) as avg2, avg(Row3) as avg3
from (select top 20 t.*
from table t
order by DateTimeRow desc
) t
) tt;
You need some identifier(s) in the row to match the table to inserted. That is what pk stands for.

sql: keep only the minimum value if two rows have duplicate id

Below is my sample data. Row 3 and 4 have the same st_case (the primary key), but their dist_min are different. I want to keep the row with the minimum dist_min value. And please notice that there could be more than 2 duplicate rows associate with the same st_case. Thank you so much for the help!
In MySQL, you can do this with a delete and join:
delete s
from sample s left join
(select st_case, min(dist_min) as mindm
from sample s
group by st_case
) ss
on ss.st_case = s.st_case and s.dist_min > ss.mindm;
You can try this one:
DELETE t1 FROM table AS t1
LEFT JOIN table t2 ON t1.st_case = t2.st_case
WHERE t1.dist_min > t2.dist_min
As SAM M suggested, I am not sure how you can have duplicate rows with same primary key.
However in case st_case is not your only key and the table has a composite key
OR had it been a non-key column,
You could write a trigger to control the insertion
Something like:
CREATE table CALC_STATUS(id varchar(40), correlatoinToken integer, requirement double)
CREATE TRIGGER keep_min BEFORE INSERT ON CALC_STATUS
FOR EACH row
BEGIN
IF NEW.correlatoinToken = correlatoinToken AND NEW.requirement <= requirement then
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = "A row with similar correlation token with lower requirement already exists";
ELSEIF NEW.correlatoinToken = correlatoinToken AND NEW.requirement > requirement
NEW.requirement = requirement;
END IF;
END;
And if you meant to query:
SELECT c1.* FROM CALC_STATUS c1,(SELECT st_case,MIN(dist_min) FROM CALC_STATUS GROUP BY road_id) AS c2 WHERE c1.st_case=c2.st_case
DELETE FROM sample
WHERE dist_min !=(SELECT dist_min FROM (SELECT * FROM sample) sample2
WHERE sample2.st_case = sample.st_case
ORDER BY dist_min
LIMIT 1)
I didn't get to test this so please forgive any syntax errors. SQL ranking functions can solve your problem. Essentially you group by one column and then rank by another. Then you can select only those of rank one.
SELECT *
FROM(
SELECT *, Rank() OVER (PARTITION BY 'st_case' Order by Dist_min DESC) as Rank
From 'tbl_Name')
WHERE Rank = '1'

Reorder a MYSQL table

I have a MySql table with a 'Order' field but when a record gets deleted a gap appears
how can i update my 'Order' field sequentially ?
If possible in one query 1 1
id.........order
1...........1
5...........2
4...........4
3...........6
5...........8
to
id.........order
1...........1
5...........2
4...........3
3...........4
5...........5
I could do this record by record
Getting a SELECT orderd by Order and row by row changing the Order field
but to be honest i don't like it.
thanks
Extra info :
I also would like to change it this way :
id.........order
1...........1
5...........2
4...........3
3...........3.5
5...........4
to
id.........order
1...........1
5...........2
4...........3
3...........4
5...........5
In MySQL you can do this:
update t join
(select t.*, (#rn := #rn + 1) as rn
from t cross join
(select #rn := 0) const
order by t.`order`
) torder
on t.id = torder.id
set `order` = torder.rn;
In most databases, you can also do this with a correlated subquery. But this might be a problem in MySQL because it doesn't allow the table being updated as a subquery:
update t
set `order` = (select count(*)
from t t2
where t2.`order` < t.`order` or
(t2.`order` = t.`order` and t2.id <= t.id)
);
There is no need to re-number or re-order. The table just gives you all your data. If you need it presented a certain way, that is the job of a query.
You don't even need to change the order value in the query either, just do:
SELECT * FROM MyTable WHERE mycolumn = 'MyCondition' ORDER BY order;
The above answer is excellent but it took me a while to grok it so I offer a slight rewrite which I hope brings clarity to others faster:
update
originalTable
join (select originalTable.ID,
(#newValue := #newValue + 10) as newValue
from originalTable
cross join (select #newValue := 0) newTable
order by originalTable.Sequence)
originalTable_reordered
on originalTable.ID = originalTable_reordered.ID
set originalTable.Sequence = originalTable_reordered.newValue;
Note that originalTable.* is NOT required - only the field used for the final join.
My example assumes the field to be updated is called Sequence (perhaps clearer in intent than order but mainly sidesteps the reserved keyword issue)
What took me a while to get was that "const" in the original answer was not a MySQL keyword. (I'm never a fan of abbreviations for that reason -- the can be interpreted many ways at times especially at these very when it is best they not be misinterpreted. Makes for verbose code I know but clarity always trumps convenience in my books.)
Not quite sure what the select #newValue := 0 is for but I think this is a side effect of having to express a variable before it can be used later on.
The value of this update is of course an atomic update to all the rows in question rather than doing a data pull and updating single rows one by one pragmatically.
My next question, which should not be difficult to ascertain, but I've learned that SQL can be a trick beast at the best of times, is to see if this can be safely done on a subset of data. (Where some originalTable.parentID is a set value).

Loop through column and update it with MySQL?

I want to loop through some records and update them with an ad hoc query in MySql. I have a name field, so I just want to loop though all of them and append a counter to each name, so it will be name1, name2, name3. Most examples I see use stored procs, but I don't need a stored proc.
As a stepping stone on your way to developing an UPDATE statement, first generate a SELECT statement that generates the new name values to your liking. For example:
SELECT t.id
, t.name
, CONCAT(t.name,s.seq) AS new_name
FROM ( SELECT #i := #i + 1 AS seq
, m.id
FROM mytable m
JOIN (SELECT #i := 0) i
ORDER BY m.id
) s
JOIN mytable t
ON t.id = s.id
ORDER BY t.id
To unpack that a bit... the #i is a MySQL user variable. We use an inline view (aliased as i) to initialize #i to a value of 0. This inline view is joined to the table to be updated, and each row gets assigned an ascending integer value (aliased as seq) 1,2,3...
We also retrieve a primary (or unique) key value, so that we can match each of the rows from the inline view (one-to-one) to the table to be updated.
It's important that you understand how that statement is working, before you attempt writing an UPDATE statement following the same pattern.
We can now use that SELECT statement as an inline view in an UPDATE statement, for example:
UPDATE ( SELECT t.id
, t.name
, CONCAT(t.name,s.seq) AS new_name
FROM ( SELECT #i := #i + 1 AS seq
, m.id
FROM mytable m
JOIN (SELECT #i := 0) i
ORDER BY m.id
) s
JOIN mytable t
ON t.id = s.id
ORDER BY t.id
) r
JOIN mytable u
ON u.id = r.id
SET u.name = r.new_name
SQL Fiddle demonstration here:
http://sqlfiddle.com/#!2/a8796/1
I had to extrapolate, and provide a table name (mytable) and a column name for a primary key column (id).
In the SQL Fiddle, there's a second table, named prodtable which is identical to mytable. SQL Fiddle only allows SELECT in the query pane, so in order to demonstrate BOTH the SELECT and the UPDATE, I needed two identical tables.
CAVEAT: be VERY careful in using MySQL user variables. I typically use them only in SELECT statements, where the behavior is very consistent, with careful coding. With DML statements, it gets more dicey. The behavior may not be as consistent in DML, the "trick" is to use a SELECT statement as an inline view. MySQL (v5.1 and v5.5) will process the query for the inline view and materialize the resultset as a temporary MyISAM table.
I have successfully used this technique to assign values in an UPDATE statement. But (IMPORTANT NOTE) the MySQL documentation does NOT specify that this usage or MySQL user variables is supported, or guaranteed, or that this behavior will not change in a future release.
Have the names stored in a table. Do a join against the names and update in the second table you want to.
Thanks