MySQL update IDs on update, insert and delete - php

In my current application I am making a menu structure that can recursively create sub menu's with itself. However due to this I am finding it difficult to also allow some sort of reordering method. Most applications might just order by an "ordering" column, however in this case although doing that does not seem impossible, just a little harder.
What I want to do is use the ID column. So if I update id 10 to be id 1 then id 1 that was there previously becomes 2.
What I was thinking at a suggestion from a friend was to use cascades. However doing a little more research that does not seem to work as I was thinking it might.
So my question is, is there an ability to do this naively in MySQL? If so what way might I do that? And if not what would you suggest to come to the end result?
Columns:
id title alias icon parent
parents have a lower id then their children, to make sure the script creates the array to put the children inside. That part works, however If I want to use an ordering column I will have to make a numbering system that would ensure a child element is never higher then its parent in the results. Possible, but if I update a parent then I must uniquely update all its children as well, resulting in more MySQL queries that I would want.
I am no MySQL expert so this is why I brought up this question, I feel there might be a perfect solution to this that can allow the least overhead when it comes to the speed of the application.

Doing it on the ID column would be tough because you can't ever have 2 rows with the same ID so you can't set row 10 to row 1 until after you've set row 1 to row 2 but you can't set row 1 to row 2 until you set row 2 to row 3, etc. You'd have to delete row 10 and then do an update ID += 1 WHERE ID < 10... but you'd also have to tell MySQL to start from the highest number and go down....
You'd have to do it in separate queries like this:
Move ID 10 to ID 2
DELETE FROM table WHERE id = 10;
UPDATE table SET id = id + 1 WHERE id >= 2 AND id < 10 ORDER BY id DESC
INSERT INTO table (id, ...) VALUES (2, ...);
Another option, if you don't want to delete and reinsert would be to set the id for row 10 to be MAX(id) + 1 and then set it to 1 after
Also if you want to move row 2 to row 10 you'd have to subtract the id:
Move ID 2 to ID 10
DELETE FROM table WHERE id = 2;
UPDATE table SET id = id - 1 WHERE id > 2 AND id <= 10 ORDER BY id DESC
INSERT INTO table (id, ...) VALUES (10, ...);
If you don't have your ID column set as UNSIGNED you could make all the IDs you want to switch to negative ids since AUTO_INCREMENT doesn't do negative numbers. Still this is pretty hacky and I wouldn't recommend it. You also probably need to lock the table so no other writes happen while this is running.:
Move ID 2 to ID 10
UPDATE table SET id = id * -1 WHERE id > 2 AND id <= 10;
UPDATE table SET id = 10 WHERE id = 2;
UPDATE table SET id = id * -1 - 1 WHERE id < -2 AND id >= -10;

Related

How to select unique value randomly from a table in heavy load server

I have a single-column MySQL database table ids (id INTEGER PRIMARY KEY AUTOINCREMENT) where I store pre-generated unique ids in an ascending order. In order to get a random id from that table I use this query:
SELECT id FROM ids ORDER BY RAND() LIMIT 1;
And now I am wondering how to ensure that the id I got is never used again. I see two options. One is to delete the id from the table and the other is add a column tracking the use of that id:
DELETE FROM ids WHERE id=?; //where id is the one I got from the previous query
or
SELECT id FROM ids WHERE used=0 ORDER BY RAND() LIMIT 1;
UPDATE ids SET used=1 WHERE id=?; //where used is new column with 0 as default value
There is only a slight problem with both of these. If the server load is heavy then two queries for a random id might return the same id before it gets removed from the list (or disabled with used column).
Would transaction help?
Wrapping your select and your update in a transaction will work. If you want to avoid a transaction as well as the race condition between selecting your item and marking it unusable, you can run the UPDATE first. You'll need a way for each of your processes to identify itself as the owner of the row between claiming it and deletion. For example, assume your ids schema is
id (integer)
owner (string)
Have each process pick a UUID (or something else suitably unique) and run the following:
UPDATE ids SET owner = $process_id WHERE owner IS NULL ORDER BY RAND() LIMIT 1
SELECT id FROM ids WHERE owner = $process_id
DELETE FROM ids WHERE id = $selected_id (or otherwise mark it used)
Step 1 atomically claims the row for the process so that no other process can claim it. Step 2 pulls out the claimed ID. Step 3 removes the ID from the available set for good. If Step 3 doesn't delete the row, just marks it used, make sure you clear owner as well so your process won't select it again later.
Add an extra column to your ids table. Let us say selected. You will update this column when it is generated in your randomized query.
1 for selected and 0 for not yet.
For example
SELECT id FROM ids ORDER BY RAND() WHERE selected = 0 LIMIT 1;
$id = $row['id']; /* STORE THE SELECTED ID TO A VARIABLE */
Then update the table with
UPDATE ids SET selected = 1 WHERE id = $id
So that your next run of your randomized query will only get the row of selected 0 value, and not the one with 1 value.
you could try updating the id 1st before selecting it. try this one.
-- get firstID
SELECT #MinID:=id FROM ids ORDER by id ASC LIMIT 1;
-- get last id
SELECT #MaxID:=id FROM ids ORDER by id DESC LIMIT 1;
-- get random id
SELECT #rndomID :=ROUND((RAND() * (#MaxID-#MinID))+#MinID);
-- update first
UPDATE ids SET used=1 WHERE id=#rndomID;
-- select the id
SELECT id FROM ids WHERE used=0 WHERE id=#rndomID;

