执行计划错误—索引统计信息的不准确


mysql在生成执行计划的时候,需要根据索引的统计信息进行一个估算,计算出成本最低的索引;
但是mysql索引统计信息的采集默认8个page:
root@test 08:48:52>show global variables like ‘%samp%’;
+—————————+——-+
| Variable_name             | Value |
+—————————+——-+
| innodb_stats_sample_pages | 8     |
+—————————+——-+
5.1估算rows estimate的算法存在bug http://bugs.mysql.com/bug.php?id=53761 ,
么你的执行计划很有可能由于索引统计信息的不准确,导致优化不能够正确的选择索引:
测试案例,表中的索引添加由于测试:
CREATE TABLE `recommend_0202` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
`status` tinyint(3) unsigned DEFAULT ’0′ COMMENT ‘推荐菜状态’,
`user_id_kb` int(10) unsigned DEFAULT ’0′ COMMENT ‘推荐菜的用户id’,
`user_id` bigint(20) unsigned DEFAULT ’0′ COMMENT ‘推荐菜的淘宝用户id’,
`review_id` varchar(32) NOT NULL COMMENT ‘推荐菜针对的点评id’,
`target_id` varchar(32) NOT NULL COMMENT ‘推荐菜针对的店铺id’,
`recommend` varchar(32) NOT NULL COMMENT ‘推荐菜的内容’,
`gmt_create` datetime NOT NULL COMMENT ‘记录创建时间’,
`gmt_modified` datetime NOT NULL COMMENT ‘记录最后修改时间’,
PRIMARY KEY (`id`),
KEY `ind_review_staus` (`review_id`,`status`),
KEY `index_reivew` (`review_id`),
KEY `ind_user_id` (`user_id`,`status`,`gmt_create`,`target_id`),
KEY `ind_user_gmt` (`user_id`,`gmt_create`)
) ENGINE=InnoDB;

我们来看一个查询:

select status from recommend_0202  where review_id=’00000015bf5445a88462ba047aab114c’ and status=1;

root@test 10:27:00>explain select status from recommend_0202  where review_id=’00000015bf5445a88462ba047aab114c’ and status=1;
+—-+————-+—————-+——+——————————-+————
| id | select_type | table          | type | possible_keys                 | key          | key_len | ref   | rows | Extra       |
+—-+————-+—————-+——+——————————-+————
|  1 | SIMPLE      | recommend_0202 | ref  | index_review,ind_review_staus | index_review | 66      | const |    1 | Using where |
+—-+————-+—————-+——+——————————-+————
1 row in set (0.00 sec)
这里可以看到优化器选择了index_reivew这个索引,然后在回表过滤,而并没有选择reivew_id,status这个索引来覆盖查询,这里优化器就没有进行优化的选择了;
那我们看看是不是统计信息出来问题:
root@test 10:27:52>show  index from recommend_0202;
+—————-+————+——————+————–+————-+———
| Table          | Non_unique | Key_name         | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+—————-+————+——————+————–+————-+———
| recommend_0202 |          0 | PRIMARY          |            1 | id          | A         |     2301924 |     NULL | NULL   |      | BTREE      |         |
| recommend_0202 |          1 | index_review     |            1 | review_id   | A         |     2301924 |     NULL | NULL   |      | BTREE      |         |
| recommend_0202 |          1 | ind_review_staus |            1 | review_id   | A         |         210 |     NULL | NULL   |      | BTREE      |         |
| recommend_0202 |          1 | ind_review_staus |            2 | status      | A         |       210 |     NULL | NULL   | YES  | BTREE      |         |
+—————-+————+——————+————–+————-+———

可以看到ind_review_staus和index_review第一个字段都同为review_id,但是他们的Cardinality相差还是很大的,这也是为什么优化器选择了index_review 这个索引;
那我们来手工分析一把:
root@test 10:31:08>analyze table recommend_0202;
+———————+———+———-+———-+
| Table               | Op      | Msg_type | Msg_text |
+———————+———+———-+———-+
| test.recommend_0202 | analyze | status   | OK       |
+———————+———+———-+———-+

root@test 10:31:27>show  index from recommend_0202;
+—————-+————+——————+————–+————-+———
| Table          | Non_unique | Key_name         | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+—————-+————+——————+————–+————-+———
| recommend_0202 |          0 | PRIMARY          |            1 | id          | A         |     2219712 |     NULL | NULL   |      | BTREE      |         |
| recommend_0202 |          1 | ind_review_staus |            1 | review_id   | A     |   2219712 |     NULL | NULL   |      | BTREE      |         |
| recommend_0202 |          1 | ind_review_staus |            2 | status      | A        |     2219712 |     NULL | NULL   | YES  | BTREE      |         |
| recommend_0202 |          1 | index_reivew     |            1 | review_id   | A         |     2219712 |     NULL | NULL   |      | BTREE      |         |
+—————-+————+——————+————–+————-+———
可以看到analyze 后,索引ind_review_staus的统计信息已经恢复了正常;那么在来看看执行计划:
root@test 10:31:35>explain select status from recommend_0202  where review_id=’00000015bf5445a88462ba047aab114c’ and status=1;
+—-+————-+—————-+——+——————————-+————
| id | select_type | table          | type | possible_keys                 | key              | key_len | ref         | rows | Extra                    |
+—-+————-+—————-+——+——————————-+————
|  1 | SIMPLE      | recommend_0202 | ref  | ind_review_staus,index_reivew | ind_review_staus | 68      | const,const |    1 | Using where; Using index |
+—-+————-+—————-+——+——————————-+————

