MySQL fixing autoincrement gaps in two tables - php

I have two tables like so;
id_image foo bar
1 3 5
2 8 1
3 17 88
7 14 23
8 12 9
id_image bar foo
1 2 3
1 5 6
2 18 11
2 10 12
3 8 21
3 17 81
7 29 50
7 1 14
8 10 26
8 27 34
There is a gap in the autoincremented id_image in the first table. In the second table, the id_image refers to the id_image in the first table, and there's two of each ID in there.
Notice: This table is theoretical. I have no idea where the gap is exactly, or whether or not there are even multiple gaps. All I know is that the first value is 1 and the last value is higher than the total row count.
Now, I'd like to fix this gap.
Before you say that the gaps don't matter and if they do, it's bad database design, let me tell you; I agree with you.
However, what I'm dealing with is a (hopelessly rear end backwards) third-party open source system to which I need to import a huge amount of existing data that doesn't have cross-referenceable IDs into multiple tables. The only way I can make sure that the same data gets a matching ID in every table throughout the system is to input it sequentially, and that means I can't have gaps.
So what I do now need to do is;
Fix the gap in the id_image column in the first table, so that the last value matches with the row count.
Edit the id_image column in the second table so that its value corresponds to the same row is corresponded to before the gap fix.
How would I begin to do this? I understand that this might be outside the capabilities of the MySQL query language, so PHP answers are also acceptable. Thanks! :)

ALTER TABLE table2
ADD FOREIGN KEY FK_IMAGE (id_image)
REFERENCES table1 (id_image)
ON DELETE CASCADE
ON UPDATE CASCADE;
SET #currentRow = 0;
UPDATE table1 INNER JOIN (
SELECT #currentRow := #currentRow + 1 AS id_image_new, id_image AS id_image_old
FROM table1
ORDER BY id_image ASC) t on t.id_image_old = table1.id_image
SET table1.id_image = t.id_image_new;
ALTER TABLE table1 AUTO_INCREMENT = 1;
The FK will automatically update ids of your 2nd table accordingly.
I'm not sure at all but in some older versions of mysql, update a table that you are referencing within a subquery of the update could crash. If so, just create a 2nd table and fill it up (inserts), then delete the old one and rename the new one.