How to set auto increment value after deleting a middle row

I have a Table
id paper_title
1 General English1
2 General English2
3 General English3
4 General English4
5 General English5
6 General English6
7 General English7
.
.
.
.
100 General English100.
Suppose If i delete row 4 and row 5, is it possible to update the id of General English6 And General English7 Set to 4,5 respectively and If i delete a row in middle or start the id should reset continuously like 1 2 3 4 5..................100.Please Help.
Determine the id of the row that you want to delete (let's call that id $foo). Then execute
UPDATE tbl SET id = id-1 WHERE id > $foo
get the highest id and call it $max:
SELECT MAX(id) FROM tbl
Then set the new auto increment value:
ALTER TABLE tbl AUTO_INCREMENT = $max+1
Do this for every row you delete. I sure hope id is not your primary key. And anyways, there is definitely a better solution to your underlying problem. There is usually no need to have the rows in a database table numbered from 1 to whatever without gaps.

delete one row in the middle and how to refresh the table id(auto increment) in mysql

For example:
Row Name
1 John
2 May
3 Marry
4 Tom
5 Peter
Suppose I delete row 2 and row 3, is it possible to update Tom and Peter to row id 2 and 3 respectively and the next insert row to be row id 4?
yes, but you need to recreate Row:
ALTER TABLE `users` DROP `Row`;
ALTER TABLE `users` AUTO_INCREMENT = 1;
ALTER TABLE `users` ADD `Row` int UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
No, because think of the problems that this could create. Imagine if you are managing a store inventory and each item has an internal barcode, based on an ID in a database table. You decide never to stock one item again and take it out of the database, and then every ID for every item with an ID greater than the removed item gets decremented by 1... all those barcodes on all those items become useless.
ID numbers in databases aren't generally meant to be used or referenced by people. They are meant to be a static index of a certain item in a database which allows the computer to pull up a given record quickly and reliably. When creating your ID field of your table, make sure you make the datatype large enough to account for these lost IDs when they do occur.
This is just a suggestion. I don't say this is the best solution. Just consider.
You execute your delete query.
DELETE FROM table_name WHERE Row IN (2,3);
Once deleted you make a select query request with PHP and get the data set to an array.
SELECT Row, Name from table_name ORDER BY Row ASC;
Then make a UPDATE execution using a loop.
$index = 1;
foreach($dataset as $item)
{
// Your update query
$sql = "UPDATE table_name SET Row=$index where Name='" . $item['Name'] . "'";
$index++;
}
Before you insert next query you have to get the max value of Row and set +1 value as the Row of the insert query.
This is just an idea. Not the complete code.

How do I assign a rotating category to database entries in the order the records come in?

I have a table which gets entries from a website, and as those entries go into the database, they need to be assigned the next category on a list of categories that may be changed at any time.
Because of this reason I can't do something simple like for mapping the first category of 5 to IDs 1, 6, 11, 16.
I've considered reading in the list of currently possibly categories, and checking the value of the last one inserted, and then giving the new record the next category, but I imagine if two requests come in at the same moment, I could potentially assign them both the same category rather then in sequence.
So, my current round of thinking is the following:
lock the tables ( categories and records )
insert the newest row into records
get the newest row's ID
select the row previous to the insertl ( by using order by auto_inc_name desc 0, 1 )
take the previous row's category, and grab the next one from the cat list
update the new inserted row
unlock the table
I'm not 100% sure this will work right, and there's possibly a much easier way to do it, so I'm asking:
A. Will this work as I described in the original problem?
B. Do you have a better/easier way to do this?
Thanks ~
I would do it way simpler... just make a table with one entry, "last_category" (unsigned tinyint not_null). Every time you do an insert just increment that value, and reset as necessary.
I'm not sure I understand your problem, but as I understand it you would like to have something like
category | data
-----------------
0 | lorem
1 | ipsum
.... | ...
4 | dolor
0 | sit
... | ...
How about having a unique auto_increment column, and let category be the MOD 5 of this column?
If you need 100% correct behaviour it sounds like you will need to lock something somewhere so that all your inserts line up properly. You might be able to avoid locking the category table if you use a single SQL statement to insert your data. I'm not sure how MySQL differs but in Oracle I can do this:
insert into my_table (id, col1, col2, category_id)
select :1, :2, :3, :4, c.id -- :1, :2, etc are bind variables. :1 corresponds to the ID.
from
(select
id, -- category id
count(*) over (partition by 1) cnt, -- count of how many categories there are
row_number() over (partition by 1 order by category.id) rn -- row number for current row in result set
from category) c
where c.rn = mod(:1, cnt)
This way in one statement I insert the next record based on the categories that existed at that moment. The insert automatically locks the my_table table until you commit. It grabs the category based on the modulus of the ID. This link shows you how to do a row-number in mysql. I'm not sure if count(*) requires group by in mysql; in oracle it does so I used a partition instead to count the whole result set.

Updating display order of multiple MySQL rows in one or very few queries

I have a table with say 20 rows each with a number for display order (1-20).
SELECT * FROM `mytable` ORDER BY `display_order` DESC;
From an admin area you can drag the rows around or type a new number manually for each row.
Surely it's not good to loop over an UPDATE query for every row, what's an alternative in one or very few queries suitable for updating one cell in 20 rows or even more, 50-200+?
Edit: A lot of good responses and ideas. I might expand on the ideas I've considered so far:
One array string: I could have the order in a string listing the unique row IDs in the order I want - eg rows 1,9,2,6,23. When the order is updated, a hidden field updates with JavaScript and adds that to the database or a text file when complete:
UPDATE `my_dispaly_order_table` SET `display_order`='1,9,2,6,23';
Update each row individually: This is what I was trying to avoid but it would only be changed very infrequently so 20-30 calls in one hit once a week or month might not be a problem so simply calling UPDATE on each row is what I usually do:
UPDATE `mytable` SET `display_order`='1' WHERE `rowId` = 1;
UPDATE `mytable` SET `display_order`='2' WHERE `rowId` = 9;
UPDATE `mytable` SET `display_order`='3' WHERE `rowId` = 2;
UPDATE `mytable` SET `display_order`='4' WHERE `rowId` = 6;
UPDATE `mytable` SET `display_order`='5' WHERE `rowId` = 23;
soulmerge's answer made me think and I think this is a better solution. What you need to do is select the rows with the id using IN() and then use CASE to set the value.
UPDATE mytable SET display_order =
CASE id
WHEN 10 THEN 1
WHEN 23 THEN 2
WHEN 4 THEN 3
END CASE
WHERE id IN (10, 23, 4)
I have a need for this in my current app. In PHP, I'm getting a serialized (and ordered) set of id's from jQuery UI's built-in Sortable feature. So the array looks like this:
$new_order = array(4, 2, 99, 15, 32); // etc
To generate the single MySQL update query, I do this:
$query = "UPDATE mytable SET display_order = (CASE id ";
foreach($new_order as $sort => $id) {
$query .= " WHEN {$id} THEN {$sort}";
}
$query .= " END CASE) WHERE id IN (" . implode(",", $new_order) . ")";
The "implode" at the end just gives me my ID list for IN(). This works beautifully for me.
You should first ensure that the column has no UNIQUE index, otherwise mysql will tell you that the constraint is broken during the query. After that you can do things like:
-- Move #10 down (i.e. swap #10 and #11)
UPDATE mytable SET display_order =
CASE display_order
WHEN 10 THEN 11
WHEN 11 THEN 10
END CASE
WHERE display_order BETWEEN 10 AND 11;
-- Move #4 to #10
UPDATE mytable SET display_order
CASE display_order
WHEN 4 THEN 10
ELSE display_order - 1
END CASE
WHERE display_order BETWEEN 4 AND 10;
But you should actually ensure that you do things in single steps. swapping in two steps will result in broken numbering if not using ids. i.e.:
-- Swap in two steps will not work as demostrated here:
UPDATE mytable SET display_order = 10 WHERE display_order = 11;
-- Now you have two entries with display_order = 10
UPDATE mytable SET display_order = 11 WHERE display_order = 10;
-- Now you have two entries with display_order = 11 (both have been changed)
And here is a reference to the CASE statement of mysql.
A little late, but it may be useful to someone else:
UPDATE mytable SET display_order = FIND_IN_SET(rowId, '1,9,2,6,23') WHERE rowId in (1,9,2,6,23)
You could try to wrap it into a few statements, I don't think it's possible in a single one. So for example, let's say you are going to update the 10th row. You want every record after 10 to be bumped up.
UPDATE table SET col=col+1 WHERE col > 10
UPDATE table SET col=10 WHERE id = X
...
But it's really tough to roll in all logic required. Because some records maybe need a decrement, etc.. You want to avoid duplicates, etc..
Think about this in terms of developer time vs. gain.
Because even if someone sorts this once per day, the overhead is minimal, compared to fixing it in a stored procedure, or pseudo-optimizing this feature so you don't run 20 queries. If this doesn't run 100 times a day, 20 queries are perfectly fine.
You could delete an re-insert all the rows - that would do the whole operation in just two queries (or three if you need to select all the data). I wouldn't count on it being faster, and you'd have to do it inside a transaction or you'll be heading for your backups before too long. It could also lead to table fragmentation.
A better option might be to record each change as a history then do something like this:
Example, position 10 is moved down two to 12th
UPDATE table SET display_order = display_order -1 WHERE display_order BETWEEN 10 AND 12
UPDATE table SET display_order = 12 WHERE row_id = [id of what was row 10]
I'm thinking about this problem, and the solution I came up is having a decimal number as order and change the number of the item change for a number between the next and the previous item
Order Item
----- ----
1 Original Item 1
2 Original Item 2
3 Original Item 3
4 Original Item 4
5 Original Item 5
If you change the item 4 to the 2nd position, you get:
Order Item
----- ----
1 Original Item 1
1.5 Original Item 4
2 Original Item 2
3 Original Item 3
5 Original Item 5
If you change the item 3 to the 3rd position, you get:
Order Item
----- ----
1 Original Item 1
1.5 Original Item 4
1.75 Original Item 3
2 Original Item 2
5 Original Item 5
Theoretically there is always a decimal between two decimals, but you could face some storage limits.
This way you only have to update the row being re-ordered.
If you need to drag you rows, this is a good implementation for a linked list.
Having your rows ordered with a linked list means that you will update at most 3 rows at a time -- even if you move the whole block (as long as it's contiguous).
Create a table of your rows like this:
CREATE TABLE t_list (
id INT NOT NULL PRIMARY KEY,
parent INT NOT NULL,
value VARCHAR(50) NOT NULL,
/* Don't forget to create an index on PARENT */
KEY ix_list_parent ON (parent)
)
id parent value
1 0 Value1
2 3 Value2
3 4 Value3
4 1 Value4
and use this MySQL query to select the rows in order:
SELECT #r := (
SELECT id
FROM t_list
WHERE parent = #r
) AS id
FROM (
SELECT #r := 0
) vars,
t_list
This will traverse your linked list and return the ordered items:
id parent value
1 0 Value1
4 1 Value4
3 4 Value3
2 3 Value2
To move a row, you'll need to update the parent of the row, the parent of its current child, and the parent of the row you're inserting before.
See this series of articles in my blog on how to do it efficiently in MySQL:
Sorting lists - how to select ordered items from a linked list
Sorting lists: moving items - how to move a single item
Sorting lists: adding items - how to insert a single item
Sorting lists: deleting items - how to delete a single item
Sorting lists: moving blocks - how to move a contiguous block
Sorting lists: deleting blocks - how to delete a contiguous block
There are lots of articles because there are some issues with row locking which should be handled slightly differently for each case.
This is a great way of sorting items on a database.
Exactly what i was looking for ;)
Here's an improved version of Jamon Holmgren's version. This function is totally dynamic:
function reOrderRows($tablename, $ordercol, $idsarray){
$query = "UPDATE $tablename SET $ordercol = (CASE $ordercol ";
foreach($idsarray as $prev => $new) {
$query .= " WHEN $prev THEN $new\n";
}
$query .= " END) WHERE $ordercol IN (" . implode(",", array_keys($idsarray)) . ")";
mysql_query($query);
}
This assumes that you have a column $ordercol on your table $tablename just for ordering purposes.
The $idsarray is an array in the format array("current_order_num" =>
"updated_order_num").
So if you just want to swap two lines in your table (imagine that you have for example a "move up" and "move down" icons on your webpage), you can use this function on top of the previous one:
function swapRows($tablename, $ordercol, $firstid, $secondid){
$swaparray = array("$firstid" => "$secondid", "$secondid" => "$firstid");
reOrderRows($tablename, $ordercol, $swaparray);
}
You could create a temporary table and populate with the primary key of the rows you want to change, and the new values of display_order for those rows, then use the multi-table version of UPDATE to update the table in one go. I wouldn't assume that this is faster, however, not without testing both approaches.
Add an id (or other key) to the table, and update where id (or key) = id (or key) of changed row.
Of course, you'll have to make sure that either there are no duplicate display_order values, or that you're OK with ties in display_order displaying in any order, or you'll introduce a second, tie-breaker to the order by list.
Collect the new order in a temporary variable and put a "save this order" button to the admin area. Then save the order for the rows with one round.
You'll get better response times, undoable changes, fewer modified rows (because in low level in the dbms, practically no updates used to be possible, but save a new instance of the whole row and delete the old one).
After all, it would be a lower cost solution for a whole reordering and would save some coding on the update optimization.

Categories