可以看到优化器已经能够正确选择索引了;


有时候我们在判断优化器是否选择了正确的执行计划的时候,有一个状态变量值,他可以给我们一些信息,就是每个执行计划的cost,
mysql生成的每个执行计划都有一个cost,和其他很多关系数据库一样,同样以成本最低的执行计划来运行实际的查询:

第一个查询强制走:  KEY `ind_user_gmt` (`user_id`,`gmt_create`)
root@test 10:52:19>explain select target_id from recommend_0202 force index(ind_user_gmt) where user_id=44312518  order by gmt_Create limit 10;
+—-+————-+—————-+——+—————+————–+———+—
| id | select_type | table          | type | possible_keys | key          | key_len | ref   | rows | Extra       |
+—-+————-+—————-+——+—————+————–+———+—
|  1 | SIMPLE      | recommend_0202 | ref  | ind_user_gmt  | ind_user_gmt | 9       | const | 6130 | Using where |
+—-+————-+—————-+——+—————+————–+———+—
1 row in set (0.00 sec)

root@test 10:53:09>show status like ‘%last_query_cost%’;
+—————–+————-+
| Variable_name   | Value       |
+—————–+————-+
| Last_query_cost | 7355.999000 |
+—————–+————-+
1 row in set (0.00 sec)

第二个查询,优化器选择:  KEY `ind_user_id` (`user_id`,`status`,`gmt_create`,`target_id`),
root@test 10:53:20>explain select target_id from recommend_0202 where user_id=44312518  order by gmt_Create limit 10 ;
+—-+————-+—————-+——+————————–+————-+—
| id | select_type | table          | type | possible_keys            | key         | key_len | ref   | rows | Extra                                    |
+—-+————-+—————-+——+————————–+————-+—
|  1 | SIMPLE      | recommend_0202 | ref  | ind_user_id,ind_user_gmt | ind_user_id | 9       | const | 5896 | Using where; Using index; Using filesort |
+—-+————-+—————-+——+————————–+————-+—
1 row in set (0.00 sec)

root@test 10:53:49>show status like ‘%last_query_cost%’;
+—————–+————-+
| Variable_name   | Value       |
+—————–+————-+
| Last_query_cost | 1243.586097 |
+—————–+————-+
1 row in set (0.00 sec)
可以看到优化器优化器对比了KEY `ind_user_id`,KEY `ind_user_gmt`,选择cost最低KEY `ind_user_id`来执行;
但是有时候这个cost并不是十分有参考意义:
root@test 10:54:51>explain select target_id from recommend_0202 force index(ind_user_id) where user_id=0  order by gmt_Create limit 10;
+—-+————-+—————-+——+—————+————-+———+—-
| id | select_type | table          | type | possible_keys | key         | key_len | ref   | rows    | Extra                                    |
+—-+————-+—————-+——+—————+————-+———+—-
|  1 | SIMPLE      | recommend_0202 | ref  | ind_user_id   | ind_user_id | 9       | const | 1158435 | Using where; Using index; Using filesort |
+—-+————-+—————-+——+—————+————-+———+—-
1 row in set (0.00 sec)

root@test 10:55:42>show status like ‘%last_query_cost%’;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 244144.278570 |
+—————–+—————+
1 row in set (0.00 sec)



root@test 10:57:25>explain select target_id from recommend_0202  where user_id=0  order by gmt_Create limit 10;
+—-+————-+—————-+——-+————————–+————–+-
| id | select_type | table          | type  | possible_keys            | key          | key_len | ref  | rows | Extra       |
+—-+————-+—————-+——-+————————–+————–+-
|  1 | SIMPLE      | recommend_0202 | index | ind_user_id,ind_user_gmt | ind_user_gmt | 17      | NULL |   20 | Using where |
+—-+————-+—————-+——-+————————–+————–+———+——+——+————-+
1 row in set (0.01 sec)

root@test 10:57:27>show status like ‘%last_query_cost%’;
+—————–+—————+
| Variable_name   | Value         |
+—————–+—————+
| Last_query_cost | 244144.278570 |
+—————–+—————+
1 row in set (0.00 sec)
两个执行计划的cost是一样的,这个时候还是需要有经验的DBA来参与调整;

 

 

Posted in database | Leave a comment

主备不一致:Table definition on master and slave does not match

昨天一同事在线上做变更,为了保证主库的稳定性,先在备库把binlog关闭,然后在进行DDL变更,在通过切换HA,把备库切换为主库,在老的主库上做DDL变更
看上去这样做法没有太大的问题,但是当备库变更一做完,HA切换到备库,开始老主库变更的时候,备库就出现复制出现错误:
Last_Error: Table definition on master and slave does not match: Column 10 type mismatch – received type 3, dbname.table_name has type 8
Skip_Counter: 0
Exec_Master_Log_Pos: 1046252634
Relay_Log_Space: 2910773181
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 1535
Last_SQL_Error: Table definition on master and slave does not match: Column 10 type mismatch – received type 3, dbname.table_name has type 8
1 row in set (0.00 sec)
<1>从这个错误上来看,是主备的表结构不一致导致的,但是之前的复制都是好好的,为什么做完变更后就会出现这个问题,应该是在DDL变更后导致的问题;
master:
mysql -uroot dbname -e “show create table table_name”>master.sql
slave:
mysql -uroot dbname -e “show create table table_name”>slave.sql

