There are 3 different methods for updating multiple rows at once in MySQL with different values:
INSERT: INSERT with ON DUPLICATE KEY UPDATE
INSERT INTO FooBar (ID, foo)
VALUES (1, 5), (2, 8), (3, 2)
ON DUPLICATE KEY UPDATE foo=VALUES(foo);
TRANSACTION: Where you do an update for each record within a transaction (InnoDB or other DBs with transactions)
START TRANSACTION;
UPDATE FooBar SET foo=5 WHERE ID=1;
UPDATE FooBar SET foo=8 WHERE ID=2;
UPDATE FooBar SET foo=2 WHERE ID=3;
COMMIT;
CASE: In which you a case/when for each different record within an UPDATE
UPDATE FooBar SET foo=CASE ID
WHEN 1 THEN 5
WHEN 2 THEN 8
WHEN 3 THEN 2
END
WHERE ID IN (1,2,3);
I feel knowing the speeds of the 3 different methods is important.
All of the following numbers apply to InnoDB.
I just tested this, and the INSERT method was 6.7x faster for me than the TRANSACTION method. I tried on a set of both 3,000 and 30,000 rows and got the same results.
The TRANSACTION method still has to run each individually query, which takes time, though it batches the results in memory, or something, while executing. The TRANSACTION method is also pretty expensive in both replication and query logs.
Even worse, the CASE method was 41.1x slower than the INSERT method w/ 30,000 records (6.1x slower than TRANSACTION). And 75x slower in MyISAM. INSERT and CASE methods broke even at ~1,000 records. Even at 100 records, the CASE method is BARELY faster.
So in general, I feel the INSERT method is both best and easiest to use. The queries are smaller and easier to read and only take up 1 query of action. This applies to both InnoDB and MyISAM.
Bonus stuff:
Using the INSERT method, there can be a problem in which NON-NULL fields with no default (in other words, required fields) are not being updated. You will get an error like “Field 'fieldname' doesn't have a default value”. The solution is to temporarily turn off STRICT_TRANS_TABLES and STRICT_ALL_TABLES in the SQL mode: SET SESSION sql_mode=REPLACE(REPLACE(@@SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES",""). Make sure to save the sql_mode first if you plan on reverting it.
As for other comments I’ve seen that say the auto_increment goes up using the INSERT method, I tested that too and it seems to not be the case.
Code to run the tests is as follows: (It also outputs .SQL files to remove PHP interpreter overhead)
<?
//Variables
$NumRows=30000;
//These 2 functions need to be filled in
function InitSQL()
{
}
function RunSQLQuery($Q)
{
}
//Run the 3 tests
InitSQL();
for($i=0;$i<3;$i++)
RunTest($i, $NumRows);
function RunTest($TestNum, $NumRows)
{
$TheQueries=Array();
$DoQuery=function($Query) use (&$TheQueries)
{
RunSQLQuery($Query);
$TheQueries[]=$Query;
};
$TableName='Test';
$DoQuery('DROP TABLE IF EXISTS '.$TableName);
$DoQuery('CREATE TABLE '.$TableName.' (i1 int NOT NULL AUTO_INCREMENT, i2 int NOT NULL, primary key (i1)) ENGINE=InnoDB');
$DoQuery('INSERT INTO '.$TableName.' (i2) VALUES ('.implode('), (', range(2, $NumRows+1)).')');
if($TestNum==0)
{
$TestName='Transaction';
$Start=microtime(true);
$DoQuery('START TRANSACTION');
for($i=1;$i<=$NumRows;$i++)
$DoQuery('UPDATE '.$TableName.' SET i2='.(($i+5)*1000).' WHERE i1='.$i);
$DoQuery('COMMIT');
}
if($TestNum==1)
{
$TestName='Insert';
$Query=Array();
for($i=1;$i<=$NumRows;$i++)
$Query[]=sprintf("(%d,%d)", $i, (($i+5)*1000));
$Start=microtime(true);
$DoQuery('INSERT INTO '.$TableName.' VALUES '.implode(', ', $Query).' ON DUPLICATE KEY UPDATE i2=VALUES(i2)');
}
if($TestNum==2)
{
$TestName='Case';
$Query=Array();
for($i=1;$i<=$NumRows;$i++)
$Query[]=sprintf('WHEN %d THEN %d', $i, (($i+5)*1000));
$Start=microtime(true);
$DoQuery("UPDATE $TableName SET i2=CASE i1\n".implode("\n", $Query)."\nEND\nWHERE i1 IN (".implode(',', range(1, $NumRows)).')');
}
print "$TestName: ".(microtime(true)-$Start)."<br>\n";
file_put_contents("./$TestName.sql", implode(";\n", $TheQueries).';');
}
The PHP MySQL extension is being deprecated in favor of the MySQLi extension in PHP 5.5, and removed as of PHP 7.0. MySQLi was first referenced in PHP v5.0.0 beta 4 on 2004-02-12, with the first stable release in PHP 5.0.0 on 2004-07-13[1]. Before that, the PHP MySQL extension was by far the most popular way of interacting with MySQL on PHP, and still was for a very long time after. This website was opened only 2 years after the first stable release!
With the deprecation, problems from some websites I help host have popped up, many of these sites being very, very old. I needed a quick and dirty solution to monkey-patch these websites to use MySQLi without rewriting all their code. The obvious answer is to overwrite the functions with wrappers for MySQLi. The generally known way of doing this is with the Advanced PHP Debugger (APD). However, using this extension has a lot of requirements that are not appropriate for a production web server. Fortunately, another extension I recently learned of offers the renaming functionality; runkit. It was a super simple install for me.
From the command line, run “pecl install runkit”
Add “extension=runkit.so” and “runkit.internal_override=On” to the php.ini
Besides the ability to override these functions with wrappers, I also needed a way to make sure this file was always loaded before all other PHP files. The simple solution for that is adding “auto_prepend_file=/PATH/TO/FILE” to the “.user.ini” in the user’s root web directory.
The code for this script is as follows. It only contains a limited set of the MySQL functions, including some very esoteric ones that the web site used. This is not a foolproof script, but it gets the job done.
//Override the MySQL functionsforeach(Array('connect', 'error', 'fetch_array', 'fetch_row', 'insert_id', 'num_fields', 'num_rows','query', 'select_db', 'field_len', 'field_name', 'field_type', 'list_dbs', 'list_fields','list_tables', 'tablename') as$FuncName) runkit_function_redefine("mysql_$FuncName", '','return call_user_func_array("mysql_'.$FuncName.'_OVERRIDE", func_get_args());');//If a connection is not explicitely passed to a mysql_ function, use the last created connectionglobal$SQLLink; //The remembered SQL LinkfunctionGetConn($PassedConn){if(isset($PassedConn))return$PassedConn;global$SQLLink;return$SQLLink;}//Override functionsfunctionmysql_connect_OVERRIDE($Host, $Username, $Password) {global$SQLLink;return$SQLLink=mysqli_connect($Host, $Username, $Password);}functionmysql_error_OVERRIDE($SQLConn=NULL) {return mysqli_error(GetConn($SQLConn));}functionmysql_fetch_array_OVERRIDE($Result, $ResultType=MYSQL_BOTH) {returnmysqli_fetch_array($Result, $ResultType);}functionmysql_fetch_row_OVERRIDE($Result) {returnmysqli_fetch_row($Result);}functionmysql_insert_id_OVERRIDE($SQLConn=NULL) {return mysqli_insert_id(GetConn($SQLConn));}functionmysql_num_fields_OVERRIDE($Result) {return mysqli_num_fields($Result);}functionmysql_num_rows_OVERRIDE($Result) {return mysqli_num_rows($Result);}functionmysql_query_OVERRIDE($Query, $SQLConn=NULL) {returnmysqli_query(GetConn($SQLConn), $Query);}functionmysql_select_db_OVERRIDE($DBName, $SQLConn=NULL) {returnmysqli_select_db(GetConn($SQLConn), $DBName);}functionmysql_field_len_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->length;}functionmysql_field_name_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->name;}functionmysql_field_type_OVERRIDE($Result, $Offset) {$Fields=$Result->fetch_fields();return$Fields[$Offset]->type;}functionmysql_list_dbs_OVERRIDE($SQLConn=NULL) {$Result=mysql_query('SHOW DATABASES', GetConn($SQLConn));$Tables=Array();while($Row=mysqli_fetch_assoc($Result))$Tables[]=$Row['Database'];return$Tables;}functionmysql_list_fields_OVERRIDE($DBName, $TableName, $SQLConn=NULL) {$SQLConn=GetConn($SQLConn);$CurDB=mysql_fetch_array(mysql_query('SELECT Database()', $SQLConn));$CurDB=$CurDB[0];mysql_select_db($DBName, $SQLConn);$Result=mysql_query("SHOW COLUMNS FROM $TableName", $SQLConn);mysql_select_db($CurDB, $SQLConn);if(!$Result) {print'Could not run query: '.mysql_error($SQLConn);returnArray(); }$Fields=Array();while($Row=mysqli_fetch_assoc($Result))$Fields[]=$Row['Field'];return$Fields;}functionmysql_list_tables_OVERRIDE($DBName, $SQLConn=NULL) {$SQLConn=GetConn($SQLConn);$CurDB=mysql_fetch_array(mysql_query('SELECT Database()', $SQLConn));$CurDB=$CurDB[0];mysql_select_db($DBName, $SQLConn);$Result=mysql_query("SHOW TABLES", $SQLConn);mysql_select_db($CurDB, $SQLConn);if(!$Result) {print'Could not run query: '.mysql_error($SQLConn);returnArray(); }$Tables=Array();while($Row=mysql_fetch_row($Result))$Tables[]=$Row[0];return$Tables;}functionmysql_tablename_OVERRIDE($Result) {$Fields=$Result->fetch_fields();return$Fields[0]->table;}
And here is some test code to confirm functionality:
global$MyConn, $TEST_Table;$TEST_Server='localhost';$TEST_UserName='...';$TEST_Password='...';$TEST_DB='...';$TEST_Table='...';functionGetResult() {global$MyConn, $TEST_Table;returnmysql_query('SELECT*FROM'.$TEST_Table.' LIMIT 1', $MyConn);}var_dump($MyConn=mysql_connect($TEST_Server, $TEST_UserName, $TEST_Password));//Set $MyConn to NULL here if you want to test global $SQLLink functionalityvar_dump(mysql_select_db($TEST_DB, $MyConn));var_dump(mysql_query('SELECT*FROM INVALIDTABLE LIMIT1', $MyConn));var_dump(mysql_error($MyConn));var_dump($Result=GetResult());var_dump(mysql_fetch_array($Result));$Result=GetResult(); var_dump(mysql_fetch_row($Result));$Result=GetResult(); var_dump(mysql_num_fields($Result));var_dump(mysql_num_rows($Result));var_dump(mysql_field_len($Result, 0));var_dump(mysql_field_name($Result, 0));var_dump(mysql_field_type($Result, 0));var_dump(mysql_tablename($Result));var_dump(mysql_list_dbs($MyConn));var_dump(mysql_list_fields($TEST_DB, $TEST_Table, $MyConn));var_dump(mysql_list_tables($TEST_DB, $MyConn));mysql_query('CREATE TEMPORARY TABLE mysqltest (i int auto_increment, primary key (i))', $MyConn);mysql_query('INSERT INTO mysqltest VALUES ()', $MyConn);mysql_query('INSERT INTO mysqltest VALUES ()', $MyConn);var_dump(mysql_insert_id($MyConn));mysql_query('DROP TEMPORARY TABLE mysqltest', $MyConn);
One of the companies I work for recently moved one of our cPanel servers to a new collocation, still running cPanel. We decided to use a new backup solution called r1soft, which so far has been working spectacularly. I’d love to use it for my personal computers, except the licenses, which are geared towards enterprise business, are way too costly.
However, since r1soft only backs up files (on the incrementally block level, yay) you can’t use it to restore a cPanel account. It can only restore things like the user’s home directory and SQL databases. Because of this, when we had need to restore an entire account today, and found out there is no easy/quick way to do it, we were up a creek. The obvious future solution for this would be to use cPanel’s backup (or legacy backup) systems, but unfortunately, you can’t easily set them to not backup the user’s databases and home directory, which can be very large, and are already taken care of by r1soft. I ended up adding the following script, ran nightly via cron, to back up user account settings.
It saves all the user settings under the backup path in their own directory, uncompressed, and named cpmove-USERNAME. It is best to do it this way so r1soft’s incremental backups don’t have much extra work if anything changes. Make sure to change line 3 in the following script to the path where you want backups to occur.
#!/bin/bash#Create and move to backup directory
BACKUPDIR=/backup/userbackup
mkdir -p $BACKUPDIR#Make sure the directory existscd$BACKUPDIR#Remove old backups
rm -rf cpmove-*#Loop over accountsforUSERin`/usr/sbin/whmapi1 listaccts | grep -oP '(?<=user: )\w+$'| sort -u`;do#Backup the account
/scripts/pkgacct --nocompress --skipbwdata --skiphomedir --skiplogs --skipmysql --skipmailman $USER ./
#Extract from and remove the tar container file
tar -xvf cpmove-$USER.tar
rm -f cpmove-$USER.tar
#Save MySQL user settings
mysqldump --compact -fnt -w "User LIKE '$USER""_%'" mysql user db tables_priv columns_priv procs_priv proxies_priv \
| perl -pe "s~('|NULL)\),\('~\1),\n('~ig" \
> cpmove-$USER/mysql-users.sql
done;
This script skips a few backup items that need to be noted. Mailman, logs, homedir, and bandwidth data should all be easy 1:1 copy over restores from r1soft. I excluded them because those can take up a lot of room, which we want r1soft to handle. The same goes for MySQL, except that your MySQL users are not backed up to your account, which is why I added the final section.
Do note, for the final section, the line starting with “| perl” is optional. It is there to separate the insert rows into their own lines. A very minor warning though; it would also pick up cases where the last field in MySQL’s user table ends in “NULL),(”. This would only happen if someone is trying to be malicious and knew about this script, and even then, it couldn’t harm anything.
Bonus note: To restore a MySQL database which does not use a shared-file (like InnoDB does by default), you could actually stop the MySQL server, copy over the binary database files, and start the server back up.
After version 4.2.0.0 (2014-05-08) of phpMyAdmin, it stopped including table’s keys inline within the create table statement, and instead opted to add all the table keys at the very end of the export file by modifying the tables. (See "rfe #1004 Create indexes at the end in SQL export). This behavior has been annoying to many people, including myself, but I never noticed anyone mentioning a fix. I looked into the source and there is a very simple way to restore this behavior to what it originally was.
Edit the file “phpMyAdmin/libraries/plugins/export/ExportSql.class.php”. In it, the code block starting with the below line needs to be skipped
if (preg_match('@CONSTRAINT|KEY@', $create_query)) {
The easiest way to do this is changing that line to
if (false && preg_match('@CONSTRAINT|KEY@', $create_query)) {
I just threw together a quick script to report status on a MySQL replication ring. While replication rings have been the only real multi-master MySQL solution for replication (with the ability for nodes to go down without majorly breaking things) until recently, I have read that MariaDB (still not MySQL) now allows a slave to have multiple masters, meaning many replication topologies are now possible (star, mesh, etc). This script could easily be adapted for those circumstances too.
This script will report all the variables from “SHOW MASTER STATUS” and “SHOW SLAVE STATUS” from all servers in your replication ring, in a unified table. It also includes a “Pretty Status” row that lets you quickly see how things look. The possibilities for this row are:
Bad state: ...
This shows if the Slave_IO_State is not “Waiting for master to send event”
Cannot determine master’s real position
This shows if the Position variable on the master could not be read
On old master file
This shows if the slave’s “Master_Log_File” variable does not equal the master’s “File” variable
Bytes behind: xxx
This shows if none of the above errors occurred. It subtracts the master’s “Position” from the slave’s “Read_Master_Log_Pos”. This should generally be at or around 0. A negative value essentially means 0 (this should only happen between the last and first server).
The “Seconds_Behind_Master” variable can also be useful for determining the replication ring’s current replication status.
The code is below the example. The entire source file can also be found here. The 3 variables that need to be configured are at the top of the file. It assumes that all servers are accessible via the single given username and password.
Example:
Master
Server Name
EXAMPLE1.MYDOMAIN.COM
EXAMPLE2
File
mysql-bin.000003
mysql-bin.000011
Position
25249746
3215834
Binlog_Do_DB
example_data,devexample_data
example_data,devexample_data
Binlog_Ignore_DB
Slave
Pretty Status
Bytes behind: 0
Bytes behind: 0
Slave_IO_State
Waiting for master to send event
Waiting for master to send event
Master_Host
EXAMPLE2
EXAMPLE1.MYDOMAIN.COM
Master_User
example_slave
example_slave
Master_Port
3306
3306
Connect_Retry
60
60
Master_Log_File
mysql-bin.000011
mysql-bin.000003
Read_Master_Log_Pos
3215834
25249746
Relay_Log_File
www-relay-bin.070901
www-relay-bin.071683
Relay_Log_Pos
252
252
Relay_Master_Log_File
mysql-bin.000011
mysql-bin.000003
Slave_IO_Running
Yes
Yes
Slave_SQL_Running
Yes
Yes
Replicate_Do_DB
example_data,devexample_data
example_data,devexample_data
Replicate_Ignore_DB
Replicate_Do_Table
Replicate_Ignore_Table
Replicate_Wild_Do_Table
Replicate_Wild_Ignore_Table
Last_Errno
0
0
Last_Error
Skip_Counter
0
0
Exec_Master_Log_Pos
3215834
25249746
Relay_Log_Space
552
552
Until_Condition
None
None
Until_Log_File
Until_Log_Pos
0
0
Master_SSL_Allowed
No
No
Master_SSL_CA_File
Master_SSL_CA_Path
Master_SSL_Cert
Master_SSL_Cipher
Master_SSL_Key
Seconds_Behind_Master
0
0
Master_SSL_Verify_Server_Cert
No
No
Last_IO_Errno
0
0
Last_IO_Error
Last_SQL_Errno
0
0
Last_SQL_Error
Replicate_Ignore_Server_Ids
Not given
Master_Server_Id
2
Not given
Code:
<?//Configurations$Servers=Array('SERVER1.YOURDOMAIN.COM', 'SERVER2.YOURDOMAIN.COM'); //List of host names to access mysql servers on. This must be in the order of the replication ring.$SlaveUserName='SLAVE_RING_USERNAME'; //This assumes all servers are accessible via this username with the same password$SlavePassword='SLAVE_RING_PASSWORD';//Get the info for each server$ServersInfo=Array(); //SERVER_NAME=>Array('Master'=>Array(Col1=>Val1, ...), 'Slave'=>Array(Col1=>Val1, ...)$ColsNames=Array('Master'=>Array('Server Name'=>0), 'Slave'=>Array('Pretty Status'=>0)); //The column names for the 2 (master and slave) queries. Custom column names are also added here$CustomFieldNames=array_merge($ColsNames['Master'], $ColsNames['Slave']); //Store the custom column names so they are not HTML escaped laterforeach($Serversas$ServerName){//Connect to the server$Link=@newmysqli($ServerName, $SlaveUserName, $SlavePassword);if($Link->connect_error)die(EHTML("Connection error to $ServerName server: $Link->connect_error"));//Get the replication status info from the server$MyServerInfo=$ServersInfo[$ServerName]=Array('Master'=>$Link->Query('SHOW MASTER STATUS')->fetch_array(MYSQLI_ASSOC),'Slave'=>$Link->Query('SHOW SLAVE STATUS')->fetch_array(MYSQLI_ASSOC) );mysqli_close($Link); //Close the connection//Gather the column namesforeach($ColsNamesas$ColType=>&$ColNames)foreach($MyServerInfo[$ColType] as$ColName=>$Dummy)$ColNames[$ColName]=0;}unset($ColNames);//Gather the pretty statusesforeach($Serversas$Index=>$ServerName){//Determine the pretty status$SlaveInfo=$ServersInfo[$ServerName]['Slave'];$MasterInfo=$ServersInfo[$Servers[($Index+1)%count($Servers)]]['Master'];if($SlaveInfo['Slave_IO_State']!='Waiting for master to send event')$PrettyStatus='Bad state: '.EHTML($SlaveInfo['Slave_IO_State']);elseif(!isset($MasterInfo['Position']))$PrettyStatus='Cannot determine master’s real position';elseif($SlaveInfo['Master_Log_File']!=$MasterInfo['File'])$PrettyStatus='On old master file';else$PrettyStatus='Bytes behind: '.($MasterInfo['Position']-$SlaveInfo['Read_Master_Log_Pos']);//Add the server name and pretty status to the output columns$ServersInfo[$ServerName]['Master']['Server Name']='<div class=ServerName>'.EHTML($ServerName).'</div>';$ServersInfo[$ServerName]['Slave']['Pretty Status']='<div class=PrettyStatus>'.EHTML($PrettyStatus).'</div>';}//Output the documentfunctionEHTML($S) { returnhtmlspecialchars($S, ENT_QUOTES, 'UTF-8'); } //Escape HTML?>
<!DOCTYPE html>
<html>
<head>
<title>Replication Status</title>
<metacharset="UTF-8">
<style>table { border-collapse:collapse; }tabletr>* { border:1pxsolidblack; padding:3px; }th { text-align:left; font-weight:bold; }.ReplicationDirectionType { font-weight:bold; text-align:center; color:blue; }.ServerName { font-weight:bold; text-align:center; color:red; }.PrettyStatus { font-weight:bold; color:red; }.NotGiven { font-weight:bold; } </style>
</head>
<body><table>
<?//Output the final tableforeach($ColsNamesas$Type=>$ColNames) //Process by direction type (Master/Slave) then columns{print'<tr><td colspan='.(count($Servers)+1).' class=ReplicationDirectionType>'.$Type.'</td></tr>'; //Replication direction (Master/Server) type title columnforeach($ColNamesas$ColName=>$Dummy) //Process each column name individually {print'<tr><th>'.EHTML($ColName).'</th>'; //Column name$IsHTMLColumn=isset($CustomFieldNames[$ColName]); //Do not escape HTML on custom fieldsforeach($ServersInfoas$ServerInfo) //Output the column for each serverif($IsHTMLColumn) //Do not escape HTML on custom fieldsprint'<td>'.$ServerInfo[$Type][$ColName].'</td>';else//If not a custom field, output the escaped HTML of the value. If the column does not exist for this server (different mysql versions), output "Not given"print'<td>'.(isset($ServerInfo[$Type][$ColName]) ? EHTML($ServerInfo[$Type][$ColName]) : '<div class=NotGiven>Not given</div>').'</td>';print'</tr>'; }}?>
</table></body>
</html>
One final note. When having this script run, you might need to make sure none of the listed server IPs evaluates to localhost (127.x.x.x), as MySQL may instead then use the local socket pipe, which may not work with users who only have REPLICATION permissions and a wildcard host.
Sigh, I just realized after writing this post that I had already covered this topic... oh well, this version has some new information the other one is missing.
I find people very often asking me to move data from an Excel spreadsheet to a MySQL database, so I thought I’d write up the procedure I follow when doing so. This assumes no multi-line cells or tabs in the excel spreadsheet data.
You need a good text editor with regular expression support. I highly recommend EditPad Pro (a free version is available too), and will be assuming you are using it for the steps below.
Make sure all data in the Excel spreadsheet is formatted for SQL insertion, for example:
To convert a date “mm/dd/yyyy” to SQL:
Copy the entire row to your text editor
Run the following regular expression replace:
Find Text
Replace Text
^(\d+)/(\d+)/(\d+)$
$3-$1-$2
Copy the text back to the spreadsheet row
Copy all the data into the text editor, and run the following regular expressions:
Find Text
Replace Text
Explanation
\\
\\\\
Escape backslash
'
\\'
Escape single quotation mark
\t
','
Change separators so that all values are encased as strings
^
('
Line prefix to insert a row and stringify the first value
$
'),
Line suffix to insert a row and stringify the last value
Change the very last character on the last line from a comma to a semi colon to end the query
Add the following to the top of the file:
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
SET CHARACTER SET 'utf8';
TRUNCATE TABLE TABLE_NAME;
INSERT INTO TABLE_NAME (Field1, Field2, ...) VALUES
Make sure the file is saved as UTF8: Menu -> Convert -> Text Encoding -> (Encode the data with another character set ...) AND (Unicode, UTF-8)
Make sure the file is saved with Unix line breaks: Menu -> Convert -> To Unix (LF Only)
Save the file and run the following in your MySQL command line prompt to import it:
\u DATABASE_NAME
\. FILE_NAME
There are of course easier solutions, but they can often be buggy, and I figured this is a good primer on regular expressions and simple data manipulation :-)
As of 2010, if you wanted to set up a MySQL replication configuration with multiple servers which could all update and send the updates to the other servers, a replication ring was the only solution (in which every server has a master and a slave in a ring configuration). While there are new (probably better) solutions as of late including using MariaDB’s multi-source replication, and tungsten-replicator (which I was referred to in late April and have not yet tried), I still find a replication ring to be an easy to use solution in some circumstances. However, there are some major disadvantages including:
If one node in the ring goes down, the entire ring stops replicating at that point until the node is brought back up
If a node goes down in the ring and has corrupted or incomplete data, say, from a power outdate, the entire ring may have to be painstakingly synced and rebuilt.
Anywho, the following is my basic MySQL configurations for setting up a replication ring, which needs to be put on every server node in the ring: (See MySQL docs for more information on each configuration)
[mysqld]
#---GENERAL REPLICATION VARIABLES--- (These should never change)
log_bin=mysql-bin.log #Turn on the binary log, which is used to hold queries that are propagated to other machines
slave-skip-errors = 1062 #Do not stop replication if a duplicate key is found (which shouldn’t happen anyways). You may want to turn this off to verify integrity, but then your replication ring will stop if a duplicate key is found
#master-connect-retry = 5 #How often to keep trying to reconnect to the master node of the current machine after a connection is lost. This has been removed as of MySQL 5.5, and needs to instead be included in the “CHANGE MASTER” command
sync_binlog = 100 #After how many queries, sync to the binlog
slave_net_timeout = 120 #The number of seconds to wait for more data from a master/slave before restarting the connection
max_binlog_size = 1000M #The maximum size the binlog can get before creating a new binlog
log-slave-updates #Log slave updates to the binary log. If there are only 2 nodes in the ring, this is not required
slave_exec_mode = IDEMPOTENT #Suppression of duplicate-key and no-key-found errors
replicate-same-server-id = 0 #Skip running SQL commands received via replication that were generated by the current server node
#---INDEPENDENT REPLICATION VARIABLES--- (These should change per setup)
binlog-do-db = DATABASE_NAME #Only add statements from this database to the binlog. You can have multiple of these configurations
replicate-do-db = DATABASE_NAME #Only read statements from this database during replication. You can have multiple of these configurations
auto_increment_increment = 2 #After ever auto-increment, add this to its number. Set this to the number of nodes. This helps assures no duplicate IDs
#---SERVER CONFIGURATION--- (These numbers should match)
server-id = 1 #The current node number in the ring. Every node needs to have its own incremental server number starting at 1
auto_increment_offset = 1 #What to start auto-increment numbers at for this server. Set this to the server-id. This helps assures no duplicate IDs
I had the need today to be able to set the current time zone for an application in multiple computer languages by the hourly offset from GMT/UTC, which turned out to be a lot harder than I expected. It seems most time zone related functions, at least in Linux, expect you to use full location strings to set the current time zone offset (i.e. America/Chicago).
After a lot of research and experimenting, I came up with the following results. All of these are confirmed working in Linux, and most or all of them should work in Windows too.
Language
Format Note
Format for GMT+5
Format for GMT-5
C
Negate
GMT-5
GMT5
Perl
Negate
GMT-5
GMT5
SQL
Requires Sign
+5:00
-5:00
PHP
Negate, Requires Sign
Etc/GMT-5
Etc/GMT+5
And here are examples of using this in each language. The “TimeZone” string variable should be a 1-2 digit integer with an optional preceding negative sign:
I am often asked to transfer data sets into MySQL databases, or other formats. In this case, I’ll use a Microsoft Excel file without line breaks in the fields to MySQL as an example. While there are many programs out there to do this kind of thing, this method doesn’t take too long and is a good example use of regular expressions.
First, select all the data in Excel (ctrl+a) and copy (ctrl+c) it to a text editor with regular expression support. I recommend EditPad Pro as a very versatile and powerful text editor.
Next, we need to turn each row into the format “('FIELD1','FIELD2','FIELD3',...),”. Four regular expressions are needed to format the data:
Search
Replace
Explanation
'
\\'
Escape single quotes
\t
','
Separate fields and quote as strings
^
('
Start of row
$
'),
End of row
From there, there are only 2 more steps to complete the query.
Add the start of the query: “INSERT INTO TABLENAME VALUES”
End the query by changing the last row's comma “,” at the very end of the line to a semi-colon “;”.
For example:
a b c
d e f
g h i
would be converted to
INSERT INTO MyTable VALUES
('a','b','c'),
('d','e','f'),
('h','h','i');
Sometimes queries may get too long and you will need to separate them by performing the “2 more steps to complete the query” from above.
After doing one of these conversions recently, I was also asked to make the data searchable, so I made a very simple PHP script for this.
This script lets you search through all the fields and lists all matches. The fields are listed on the 2nd line in an array as "SQL_FieldName"=>"Viewable Name". If the “Viewable Name” contains a pound sign “#” it is matched exactly, otherwise, only part of the search string needs to be found.
<?
$Fields=Array('ClientNumber'=>'Client #', 'FirstName'=>'First Name', 'LastName'=>'Last Name', ...); //Field list
print '<form method=post action=index.php><table>'; //Form action needs to point to the current file
foreach($Fields as $Name => $Value) //Output search text boxes
print "<tr><td>$Value</td><td><input name=\"$Name\" style='width:200px;' value=\"".
(isset($_POST[$Name]) ? htmlentities($_POST[$Name], ENT_QUOTES) : '').'"></td></tr>';//Text boxes w/ POSTed values,if set
print '</table><input type=submit value=Search></form>';
if(!isset($_POST[key($Fields)])) //If search data has not been POSTed, stop here
return;
$SearchArray=Array('1=1'); //Search parameters are stored here. 1=1 is passed in case no POSTed search parameter are ...
//... requested so there is at least 1 WHERE parameter, and is optimized out with the MySQL preprocessor anyways.
foreach($Fields as $Name => $Value) //Check each POSTed search parameter
if(trim($_POST[$Name])!='') //If the POSTed search parameter is empty, do not use it as a search parameter
{
$V=mysql_escape_string($_POST[$Name]); //Prepare for SQL insertion
$SearchArray[]=$Name.(strpos($Value, '#')===FALSE ? " LIKE '%$V%'" : "='$V'"); //Pound sign in the Viewable Name=exact ...
//... value, otherwise, just a partial patch
}
//Get data from MySQL
mysql_connect('SQL_HOST', 'SQL_USERNAME', 'SQL_PASSWORD');
mysql_select_db('SQL_DATABASE');
$q=mysql_query('SELECT * FROM TABLENAME WHERE '.implode(' AND ', $SearchArray));
//Output retrieved data
$i=0;
while($d=mysql_fetch_assoc($q)) //Iterate through found rows
{
if(!($i++)) //If this is the first row found, output header
{
print '<table border=1 cellpadding=0 cellspacing=0><tr><td>Num</td>'; //Start table and output first column header (row #)
foreach($Fields as $Name => $Value) //Output the rest of the column headers (Viewable Names)
print "<td>$Value</td>";
print '</tr>'; //Finish header row
}
print '<tr bgcolor='.($i&1 ? 'white' : 'gray')."><td>$i</td>"; //Start the data field's row. Row's colors are alternating white and gray.
foreach($Fields as $Name => $Value) //Output row data
print '<td>'.$d[$Name].'</td>';
print '</tr>'; //End data row
}
print ($i==0 ? 'No records found.' : '</table>'); //If no records are found, output an error message, otherwise, end the data table
?>