The basic idea here is to find all of the gaps first to determine how much you need to decrement each id. Then, you have to iterate through both tables and apply the decrement. (You'll need to add: host, db, user, pass, and the actual table names)
try {
$pdo = new PDO('mysql:host=HOST;dbname=DB', 'user', 'pass');
$pdo->beginTransaction();
// Iterate through all id's in the first table
$stmt = $pdo->exec('SELECT image_id FROM TableOne ORDER BY image_id ASC');
$stmt->bindColumn('image_id', $id);
if(!$stmt->fetch(PDO::FETCH_BOUND)) {
throw Exception('No rows in table');
}
$lastId = $id;
$gaps = array();
// Find all the gaps
while($stmt->fetch(PDO::FETCH_BOUND)) {
if($id != ($lastId + 1)) {
$gaps[] = $id;
}
$lastId = $id;
}
if(!isset($gaps[0])) {
throw new Exception('No gaps found');
}
// For each gap, update the range from the last gap to that gap by subtracting
// the number of gaps there has been from the id
$lastGap = $gaps[0];
for($i = 1; $i < count($gaps); $i++) {
$stmt = $pdo->prepare('UPDATE TableOne SET image_id = image_id - :i WHERE image_id BETWEEN :lastGap AND :gap');
$stmt->execute(array(
':i' => $i,
':lastGap' => $lastGap,
':gap' => $gaps[$i]
));
$stmt = $pdo->prepare('UPDATE TableTwo SET image_id = image_id - :i WHERE image_id BETWEEN :lastGap AND :gap');
$stmt->execute(array(
':i' => $i,
':lastGap' => $lastGap,
':gap' => $gaps[$i]
));
$lastGap = $gaps[$i];
}
// Finally, fix the gap between the last found gap and the end of the table
$stmt = $pdo->prepare('UPDATE TableOne SET image_id = image_id - :i WHERE image_id BETWEEN :lastGap AND :gap');
$stmt->execute(array(
':i' => $i,
':lastGap' => $lastGap,
':gap' => $gaps[$i]
));
$stmt = $pdo->prepare('UPDATE TableTwo SET image_id = image_id - :i WHERE image_id BETWEEN :lastGap AND :lastId');
$stmt->execute(array(
':i' => $i,
':lastGap' => $lastGap,
':lastId' => $lastId
));
// Verify everything is correct
$stmt = $pdo->exec('SELECT image_id FROM TableOne ORDER BY image_id ASC');
$stmt->bindColumn('image_id', $id);
if(!$stmt->fetch(PDO::FETCH_BOUND)) {
throw new Exception('No rows'); // Should never be thrown
}
$lastId = $id;
while($stmt->fetch(PDO::FETCH_BOUND)) {
if($id != ($lastId + 1)) {
throw new Exception('There was an error between ids ' . $lastId . ' and '. $id);
}
$lastId = $id;
}
$stmt = $pdo->exec('SELECT image_id FROM TableTwo ORDER BY image_id ASC');
$stmt->bindColumn('image_id', $id);
if(!$stmt->fetch(PDO::FETCH_BOUND)) {
throw new Exception('No rows in table two'); // Shouldn't hit this
}
$lastId = $id;
$ids = array($id);
while($stmt->fetch(PDO::FETCH_BOUND)) {
$ids[] = $id;
if(count($ids) == 2) {
if($ids[0] !== $ids[1]) {
throw new Exception('Table two error on ids ');
}
if($ids[0] !== $lastId) {
throw new Exception('Table two error on id gapfix');
}
$lastId = $ids[0];
$ids = array();
}
}
$pdo->commit();
} catch(Exception $e) {
$pdo->rollBack();
var_dump($e);
}
Important: You might want to throw this in a file and run via the CLI: php -f gapfix.php and include a query before $pdo->commit() that returns a list of all the ids so you can verify the operation worked as expected. If it didn't, you can roll it back as if nothing happened. The code now checks for itself if the first table is in the right order. It doesn't however check the second table yet. All checking has been implemented!

Painful this.
Create a table like the first, no identity on Id_Image and an extra int column called rownumber
Use the pseudo row_number trick to populate it, some like
Insert into NewTable
Select id_image,foo,bar,#RowNumber := #RowNumber + 1 order by id_image.
If you have a foreign key to the second table drop it, then it's a simple update with a join. Drop the old table1 , rename the new one, add the identity and reseed, and put your foreign key back if you had one.
You do realise you are going to have to keep doing this crap?
There's probably a fun way of doing all this in one go, if you have cascading updates on, but pay careful attention to the execution plan. The RowNumber trick only works if things are done in Id_Image order. If Mysql decides there's a more efficient way of doing the query....

Related

Getting the missing IDs from a table by storing the records into an array and compare the set of numbers with a for loop

I'm currently working on a program that gets the missing IDs of a table and the idea that I come up with is that by storing the IDs into an array and use a for loop to check if a number exists in the array and if it's existing then it is classified as a missing ID. I also used the php function - in_array() to check if a number exists in the array.
This is the code that I came up with, but I ended up with just displaying the numbers from the for loop.
<?php
include 'dbconnect.inc'; //just to the the dbconnect for connecting into the database.
$numbers = array(1, 2, 4, 6, 7, 9);
$arrlength = count($numbers);
$query = "SELECT id FROM existing";
$result = mysqli_query($conn, $query);
$existing = array();
while ($row = mysqli_fetch_assoc($result)) {
$existing[] = $row;
}
for ($i=0; $i<7358; $i++) {
if (in_array($i, $existing)) {
echo $i . " is a missing ID <br>";
} elseif(!in_array($i, $existing)) {
echo $i . " exists in the table <br>";
}
}
?>
I prefer this solution than using the temporary tables in an SQL because it takes more than to load the query and it would not be good for a webpage.
Hope that you could help me. Thanks!
From this answer:
To get missing ranges:
SELECT a.id+1 AS 'Missing From', MIN(b.id)-1 AS 'Through'
FROM existing AS a
JOIN existing AS b ON a.id < b.id
GROUP BY a.id
HAVING a.id+1 < MIN(b.id)
fiddle
User variables are only evaluated when sent, so using a HAVING NOT (gap_from=0 AND gap_to=0) clause isn't possible as an optimization (see user variables manual). A such we use the "sending" to be sending to the temporary table to save a larger time full of data that is about to be discarded.
The temporary table uses the primary key ensure there will only be one (0,0) entry that occurs when the there is no gap. Inserting subsequent existing entries (0,0) gets ignored resulting in a minimal table of gaps.
The remainder of the table is the gaps in the sequence:
create table existing (id int unsigned not null)
insert into existing values (3),(5),(6),(7),(8),(19),(20),(21),(30)
set #last=0
CREATE TEMPORARY TABLE v (gap_from int unsigned, gap_to int unsigned, next int unsigned, PRIMARY KEY(gap_from, gap_to))
IGNORE SELECT IF(#last=id, 0, #last) as gap_from,
IF(#last=id, 0, id-1) as gap_to,
#last:=id+1 as next
FROM existing ORDER BY id
select gap_from,gap_to from v where NOT (gap_from=0 AND gap_to=0)
gap_from | gap_to
-------: | -----:
0 | 2
4 | 4
9 | 18
22 | 29
If you don't want the first gap, the one between 0 and the first entry in the table:
select gap_from,gap_to from v where gap_from!=0
db<>fiddle here

mysql select and insert safely and quickly with php

I have a simple table, the logic is that before inserting a new row, I should fetch one column from that table. Let me explain:
table
id key groupId Note
1 00001 1 abd
2 00002 1 aasdas
3 00003 1 aasdas
4 00001 2 q2eqwd
5 00002 2 qwdvvd
6 00003 2 qwrqw
7 00004 2 qwdqdqw
You see, key increases like Auto Increment for each groupId.
When group with id 2, wants to add a new note, he should know last key. After finding it, php addes +1 to last key and inserts a new row. I do it like below:
$groupId = 2; //for example
$note = $_POST['note'];
$select = $db -> prepare("SELECT key FROM table where groupId = :groupId ORDER BY id DESC limit 1");
$select -> bindValue(":groupId", $groupId, PDO::PARAM_INT);
$select -> execute();
$fetch = $select -> fetch(PDO::FETCH_ASSOC);
$lastKey = $fetch['key']+1;
$insert = "INSERT INTO table (key, groupId, note) VALUES(:key, :groupId, :note)";
$ins = $db -> prepare($insert);
$insert -> bindValue(":key", $lastKey, PDO::PARAM_INT);
$insert -> bindValue(":groupId", $groupId, PDO::PARAM_INT);
$insert -> bindValue(":note", $note, PDO::PARAM_STR);
This method works good for me, but I am afraid of is here will be any conflict while fetching last key from table? Because, at the same time, 10 user with same groupId can add a new row. May php fetch same key to 3 users with group ID 2 at the same time?
Is there any quickly and safely way?
You can do this with AUTO_INCREMENT using MyISAM.
From MySQL Docs:
For MyISAM and BDB tables you can specify AUTO_INCREMENT on a
secondary column in a multiple-column index. ... This is useful when you want to put data into ordered groups.
Otherwise, you should set the value in your insert query with a subquery like SELECT MAX(key) + 1 FROM table WHERE groupID = 1 and read back the value.

How to check if ID exists in database, otherwise insert it into database?

I have a code which generates a 6 digit random number with the code mt_rand(100000,999999); and stores it in the variable $student_id. I check if the number exists in the database with the following code.
if($stmt = mysqli_prepare($connect, "SELECT id FROM students WHERE id = ? LIMIT 1")) {
mysqli_stmt_bind_param($stmt, "i", $student_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
mysqli_stmt_bind_result($stmt, $db_id);
mysqli_stmt_fetch($stmt);
}
if(mysqli_stmt_num_rows($stmt) == 1) {
echo "The ID exists.";
} else {
// Insert $student_id into database
}
This code can insert the ID into the database if mysqli_stmt_num_rows($stmt) == 0. But, if the ID already exists, it won't generate a new 6 digit random ID, it won't check if that ID already exists in the database, etc. I can imagine that I would have to use some kind of loop which will keep generating a random ID untill it doesn't exists in the database and then insert it into the database. I've read about a few loops (like while or for), but I don't have any idea how to use them.
Can anyone help me out with this or help me in the right direction?
Cheers
if you don't want to use autoincrement field, you can change your query in this way:
SELECT id FROM students WHERE id = ? limit 1
becames
SELECT count(id) FROM students WHERE id = ?
So you can check if the result is > 0 (exists) or not (not exists)
I created a simple PHP solution for this, while Bohemian answer is valid for a MySQL approach, here is a way to do it using do...while loop as the questionnaire wanted.
<?php
do {
$unique_id = mt_rand(100000, 900000);//generates 6 random numbers
$query = mysqli_query($mysqli, "select * from users where unique_id='$unique_id'");
} while(mysqli_num_rows($query) > 0);// repeats loop till generated id is not found
//php insert query comes here
?>
If you just need 6 digits, use an auto increment column, but have it start at 100000 instead of the usual starting value of 1.
You can do it at the time you create the table:
CREATE TABLE MYTABLE (
ID INT NOT NULL AUTO_INCREMENT,
-- other columns
)
AUTO_INCREMENT = 100000;
See this demonstrated on SQLFiddle.
Or if the table already exists with an auto increment column, you can set it:
ALTER TABLE MYTABLE AUTO_INCREMENT = 100000;

InnoDB only insert record if referenced id exists (without FOREIGN KEYS)

Foreign keys may be the best approach for this problem. However, I'm trying to learn about table locking/transactions, and so I'm hoping that we can ignore them for the moment.
Let's pretend that I have two tables in an InnoDB database: categories and jokes; and that I'm using PHP/MySQLi to do the work. The tables look like so:
CATEGORIES
id (int, primary, auto_inc) | category_name (varchar[64])
============================================================
1 knock, knock
JOKES
id (int, primary, auto_inc) | category_id (int) | joke_text (varchar[255])
=============================================================================
empty
Here are two functions, each of which is being called by a different connection, at the same time. The calls are: delete_category(1) and add_joke(1,"Interrupting cow. Interrup-MOOOOOOOO!")
function delete_category($category_id) {
// only delete the category if there are no jokes in it
$query = "SELECT id FROM jokes WHERE category_id = '$category_id'";
$result = $conn->query($query);
if ( !$result->num_rows ) {
$query = "DELETE FROM categories WHERE id = '$category_id'";
$result = $conn->query($query);
if ( $conn->affected_rows ) {
return true;
}
}
return false;
}
function add_joke($category_id,$joke_text) {
$new_id = -1;
// only add the joke if the category exists
$query = "SELECT id FROM categories WHERE id = '$category_id'";
$result = $conn->query($query);
if ( $result->num_rows ) {
$query = "INSERT INTO jokes (joke_text) VALUES ('$joke_text')";
$result = $conn->query($query);
if ( $conn->affected_rows ) {
$new_id = $conn->insert_id;
return $new_id;
}
}
return $new_id;
}
Now, if the SELECT statements from both functions execute at the same time, and proceed from there, delete_category will think it's okay to delete the category, and add_joke will think it's okay to add the joke to the existing category, so I'll get an empty categories table and an entry in the joke table that references a non-existent category_id.
Without using foreign keys, how would you solve this problem?
My best thought so far would be to do the following:
1) "LOCK TABLES categories WRITE, jokes WRITE" at the start of delete_category. However, since I'm using InnoDB, I'm quite keen to avoid locking entire tables (especially main ones that will be used often).
2) Making add_joke a transaction and then doing "SELECT id FROM categories WHERE id = '$category_id'" after inserting the record as well. If it doesn't exist at that point, rollback the transaction. However, since the two SELECT statements in add_joke might return different results, I believe I need to look into transaction isolation levels, which I'm not familiar with.
It seems to me that if I did both of those things, it should work as expected. Nevertheless, I'm keen to hear more informed opinions. Thanks.
You can DELETE a category only if is no matching joke:
DELETE c FROM categories AS c
LEFT OUTER JOIN jokes AS j ON c.id=j.category_id
WHERE c.id = $category_id AND j.category_id IS NULL;
If there are any jokes for the category, the join will find them, and therefore the outer join will return a non-null result. The condition in the WHERE clause eliminates non-null results, so the overall delete will match zero rows.
Likewise, you can INSERT a joke to a category only if the category exists:
INSERT INTO jokes (category_id, joke_text)
SELECT c.id, '$joke_text'
FROM categories AS c WHERE c.id = $category_id;
If there is no such category, the SELECT returns zero rows, and the INSERT is a no-op.
Both of these cases create a shared lock (S-lock) on the categories table.
Demonstration of an S-lock:
In one session I run:
mysql> INSERT INTO bar (i) SELECT SLEEP(600) FROM foo;
In second session I run:
mysql> SHOW ENGINE INNODB STATUS\G
. . .
---TRANSACTION 3849, ACTIVE 1 sec
mysql tables in use 2, locked 2
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 18, OS thread handle 0x7faefe7d1700, query id 203 192.168.56.1 root User sleep
insert into bar (i) select sleep(600) from foo
TABLE LOCK table `test`.`foo` trx id 3849 lock mode IS
RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`foo` trx id 3849 lock mode S
You can see that this creates an IS-lock on the table foo, and an S-lock on one row of foo, the table I'm reading from.
The same thing happens for any hybrid read/write operations such as SELECT...FOR UPDATE, INSERT...SELECT, CREATE TABLE...SELECT, to block the rows being read from being modified while they are needed as a source for the write operation.
The IS-lock is a table-level lock that prevents DDL operations on the table, so no one issues DROP TABLE or ALTER TABLE while this transaction is depending on some content in the table.

insert values between rows

I don't know if it can be done with just a sql query or it needs a php code
when a cid is missing
There exist many missing values which I can't handle manually
For example, here I don't have cid=1 and cid=6.
I want to insert a row:
cid=1 tcp_sport='undefined' tcp_dport='undefined'
and
cid=6 tcp_sport='undefined' tcp_dport='undefined'
It seems to me I should create a procedure and insert between lines
another solution that I thaught was that I will create a table with cid and undifined values with the respective order and then join this one with that one and this join should have for example ifnull(tcp_sport,'')
would you please help me?
First, use MAX for get the largest ID.
SELECT MAX(cid) as max FROM table
Then, create a for loop for checking if the individual IDs exist:
for ($i = 0; $i < $max; $i++) {
// $query = ... SELECT 1 FROM table WHERE cid = $i ...
// check if the number of rows for $query is greater than 0
// if not, INSERT INTO table VALUES ($i, DEFAULT, DEFAULT)
}
The whole idea of an auto increment ID is to have a value that only refers to one thing ever. By "inserting between the lines" you may be opening yourself up to a lot of unforeseen problems. Image you have another table that has some values that link to the CID of this table. What if that table already has an entry for CID=1, When you insert a new item with CID=1 it will then join to that supporting record. So Data that really belongs to the original item with CID=1 will show for the new item which it probably has nothing to do with.
You aren't going to run out of ID values (if you are approaching the limit of integer, switch it to bigInt), don't re-use IDs if you can avoid it.
You need to use PHP to automate this.
<?php
$link = mysql_connect("localhost", "mysql_user", "mysql_password");
mysql_select_db("database", $link);
while($i < max_value_cid)//replace max_value_cid by the numeric maximum value of cid (SELECT MAX(cid) as max FROM table)
{
$result = mysql_query("SELECT * FROM `table` WHERE cid=".$i, $link);
if(mysql_num_rows($result) == 0)
mysql_query("INSERT INTO `table` VALUES ($i, NULL, NULL);", $link);
$i++;
}
?>
Do test the query on a sample set before execution and remember to backup the entire table, just-in-case.

Categories