diff -u master.sql slave.sql没有找到两个表结构有什么大问题;
<2>查看出问题的数据字段:
root@127.0.0.1 : dbname 17:46:35> desc table_name;
+—————-+———————+——+—–+——————-+—————————–+
| Field | Type | Null | Key | Default | Extra |
+—————-+———————+——+—–+——————-+—————————–+
| url | varchar(333) | NO | UNI | NULL | |
| Description | varchar(255) | YES | | NULL | |
| HttpStatus | int(11) | YES | | NULL | |
| AddTime | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| ServerBanner | varchar(255) | YES | | NULL | |
| TaskTag | varchar(255) | NO | MUL | NULL | |
| TaskTag2 | varchar(255) | YES | | NULL | |
| DomainName | varchar(255) | NO | MUL | NULL | |
| R_DomainName | varchar(255) | YES | MUL | NULL | |
| ScanTaskID | int(11) | YES | | NULL | |
| SubTaskID | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| urlhash | varchar(32) | YES | UNI | | |
| duplicateHash | varchar(32) | YES | | | |
| enable | tinyint(1) | YES | | 0 | |
| webappid | int(11) | YES | | NULL | |
| crc_DomainName | int(10) unsigned | YES | MUL | NULL | |
| wapscore | int(11) | YES | | 0 | |
| ip | varchar(45) | YES | | NULL | |
+—————-+———————+——+—–+——————-+—————————–+
slave:
root@127.0.0.1 : information_schema 14:59:40> select * from columns where table_schema=”dbname” and table_name=”table_name” and ORDINAL_POSITION= 10\G;
*************************** 1. row ***************************
TABLE_CATALOG: NULL
TABLE_SCHEMA: dbname
TABLE_NAME: table_name
COLUMN_NAME: ScanTaskID
ORDINAL_POSITION: 10
COLUMN_DEFAULT: NULL
IS_NULLABLE: YES
DATA_TYPE: int
CHARACTER_MAXIMUM_LENGTH: NULL
CHARACTER_OCTET_LENGTH: NULL
NUMERIC_PRECISION: 10
NUMERIC_SCALE: 0
CHARACTER_SET_NAME: NULL
COLLATION_NAME: NULL
COLUMN_TYPE: int(11)
COLUMN_KEY:
EXTRA:
PRIVILEGES: select,insert,update,references
COLUMN_COMMENT:
1 row in set (0.00 sec)
master:
root@127.0.0.1 : information_schema 14:59:19> select * from information_schema.columns where table_schema=”dbname” and table_name=”table_name” and ORDINAL_POSITION= 10\G;
*************************** 1. row ***************************
TABLE_CATALOG: NULL
TABLE_SCHEMA: dbname
TABLE_NAME: table_name
COLUMN_NAME: ScanTaskID
ORDINAL_POSITION: 10
COLUMN_DEFAULT: NULL
IS_NULLABLE: YES
DATA_TYPE: int
CHARACTER_MAXIMUM_LENGTH: NULL
CHARACTER_OCTET_LENGTH: NULL
NUMERIC_PRECISION: 10
NUMERIC_SCALE: 0
CHARACTER_SET_NAME: NULL
COLLATION_NAME: NULL
COLUMN_TYPE: int(11)
COLUMN_KEY:
EXTRA:
PRIVILEGES: select,insert,update,references
COLUMN_COMMENT:
1 row in set (0.00 sec)
查看数据字段,主备库还是一致的,这个时候好像到了穷途;
<3>查看同事昨天的DDL变更脚本,涉及到加字段,调整字段的长度,看上去很平常,
我们是先在备库做的变更,然后在到主库的变更,期间的binlog是关闭的,这时候,印风同学想到如果在备库变更的时候,主库的业务是没有停止的,
如果主库变更的数据同步到备库,备库的变更做完,主备已经不一致了,这样的话,就会造成复制失败了,看了看脚本中有字段长度调长的,这下就迎刃而解了;
问了一下B2B的plinux,他们只有加字段的时候,才放到备库上去做,其他的还是在主库上直接做的;
<4>刚才看到从 information_schema.columns 中查询有问题的列的时候,直接代入ORDINAL_POSITION= 10得到的是ScanTaskID
字段,但出问题的字段是第11为字段(即我们调整长度的字段),所以binlog中是从0开始计算字段的位置的;
<5>5.5中报错显得更加人性了:
Column 0 of table ‘test.t3′ cannot be converted from type ‘int’ to type ‘bigint(20)’;
<6>.那如何避免这样的问题喃,由于我们的库采用的是row模式,只要把slave的复制改为statement就可以了,将主库的binlog_format由row改为statement,这样达到备库的binlog就不会出现错误;

 

 

Posted in database | Leave a comment

内建的builtin InnoDB,子查询阻塞更新

奇龙在做db变更的时候,update被阻塞:

root@127.0.0.1 : (none) 23:51:35> show variables like ‘%ignore_builtin%’;

| Variable_name | Value |

| ignore_builtin_innodb | OFF |

—>查看show processlist:

| 5696061853 | houyi | 10.242.219.1:42661 | dbname | Query | 22936 | Sending data | select * from xx_shot where id in (select parent_xx_shot from xx_shot where parent_xx_shot!=”" a |

|5696162576 | houyi | 10.242.219.1:44290 | dbname | Query | 22695 | Sending data | select id from xx_shot where id in (select parent_xx_shot from xx_shot where parent_xx_shot!=”" |

| 5696208999 | houyi | 10.242.219.1:45610 | dbname | Query | 22586 | Sending data | select id from xx_shot where id in (select parent_xx_shot from xx_shot where parent_xx_shot

—>查看show innodb status\G;

mysql tables in use 2, locked 14562 lock struct(s), heap size 489456, 604071 row lock(s)MySQL thread id 5696208999, query id 15226780028 10.242.219.1 houyi Sending dataselect id from xx_shot where id in (select parent_xx_shot from xx_shot where parent_xx_shot!=”" and gmt_create>’2012-04-27′ and xx_shot_progress<0 )Trx read view will not see trx with id >= 1 2411988144, sees < 1 2411827879—TRANSACTION 1 2411935441, ACTIVE 22560 sec, process no 1982, OS thread id 1188837696 fetching rows, thread declared inside InnoDB 228mysql tables in use 2, locked 15311 lock struct(s), heap size 636912, 1199769 row lock(s)

这个表确实被锁住了

—>kill 子查询

kill 5696061853 ;

kil 5696162576;

kil 5696208999;

root@127.0.0.1 : dbname 23:41:40> update xx_shot set xx_shot_progress=-6 where id=2083430;

Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: root@127.0.0.1 : dbname 23:41:42> commit;

Query OK, 0 rows affected (0.00 sec)
测试5.1 innodb db pluginroot@test 02:51:27>show global variables like ‘%igno%’; Variable_name | Value |

ignore_builtin_innodb | ON |

root@test 02:51:53>show global variables like ‘%version%’;

| Variable_name | Value |

| innodb_version | 1.0.9 |

session1:

root@test 02:46:31>select id from xx_shot where id in (select parent_xx_shot   from xx_shot where parent_xx_shot != “” and gmt_create > ’2012-04-27′  and xx_shot_progress < 0 group by device_id HAVING count(device_id) = 1);

session2:

root@test 02:47:26>update xx_shot set xx_shot_progress=-6 where id=1163184;Query OK, 0 rows affected (0.00 sec)

关闭掉bultin,innodb_version为1.0.9中子查询并没有阻塞更新;
造成上面问题的还有一个主要原因为mysql处理子查询的不优秀:http://hidba.org/?p=412

root@test 03:21:06>explain select id  from xx_shot c1, (select parent_xx_shot-> from xx_shot where parent_xx_shot != “” and gmt_create  ’2012-04-27′  and xx_shot_progress < 0  group by device_id  HAVING count(device_id) = 1) c2  where c1.id = c2.parent_xx_shot  ;

| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE noticed after reading const tables |

| 2 | DERIVED | xx_shot | ALL | NULL | NULL | NULL | NULL | 170786 | Using where; Using temporary; Using filesort |

改写sql后:root@test 03:21:06>explain select id  from xx_shot c1,  (select parent_xx_shot  from xx_shot where parent_xx_shot != “”  and gmt_create > ’2012-04-27′ and xx_shot_progress < 0 group by device_id HAVING count(device_id) = 1) c2 where c1.id = c2.parent_xx_shot ;

| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE noticed after reading const tables |

| 2 | DERIVED | xx_shot | ALL | NULL | NULL | NULL | NULL | 170786 | Using where; Using temporary; Using filesort |

执行时间:root@test 03:21:05>select id  from xx_shot c1,  (select parent_xx_shot  from xx_shot where parent_xx_shot != “”  and gmt_create > ’2012-04-27′ and xx_shot_progress < 0 group by device_id HAVING count(device_id) = 1) c2 where c1.id = c2.parent_xx_shot ;

Empty set (0.17 sec)

Posted in database, mysql | Leave a comment

一则优化案例

昨晚收到客服MM电话,一用户反馈数据库响应非常慢,手机收到load异常报警,登上主机后发现大量sql执行非常慢,有的执行时间超过了10s
优化点一:
SELECT * FROM `sitevipdb`.`game_shares_buy_list` WHERE price>=’2.00′ ORDER BY tran_id DESC LIMIT 10;
表结构为:
CREATE TABLE `game_shares_buy_list` (
`tran_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`………..’
PRIMARY KEY (`tran_id`),
KEY `ind_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3144200 DEFAULT CHARSET=utf8;
执行计划:
root@127.0.0.1 : sitevipdb 09:10:22> explain SELECT * FROM `sitevipdb`.`game_shares_buy_list` WHERE price>=’2.00′ ORDER BY tran_id DESC LIMIT 10;
+—-+————-+———————-+——-+—————+———+———+——+——+————-+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+—-+————-+———————-+——-+—————+———+———+——+——+————-+
| 1 | SIMPLE | game_shares_buy_list | index | NULL | PRIMARY | 4 | NULL | 10 | Using where |
+—-+————-+———————-+——-+—————+———+———+——+——+————-+
1 row in set (0.00 sec)
分析该sql的执行计划,由于tran_id是表的主键,所以查询根据主键降序顺序扫描,这样就可以不用排序,
然后在过滤条件price>2.00的记录,看上去这个执行计划貌似非常好,如果查询扫描到了满足条件的10条记录,就会停止扫描;
但是这里有个问题,如果表中有大量的记录是不符合2.00的,意味查询就需要扫描非常多的记录,才能找到符合条件的10条:
root@127.0.0.1 : sitevipdb 09:17:23> select price,count(*) as cnt from `game_shares_buy_list` group by price order by cnt desc limit 10;
+——-+——-+
| price | cnt |
+——-+——-+
| 1.75 | 39101 |
| 1.68 | 38477 |
| 1.71 | 34869 |
| 1.66 | 34849 |
| 1.72 | 34718 |
| 1.70 | 33996 |
| 1.76 | 32527 |
| 1.69 | 27189 |
| 1.61 | 25694 |
| 1.25 | 25450 |
可以看到表中有大量的记录不是2.00的,所以这个时候不能在根据主键顺序扫描,在过滤记录;
那么是否需要在price建立一个索引:
root@127.0.0.1 : sitevipdb 09:09:01> select count(*) from `game_shares_buy_list` where price>’2′;
+———-+
| count(*) |
+———-+
| 4087 |
+———-+
root@127.0.0.1 : sitevipdb 09:17:31> select count(*) from `game_shares_buy_list` ;
+———-+
| count(*) |
+———-+
| 1572100 |
从上面price的数据分布可以看出,price的分布相对还是比较集中的,如果在price建立索引,mysql也有可能认为由于需要回表的记录过多,
同时需要额外的排序,而不选择在price上的索引:
root@127.0.0.1 : sitevipdb 09:24:53> alter table game_shares_buy_list add index ind_game_shares_buy_list_price(price);
Query OK, 0 rows affected (5.79 sec)

可以看到优化器虽然注意到了我们新加的索引,但是最终还是选择了primary来扫描;
所以这个时候我们加上去的索引没有产生效果,数据库负载依然很高,如果强制走price上的索引,效果会这样:
root@127.0.0.1 : sitevipdb 09:35:38> SELECT * FROM `sitevipdb`.`game_shares_buy_list` WHERE price>=’2.0′ ORDER BY tran_id DESC LIMIT 10;
。。。。。
10 rows in set (7.06 sec)

root@127.0.0.1 : sitevipdb 09:36:00> SELECT * FROM `sitevipdb`.`game_shares_buy_list` force index(ind_game_shares_buy_list_price) WHERE price>=’2.0′ ORDER BY tran_id DESC LIMIT 10;
。。。。
10 rows in set (1.01 sec)
可以看到如果强制走索引,时间已经明显下降了,但是还是有些慢,能不能在快一点?其实我们需要扫描的记录只有10条,但查询在取得这10条记录的时候需要扫描大量无效的记录

怎么降低这个数据:其实只要改写一下sql就可以,我们先从索引中得到满足条件的10个id,在回表进行关联:
root@127.0.0.1 : sitevipdb 09:44:45> select * from game_shares_buy_list t1,
-> ( SELECT tran_id FROM sitevipdb.game_shares_buy_list WHERE price>=’2.0′ ORDER BY tran_id DESC LIMIT 10) t2
-> where t1.tran_id=t2.tran_id;
10 rows in set (0.00 sec)
可以看到执行时间已经不在秒级别了,和客户电话沟通后,很愿意这样改写sql。

—这里看到是order by tran_id是要额外排序的,索引也可以这样来建立消除排序(tran_id,price)这样可以消除排序,同时可以利用order by desc/asc +limit M,N的优化。

优化点二:
CREATE TABLE `game_session` (
`session_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT ,
`session_expires` int(10) unsigned NOT NULL DEFAULT ’0′,
`client_ip` varchar(16) DEFAULT NULL,
`session_data` text,
…………………….
PRIMARY KEY (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

查询为select `session_data`, `session_expires` from `game_session` where session_id=’xxx’出现大量等待情况
同时该表的insert,也有等待的现象;
可以看到这个表结构设计是有些问题的,咨询了客户后,可以改为下面结构:
CREATE TABLE `game_session` (
id int auto_increment,
`session_id` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT ,
`session_expires` int(10) unsigned NOT NULL DEFAULT ’0′,
`client_ip` varchar(16) DEFAULT NULL,
`session_data` varchar(200),
PRIMARY KEY (id),
key ind_session_id(session_id,session_data, session_expires)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-新增自增主键id作为表的主键,这样对插入的性能提升是很好的,同时也降低了表主键的大小;
-将session_data由text改为了varchar(200),咨询了客户后,这个字段可以不用大字段存储,同时有text改为了varchar,就可以冗余到索引中;
由于查询可以使用覆盖索引来完成,所以将查询的3个字段冗余到索引中,查询通过索引完成,不用回表;

 

 

Posted in database | Leave a comment

4月杂记–2012数据库技术大会

丁总给了我一张2012数据库技术大会的门票,让我和他们一块去北京,记得上次去北京的时候,2天都在酒店里呆着,说是中国人,北京都没有逛逛,于是乎想着这次去北京的3天,一定要去外面看看,更重要的事情是要拜访道德经艺术馆和韩金英老师;

我们团队去的人有苏普,应元和我,苏普是这次演讲的嘉宾,我和应元就得自己定酒店了,公司给每个人出差的酒店标准还是比较高的,但我们还是定了比较便宜的如家。到了北京后,就直接打车到了新疆驻京办吃羊肉,听起来倒是挺拉风,到了那里后海吃了一大桌羊肉。第二天早上一大早,我们就感到了会场,看到了很多久负盛名的DBA前辈,想起在大学的时候,就是看他们的博客,帖子,然后转到DBA这个领域的;

丁总和苏普这次分享的主题是在年初的技术大学的mysql@taobao系列课程中的内容,每次听他们的分享都有不同的体会,丁总的–《在淘宝五年》是淘宝数据库发展的见证着,也最具有发言权,丁总做事情一直站在很高的高度去做,因此看的东西就比较深入;

 

 

苏普—-《mysql核心库运维》最大的感受是这伙计是用心做事情,就是不一样;

晚上和应元去了一趟天安门,到王府井买北京烤鸭,还带来一些北京的特产回去忽悠同事,哈哈,真是两个土人;

这次北京之行,还有一个很重要的事情就是去拜访韩金英老师,第二天一大早,我就查好了路线,从早上9点出发,坐公交,坐地铁,在乘到通州的公交车,中午12点30才到镇上,在小镇上吃了一碗面后,就开始找道德经艺术馆,那天太阳异常的大,走在街上,心里总是默默的念着难得到北京一回,一定要找到,最后在镇上绕到了大半圈,在15:30才最终找到馆址。开门的是韩老师,当时还是很激动的,以前只在博客上看到过她,进屋后已经有两位今天同样来馆参观的友人,他们正在那里喝着茶,茶是来自台湾友人带来的,韩老师招呼我坐下,我就静静的坐在那里听他们喝茶论道,屋里屋外显得非常非常的安静;一些道理虽然在书上看到过,但是听人讲起来真的不一般—德者本也,财者末也;有德此有土,有土此有财。和台湾友人喝茶论道的是灵珠子(后来才从韩老师博客上得知),也是他给我指来的路,从他的言语中常常能够吐出一些妙语来,在聊起他和韩老师的缘分的时候,讲到他是一个隐士修行之人,有很高的修为,在艺术馆只是帮韩老师打理日常的事情。

临走的时候,一同到来的两位友人觉得我们三人很有缘分,于是乎一定要拍照做纪念,哈哈,感觉今天真的很奇妙,还以为今天会白跑一趟,见到了韩老师,灵珠子,两位友人,不枉此行啊。

 

Posted in database | Leave a comment

4月杂记–清明节

4月马上就要完了,很久都没有更新博客了,是自己太懒了吧,回忆一下这过去的4周,好像一直没有停下来过,先从第一周开始说起吧。

第一周是清明节,省去了外地旅游的机会,跑到了朋友的书院去图个清闲,找到了一个在杭州幽远宁静的地方,这个书院靠山临水,四周种满了茶叶,由于去的时候正值采摘清明茶,正好露一手:

记得小时候可是靠这个来补贴每周的生活费的,自家山林里种植了一亩左右的茶,每周末一早上起来就提着口袋去采,采完回来后就立刻拿到茶厂去买,多的时候可以卖到四五十元,然后一路哼着小曲回家吃中午饭,那个真的是高兴啊;有时候为了卖一个好一些的价钱,要走很长的路,到很远的茶厂去卖,真的是天真无比;如果自家的茶采完了的话,还可以和大人们一起到山上的大茶园去采茶,天一亮就要起来准备好上山,那个时候想睡懒觉都不行,现在想想那个时候生活在农村的孩子,必须要靠自己的双手来获得生存,从小就建立自力更生的习惯,而现在的孩子又是怎么一个现状…..书院面就有一大片的茶园,看到地里有很多采茶的人,我一下就跑到茶园里和她们一起采起茶来,和他们的聊天中,居然发现还有四川的老乡,真是天涯无处不相逢。

朋友以前在书院教书,这次从北京回来在书院住了几天,听他说过完清明节,他将要骑自行车南下到广东,帐篷和车都已经买好了,

真的很佩服他的决心和力量,当一个人下定心要做一件事情的时候,这个力量是非常之大的。我和另外一个朋友折腾了很久才把它给搭建好,还担心他一个人在外地能不能顺利装上,如果遇到下雨天,他就很倒霉了。

这所书院的学生主要还是在3-5岁的小孩子,周一到周五都在书院里吃住,只有周末,父母在把他们接回家中,听朋友说,很多小孩子一开始都很不习惯,但是后来慢慢都习惯了这里的生活环境,个个都能够独立自己的生活,有一次一个小女孩想她的妈妈,在那里一直哭,带她们上课的老师对她说,如果还要哭的话,到另外一边自己拿着纸巾在哭,这么一说她就默默的到另外一边自己慢慢的哭,不过一会儿,居然不哭了,又回到了老师的身边。

朋友给我推荐了一本书《与神对话》,我一看目录吸引住了我,目录的内容就像是为我心中一直以来的疑问解答而精心编排的…..

Posted in database | Leave a comment

mysql中的Waiting for tables

接着上篇中遇到的mysql子查询,在问题的诊断中,丹臣注意到一个较为严重的问题,就是我们生产库中全部的数据库访问请求都处于Waiting for tables的状态,在将大查询kill掉后,所有的请求恢复正常;简单的理解为大查询阻塞了其他访问请求,但是这个理论是不可信,如果阻塞该表的DML还可以理解,但是把该数据库上的所有请求都阻塞了,这还是说不通的。那么我们就来看看所有的请求处于Waiting for tables这个状态是什么原因导致的:

The thread got a notification that the underlying structure for a table has changed and it needs to reopen the table to get the new structure. However, to reopen the table, it must wait until all other threads have closed the table in question.

This notification takes place if another thread has used FLUSH TABLES or one of the following statements on the table in question: FLUSH TABLES tbl_nameALTER TABLERENAME TABLEREPAIR TABLEANALYZE TABLE, orOPTIMIZE TABLE.

从文档上的解释来看,是主库做了一个flush tables的操作,导致所有的表都需要打开,但是由于在10-07号放假,应该不会有人在主库上执行flush tables,而且从日志中可以看到:

1044 system user Connect 27406 Flushing tables FLUSH TABLES

所以可以判断是系统自己执行了这个操作,那么既然不是主库上执行而来,那么这个flush tables操作是从slave上复制过来的(M-M结构),

那么备库的什么操作会有flush tables,真的百思不其解,我们备库在6点之前做的是什么,后端应用的dump?还是数据库的备份?是不是xtrabackup,很有可能是xtrabackup在备份的时候做的fulsh tables,查看备份脚本,应该轮到mysqldump做逻辑备份操作了,并不是xtrabackup,检查了mysqldump的备份脚本,脚本里:

-uroot -P$port –protocol=tcp –single-transaction –master-data=2是这样的

Single-transaction这个选项是加上了的,希望再一次被打破 >_<

最后想还是想到到官网上去看看,mysqldump+flush tables是否有bug,

唉,搜索了一下果然发现了蹊跷:

http://bugs.mysql.com/bug.php?id=35157

When using the –master-data option with mysqldump, mysqldump uses a FLUSH TABLES command. However, this statement got replicated to the slave(s), which caused the slave(s) to block unnecessarily while the FLUSH tables command completed.

在5.0存在的bug很好的解释了这个问题,在mysqldump加入了–master-data就会将flush tables记录到binglog中,然后在被同步到主库,主库执行binglog后,由于有一个大查询正在

执行,这个子查询由于执行了很长时间,阻塞了flush tables的操作,最后导致了雪崩,所有的请求都被阻塞:

The thread got a notification that the underlying structure for a table has changed and it needs to reopen the table to get the new structure. However, to reopen the table,

it must wait until all other threads have closed the table in question.

综合上篇的所写,Mysqldump的在5.0的bug加上数据库的低效子查询构成这次故障的原因。

峰回路转,山穷水尽,哈哈

^_^

Posted in mysql | Leave a comment

生产库中遇到mysql的子查询

使用过oracle或者其他关系数据库的DBA或者开发人员都有这样的经验,在子查询上都认为数据库已经做过优化,能够很好的选择驱动表执行,然后在把该经验移植到mysql数据库上,但是不幸的是,mysql在子查询的处理上有可能会让你大失所望,在我们的生产系统上就由于碰到了这个问题:

select  i_id, sum(i_sell) as i_sell

from table_data

where i_id in (select i_id from table_data where Gmt_create >= ’2011-10-07 00:00:00′)

group by i_id;

(备注:sql的业务逻辑可以打个比方:先查询出10-07号新卖出的100本书,然后在查询这新卖出的100本书在全年的销量情况)。

这条sql之所以出现的性能问题在于mysql优化器在处理子查询的弱点,mysql优化器在处理子查询的时候,会将将子查询改写。通常情况下,我们希望由内到外,先完成子查询的结果,然后在用子查询来驱动外查询的表,完成查询;但是mysql处理为将会先扫描外面表中的所有数据,每条数据将会传到子查询中与子查询关联,如果外表很大的话,那么性能上将会出现问题;

针对上面的查询,由于table_data这张表的数据有70W的数据,同时子查询中的数据较多,有大量是重复的,这样就需要关联近70W次,大量的关联导致这条sql执行了几个小时也没有执行完成,所以我们需要改写sql:

SELECT t2.i_id, SUM(t2.i_sell) AS sold

FROM (SELECT distinct i_id FROM table_data

WHERE gmt_create >= ’2011-10-07 00:00:00′) t1,  table_data t2

WHERE t1.i_id = t2.i_id GROUP BY t2.i_id;

我们将子查询改为了关联,同时在子查询中加上distinct,减少t1关联t2的次数;

改造后,sql的执行时间降到100ms以内。

 

 

 

 

 

 

 

Posted in database | Leave a comment

mysql explain 中key_len的计算

今天丁原问我mysql执行计划中的key_len是怎么计算得到的,当时还没有注意,在高性能的那本书讲到过这个值的计算,但是自己看执行计划的时候一直都没有太在意这个值,更不用说深讨这个值的计算了:

ken_len表示索引使用的字节数,根据这个值,就可以判断索引使用情况,特别是在组合索引的时候,判断所有的索引字段都被查询用到。

在查看官方文档的时候,也没有发现详细的key_len的计算介绍,后来做了一些测试,在咨询了丁奇关于变长数据类型的值计算的时候,突然想到innodb 行的格式,在这里的计算中有点类似,总结一下需要考虑到以下一些情况:

(1).索引字段的附加信息:可以分为变长和定长数据类型讨论,当索引字段为定长数据类型,比如char,int,datetime,需要有是否为空的标记,这个标记需要占用1个字节;对于变长数据类型,比如:varchar,除了是否为空的标记外,还需要有长度信息,需要占用2个字节;

(备注:当字段定义为非空的时候,是否为空的标记将不占用字节)

(2).同时还需要考虑表所使用的字符集,不同的字符集,gbk编码的为一个字符2个字节,utf8编码的一个字符3个字节;

 

先看定长数据类型的一个例子(编码为gbk):

root@test 07:32:39>create table test_char(id int not null ,name_1 char(20),name_2 char(20),

-> primary key(id),key ind_name(name_1,name_2))engine=innodb charset=gbk;

root@test 07:33:55>insert into test_char values(1,’xuancan’,'taobaodba’);

root@test 07:34:55>explain select * from test_char where name_1=’xuancan’\G;

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: test_char

type: ref

possible_keys: ind_name

key: ind_name

key_len: 41

ref: const

rows: 1

Extra: Using where; Using index

key_len=41=20*2+1(备注:由于name_1为空,isnull的标记被打上,需要计算1个字节)

root@test 07:35:31>explain select * from test_char where name_1=’xuancan’ and name_2=’taobaodba’\G;

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: test_char

type: ref

possible_keys: ind_name

key: ind_name

key_len: 82

ref: const,const

rows: 1

Extra: Using where; Using index

key_len=82=20*2+20*2+1+1(备注:由于name_1,name_2两列被使用到,但两列都为为空,需要计算2个字节)

变长数据类型(gbk编码):

root@test 08:30:51>create table test_varchar(id int not null ,name_1 varchar(20),name_2 varchar(20),

-> primary key(id),key ind_name(name_1,name_2))engine=innodb charset=gbk;

root@test 08:37:51>insert into test_varchar values(1,’xuancan’,'taobaodba’);

root@test 08:38:14>explain select * from test_varchar where name_1=’xuancan’\G;

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: test_varchar

type: ref

possible_keys: ind_name

key: ind_name

key_len: 43

ref: const

rows: 1

Extra: Using where; Using index

key_len=43=20*2+1+2(备注:由于为name_1字段定义为空,所以需要计算1,;同时由于是变长字段varchar,所以需要加上2)

root@test 08:38:46>alter table test_varchar modify column name_1 varchar(20) not null;

Query OK, 1 row affected (0.52 sec)

Records: 1 Duplicates: 0 Warnings: 0

root@test 08:42:11>explain select * from test_varchar where name_1=’xuancan’\G;

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: test_varchar

type: ref

possible_keys: ind_name

key: ind_name

key_len: 42

ref: const

rows: 1

Extra: Using where; Using index

key_len=42=20*2+2(备注由于name_1字段修改为not null之后,isnull的标记锁占用的字节释放掉,但是变长字段长度所占用的2个字节没有释放);

上面是测试gbk编码的测试,同时也可以测试一下其他编码的key_len计算。

 

Posted in database | Leave a comment

innodb 的预读

innodb在io的优化上有个比较重要的特性为预读,innodb以64个page为一个extent,那么innodb的预读是以page为单位还是以extent?

这样就进入了下面的话题:linear read-ahead和randomread-ahead;

为了区分这两种预读的方式,我们可以把linear预读放到以extent为单位,而random 预读放到以extent中的page为单位;

linear 预读着眼于将下一个extent提前读取到buffer pool中,

而random预读着眼于将当前extent中的剩余的page提前读取到buffer pool 中:

linear的预读方式有一个很重要的变量控制是否将下一个extent预读到buffer pool中: innodb_read_ahead_threshold:如果一个extent中的被顺序读取的page超过或者等于该参数变量的,innodb将会异步的将下一个extent读取到buffer pool中,比如该参数的值为30,那么当该extent中有30个pages 被 sequentially的读取,则会触发innodb linear预读,将下一个extent读到内存中;在没有该变量之前,当访问到extent的最后一个page的时候,innodb会决定是否将下一个extent放入到buffer pool中;

该参数可以动态的修改:

root@(none) 09:20:02>set global innodb_read_ahead_threshold=40;

Query OK, 0 rows affected (0.00 sec)

random的预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,innodb会将该extent中的剩余page一并读到buffer pool中,由于random的预读方式给innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃。

在监控innodb的预读时候,我们可以通过show innodb status中的 Pages read ahead和evicted without access 两个值来观察预读的情况:

或者通过两个状态值:

Innodb_buffer_pool_read_ahead 和 Innodb_buffer_pool_read_ahead_evicted.

Innodb_buffer_pool_read_ahead:表示通过预读请求到buffer pool的pages;

Innodb_buffer_pool_read_ahead_evicted:表示由于请求到buffer pool中没有被访问,而驱逐出buffer pool的pages;

root@(none) 10:19:42>show global status like ‘%read_ahead%’;

+—————————————+———+

| Variable_name | Value |

+—————————————+———+

| Innodb_buffer_pool_read_ahead | 775378 |

| Innodb_buffer_pool_read_ahead_evicted | 1888537 |

而通过show innodb status得到的 Pages read ahead 和evicted without access 则表示每秒读入和读出的pages;

Pages read ahead 1.00/s, evicted without access 9.99/s.

ref:

Reintroducing Random Readahead in InnoDB

http://dev.mysql.com/doc/innodb/1.1/en/innodb-performance-read_ahead.html

Posted in database | Leave a comment