#MySQL# 文章列表 MySQL:简介 Ubuntu 安装 MySQL MySQL:官方文档地址 MySQL:DDL、DQL、DML、DCL的含义 MySQL:Mac 中启动 关闭 MySQL 服务 MySQL:有哪些好用的管理工具? MySQL:命令行工具 mycli MySQL:CHAR类型 MySQL:VARCHAR类型 MySQL:整型数字 MySQL:datetime 类型 MySQL:时间戳 MySQL:创建和删除数据库 MySQL:切换和查看数据库 MySQL:创建和删除表 MySQL:在表中增加、删除、修改列 MySQL:创建和删除主键 MySQL:使用 rename 修改表名 MySQL:修改自增主键id的类型 MySQL:如何创建一个相同的表 MySQL:修改表的字符编码 MySQL:增删查改 MySQL:插入数据 MySQL:插入多行数据 MySQL:使用 insert set 插入数据 MySQL:大小写和反引号 MySQL:字符串类型值的大小写 MySQL:SQL注释 MySQL:不要使用utf8 MySQL:NULL的判等 MySQL:InnoDB存储引擎的限制 MySQL:if和case的使用 MySQL:使用 load data 快速导入数据 MySQL:使用 select into outfile 导出数据 MySQL:查询和设置 sql_mode MySQL:严格模式 MySQL:NOT NULL 字段不插入数据,会发生什么? MySQL:无符号整数列插入负数会发生什么? MySQL:关于 null 的那些事 MySQL:大表行数查询 MySQL:自动生成创建时间、更新时间;自动更新更新时间 MySQL:insert ignore MySQL:字符集排序规则 MySQL:如果连续更新一个字段两次,结果是? MySQL:字符串转数字 MySQL:尾部空格 MySQL:添加和删除索引 MySQL:唯一索引与NULL MySQL:唯一索引的单列长度限制 MySQL:InnoDB 索引 MySQL:字符集排序规则对唯一索引的影响 MySQL:唯一索引冲突消耗主键 ID MySQL 使用 index hint 指定索引:ignore index、force index、use index MySQL:查看客户端连接信息 MySQL:查看表的状态 show table status MySQL:如何治理连接数 ? MySQL:如何监控和处理慢查询与长事务 ? MySQL:自定义函数 MySQL:now() 函数 MySQL:unix_timestamp() 函数 MySQL:from_unixtime() 函数 MySQL:version() 函数 MySQL:current_timestamp() 函数 MySQL:cast 函数 MySQL:convert 函数 MySQL:使用 greatest、least 函数获取行最大值、最小值 MySQL:使用 group_concat 函数连接多行数据为一个字符串 MySQL:获取版本号 MySQL:Java 类型映射 MySQL下创建只能有一行记录的table 关于MySQL的字符集 理解数据库中的undo日志、redo日志、检查点 ubuntu下源码安装MySQL MySQL:JOIN解惑 如何快速更新数据库中的百万条数据

如何快速更新数据库中的百万条数据


#MySQL#


2015-06-22

环境:ubuntu 14.04,Mysql 5.6,python 2.7。

本文使用python的MySQLdb这个库,MySQL Python tutorial这个教程很赞。

MySQLDBdb处理INSERT、UPDATE、DELETE时需要显式得commit()才能使更新有效,这也意味着commit之前的更新语句是放在一个事务中的。

For databases that support transactions, the Python interface silently starts a transaction when the cursor is created. The commit() method commits the updates made using that cursor, and the rollback() method discards them. Each method starts a new transaction.

对比如下:

更新方式 总时间(ms) rows 平均(ms)
1 57831 1000 57.8
2 89695 10 896
3 92941 40×10000 0.23
4 47956 40×10000 0.119
5 28407 40×10000 0.071
6 3272 48×10000 0.008

方式1是单线程,where使用了索引,一个更新对应一个commit。
方式2是单线程,where没有使用索引,所有更新对应一个commit。
方式3是单线程,where使用了索引,所有更新对应一个commit。
方式4使用了2个进程来更新,where使用了索引,每个进程里的所有更新对应一个commit。
方式5使用了4个进程来更新,where使用了索引,每个进程里的所有更新对应一个commit。
方式6是在一个update语句里面更新所有的数据。

建立表

mysql> CREATE TABLE `test`.`number` (
  `id` INT NOT NULL COMMENT '',
  `num` INT NULL COMMENT '',
  PRIMARY KEY (`id`)  COMMENT '');

mysql> show create table number \G
*************************** 1. row ***************************
       Table: number
Create Table: CREATE TABLE `number` (
  `id` int(11) NOT NULL,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

使用mysqldb插入数据(100*000条)

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306


start_time = time.time()

conn = MySQLdb.connect(host, user, passwd, db, port)
cur = conn.cursor()

for x in xrange(100*10000): # 0 到 999999
    print x
    sql = "INSERT INTO number VALUES(%s,%s)" % (x, x)
    cur.execute(sql)

conn.commit()

end_time = time.time()

print '用时:', end_time - start_time

运行结果:

...
999997
999998
999999
用时:219.332565069

挺快。

看下数据:

mysql> select count(*) from number;
+----------+
| count(*) |
+----------+
|  1000000 |
+----------+
1 row in set (0.60 sec)

mysql> select * from number limit 3;
+----+------+
| id | num  |
+----+------+
|  0 |    0 |
|  1 |    1 |
|  2 |    2 |
+----+------+
3 rows in set (0.00 sec)

更新方式1:单线程,使用索引,多个commmit,更新1000条

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306


start_time = time.time()

conn = MySQLdb.connect(host, user, passwd, db, port)
cur = conn.cursor()

for x in xrange(1000):
    print x
    sql = "UPDATE number SET num=%s where id=%s" % (x+1, x)
    cur.execute(sql)
    conn.commit()

cur.close()
conn.close()

end_time = time.time()

print '用时:', end_time - start_time

输出:

...
997
998
999
用时:57.8318688869

1000条用时约60秒。

为什么慢?

先explain一下:

mysql> explain update number set num=3 where id=2 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: number
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra: Using where
1 row in set (0.06 sec)

sql语句没什么问题。

再看一下磁盘IO:

letian $ iostat -d -k 1 100
Linux 3.13.0-37-generic (myhost)  06/22/2015  _x86_64_  (4 CPU)

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              57.00         4.00       468.00          4        468

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              71.00        28.00       404.00         28        404

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              71.00        28.00       608.00         28        608

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              61.00         4.00       492.00          4        492

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              73.00         0.00       536.00          0        536

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              69.00         4.00       516.00          4        516

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              61.00         4.00       472.00          4        472

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              63.00         4.00       452.00          4        452

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              16.00         0.00      1804.00          0       1804

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               3.00         0.00        16.00          0         16

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               0.00         0.00         0.00          0          0

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               2.00         0.00        12.00          0         12

tps代表每秒IO传输的次数,与下面的更新方式4对比一下,可以看出当前的更新方式IO较多。(为什么呢?移步Why does TRANSACTION / COMMIT improve performance so much with PHP/MySQL (InnoDB)?

更新方式2,单线程,不使用索引,一个commit,更新100条

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306


start_time = time.time()

conn = MySQLdb.connect(host, user, passwd, db, port)
cur = conn.cursor()

for x in xrange(100):
    print x
    sql = "UPDATE number SET num=%s where num=%s" % (x*2, x)
    cur.execute(sql)

conn.commit()
cur.close()
conn.close()

end_time = time.time()

print '用时:', end_time - start_time

输出:

...
97
98
99
用时: 89.695207119

为什么这么慢:

mysql> explain update number set num=4 where num=2 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: number
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 979800
        Extra: Using where
1 row in set (0.05 sec)

可以看到,每次update几乎要扫全表(rows: 979800)。

更新方式3:单线程,使用索引,一个commit,更新40×10000条

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306

start_time = time.time()

conn = MySQLdb.connect(host, user, passwd, db, port)
cur = conn.cursor()

for x in xrange(40*10000): # 0 到 999999
    print x
    sql = "UPDATE number SET num=%s where id=%s" % (x+1, x)
    cur.execute(sql)

conn.commit()

end_time = time.time()

print '用时:', end_time - start_time

输出:

...
399997
399998
399999
用时:92.9413559437

查看磁盘IO:

letian $ iostat -d -k 1 100
Linux 3.13.0-37-generic (myhost)  06/22/2015  _x86_64_  (4 CPU)

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              20.88       288.50       283.18    2458129    2412772

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              18.00       116.00       328.00        116        328

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              17.00         8.00       548.00          8        548

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              11.00        12.00       396.00         12        396

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       488.00          4        488

............


Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       460.00          4        460

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       448.00          4        448

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       464.00          4        464

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       456.00          4        456

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               9.00         4.00       452.00          4        452

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               9.00         0.00       140.00          0        140

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              15.00         4.00      2448.00          4       2448

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              43.00       144.00      2544.00        144       2544

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               9.00         0.00       516.00          0        516

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       512.00          4        512

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              12.00         4.00       540.00          4        540

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              14.00         4.00      2288.00          4       2288

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              10.00         4.00       528.00          4        528

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               6.00         0.00       184.00          0        184

............

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              14.00         0.00      3868.00          0       3868

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              15.00         0.00      2604.00          0       2604

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              15.00         0.00      1516.00          0       1516

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               3.00         0.00        16.00          0         16

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda              12.00         0.00        68.00          0         68

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               4.00         0.00        28.00          0         28

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               0.00         0.00         0.00          0          0

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               0.00         0.00         0.00          0          0

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
sda               0.00         0.00         0.00          0          0

更新方式4:2个进程,一个进程里一个commit,共更新40*10000条

两个进程:

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time
import multiprocessing

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306

start_time = time.time()

def update(int_iter):
    conn = MySQLdb.connect(host, user, passwd, db, port)
    cur = conn.cursor()
    for x in int_iter:
        print x
        sql = "UPDATE number SET num=%s where id=%s" % (x+5, x)
        cur.execute(sql)
    conn.commit()
    cur.close()
    conn.close()

pool = multiprocessing.Pool(processes = 2)

pool.apply_async(update, (xrange(20*10000), ))
pool.apply_async(update, (xrange(20*10000, 40*10000), ))
pool.close()
pool.join()

end_time = time.time()

print '用时:', end_time - start_time

输出:

...
199998
199999
用时: 47.9561839104

看下数据:

mysql> select * from number where id=1;
+----+------+
| id | num  |
+----+------+
|  1 |    6 |
+----+------+
1 row in set (0.00 sec)

mysql> select * from number where id=399999;
+--------+--------+
| id     | num    |
+--------+--------+
| 399999 | 400004 |
+--------+--------+
1 row in set (0.00 sec)

更新方式5:4个进程,一个进程里一个commit,共更新40*10000条

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time
import multiprocessing

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306

start_time = time.time()

def update(int_iter):
    conn = MySQLdb.connect(host, user, passwd, db, port)
    cur = conn.cursor()
    for x in int_iter:
        print x
        sql = "UPDATE number SET num=%s where id=%s" % (x+10, x)
        cur.execute(sql)
    conn.commit()
    cur.close()
    conn.close()

pool = multiprocessing.Pool(processes = 4)

pool.apply_async(update, (xrange(10*10000), ))
pool.apply_async(update, (xrange(10*10000, 20*10000), ))
pool.apply_async(update, (xrange(20*10000, 30*10000), ))
pool.apply_async(update, (xrange(30*10000, 40*10000), ))

pool.close()
pool.join()

end_time = time.time()

print '用时:', end_time - start_time

运行结果:

299996
299997
299998
299999
用时: 28.4070010185

查看更新效果:

mysql> select * from number where id=1;
+----+------+
| id | num  |
+----+------+
|  1 |   11 |
+----+------+
1 row in set (0.00 sec)

mysql> select * from number where id=399999;
+--------+--------+
| id     | num    |
+--------+--------+
| 399999 | 400009 |
+--------+--------+
1 row in set (0.00 sec)

更新方式6:一次更新多行,更新40*10000条

# coding: UTF-8
__author__ = 'letian'

import MySQLdb
import time

host='127.0.0.1'
user='root'
passwd='123456'
db='test'
port = 3306

start_time = time.time()

conn = MySQLdb.connect(host, user, passwd, db, port)
cur = conn.cursor()

sql = "UPDATE number SET num=id+100 where id<%s" % (40*10000, )
cur.execute(sql)
conn.commit()

cur.close()
conn.close()

end_time = time.time()

print '用时:', end_time - start_time

输出:

用时: 3.27281808853

查看更新效果:

mysql> select * from number where id=1;
+----+------+
| id | num  |
+----+------+
|  1 |  101 |
+----+------+
1 row in set (0.00 sec)

mysql> select * from number where id=399999;
+--------+--------+
| id     | num    |
+--------+--------+
| 399999 | 400099 |
+--------+--------+
1 row in set (0.00 sec)

分析使用的sql语句:

mysql> explain UPDATE number SET num=id+100 where id<400000 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: number
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 489900
        Extra: Using where
1 row in set (0.00 sec)

这种方式非常快,灵活性不够。

如果遇到这样一个场景:将id为0、1、2的num置为2,其他全部置为4,可以先全部置为4,然后在更新id为0、1、2的记录,这样速度会很快。


( 本文完 )