深入MYSQL字符数字转换的详解

1、将字符的数字转成数字,比如’0’转成0可以直接用加法来实现

例如:将pony表中的d 进行排序,可d的定义为varchar,可以这样解决

select * from pony order by (d+0)

2、在进行ifnull处理时,比如 ifnull(a/b,’0′) 这样就会导致 a/b成了字符串,因此需要把’0’改成0,即可解决此困扰

3、比较数字和varchar时,比如a=11,b=”11ddddd”;

则 select 11=”11ddddd”相等

若绝对比较可以这样:

select binary 11 =binary "11ddddd"

4、字符集转换 : CONVERT(xxx USING gb2312)

类型转换和SQL Server一样,就是类型参数有点点不同 : CAST(xxx AS 类型) , CONVERT(xxx,类型),类型必须用下列的类型:

可用的类型

二进制,同带binary前缀的效果 : BINARY

字符型,可带参数 : CHAR()

日期 : DATE

时间: TIME

日期时间型 : DATETIME

浮点数 : DECIMAL

整数 : SIGNED

无符号整数 : UNSIGNED

基于MySQL游标的具体使用详解

测试表 level ;

create table test.level (name varchar(20));

再 insert 些数据 ;

代码

初始化

drop procedure if exists useCursor //

建立 存储过程 create

CREATE PROCEDURE useCursor()

BEGIN

局部变量的定义 declare

declare tmpName varchar(20) default '' ;  
declare allName varchar(255) default '' ;  
declare cur1 CURSOR FOR SELECT name FROM test.level ;  

MySQL 游标 异常后 捕捉

并设置 循环使用 变量 tmpname 为 null 跳出循环。

declare CONTINUE HANDLER FOR SQLSTATE '02000' SET tmpname = null; 

开游标

OPEN cur1;

游标向下走一步

FETCH cur1 INTO tmpName;

循环体 这很明显 把MySQL 游标查询出的 name 都加起并用 ; 号隔开

WHILE ( tmpname is not null) DO 
set tmpName = CONCAT(tmpName ,";") ; 
set allName = CONCAT(allName ,tmpName) ; 

游标向下走一步

FETCH cur1 INTO tmpName;

结束循环体:

END WHILE;

关闭游标

CLOSE cur1;

选择数据

select allName ;

结束存储过程

END;//

调用存储过程:

call useCursor()//

运行结果:

mysql> call useCursor()//

+--------------------------------------+
| allName                              |
+--------------------------------------+
| f1;c3;c6;c5;c2;c4;c1;f1;f3;f4;f2;f5; |
+--------------------------------------+
1 row in set (0.00 sec)

loop循环游标:

DELIMITER $$  

DROP PROCEDURE IF EXITS cursor_example$$  
CREATE PROCEDURE cursor_example()  
     READS SQL DATA  
BEGIN  
     DECLARE l_employee_id INT;  
     DECLARE l_salary NUMERIC(8,2);  
     DECLARE l_department_id INT;  
     DECLARE done INT DEFAULT 0;  
     DECLARE cur1 CURSOR FOR SELECT employee_id, salary, department_id FROM employees;  
     DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;  

     OPEN cur1;  
     emp_loop: LOOP  
         FETCH cur1 INTO l_employee_id, l_salary, l_department_id;  
         IF done=1 THEN  
             LEAVE emp_loop;  
         END IF;  
     END LOOP emp_loop;  
     CLOSE cur1;  
END$$  
DELIMITER ;  

repeat循环游标:

/*创建过程*/
DELIMITER //
DROP PROCEDURE IF EXISTS test //
CREATE PROCEDURE test()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE a VARCHAR(200) DEFAULT '';
    DECLARE c VARCHAR(200) DEFAULT '';

    DECLARE mycursor CURSOR FOR SELECT  fusername FROM uchome_friend;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;

    OPEN mycursor;

    REPEAT 
        FETCH mycursor INTO a;
        IF NOT done THEN
            SET c=CONCAT(c,a);/*字符串相加*/
        END IF;

    UNTIL done END REPEAT;

    CLOSE mycursor;

    SELECT c;
END //
DELIMITER ;
/*创建过程*/
DELIMITER //
DROP PROCEDURE IF EXISTS test //
CREATE PROCEDURE test()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE a VARCHAR(200) DEFAULT '';
    DECLARE c VARCHAR(200) DEFAULT '';

    DECLARE mycursor CURSOR FOR SELECT  fusername FROM uchome_friend;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;

    OPEN mycursor;

    REPEAT 
        FETCH mycursor INTO a;
        IF NOT done THEN
            SET c=CONCAT(c,a);/*字符串相加*/
        END IF;

    UNTIL done END REPEAT;

    CLOSE mycursor;

    SELECT c;
END //
DELIMITER ;

mysql prompt的用法详解

prompt命令可以在mysql提示符中显示当前用户、数据库、时间等信息

mysql -uroot -p --prompt="\u@\h:\d \r:\m:\s>"

设置成功后:

Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 5
Server version: 5.1.60-log Source distribution
Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.
root@localhost:(none) 11:04:23>

也可以在在my.cnf配置文件里进行配置:

[mysql]  
prompt=mysql(\u@\h:\d)>  

default-character-set=utf8  

选项说明:

Option  Description
c  A counter that increments for each statement you issue
D  The full current date
d The default database
h The server host
l The current delimiter (new in 5.1.12)
m  Minutes of the current time
n  A newline character
O  The current month in three-letter format (Jan, Feb, …)
o  The current month in numeric format
P  am/pm
p The current TCP/IP port or socket file
R  The current time, in 24-hour military time (0–23)
r  The current time, standard 12-hour time (1–12)
S  Semicolon
s  Seconds of the current time
t  A tab character
U   www.jb51.net  
Your full user_name@host_name account name

u Your user name
v  The server version
w  The current day of the week in three-letter format (Mon, Tue, …)
Y  The current year, four digits
y  The current year, two digits
_  A space
   A space (a space follows the backslash)
'  Single quote
"  Double quote
\  A literal “” backslash character
x 
x, for any “x” not listed above

mysql prompt一个特别好用的命令

想必大家在命令行操作mysql的时候会十分发愁现在的操作的是那个数据库吧,至少我就纠结过,可能您会说我打一条命令不就知道了,是的这样做确实可以的。

可是今儿给大家介绍个好用的命令–prompt

[root@fsailing1 ~]# mysql -uroot -p --prompt="\u@\h:\d \r:\m:\s>"
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 378
Server version: 5.0.95 Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.
root@localhost:(none) 08:23:32>use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
root@localhost:test 08:23:44>exit;

这样做的好处就是让我们大家在操作数据库时更加方便快捷。

您也可以在在my.cnf配置文件里进行配置:

[mysql]
prompt=mysql(\u@\h:\d)>

default-character-set=utf8
[root@fsailing1 ~]# vim /etc/my.cnf
[root@fsailing1 ~]# service mysqld restart
停止 MySQL:                                               [确定]
启动 MySQL:                                               [确定]
[root@fsailing1 ~]# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 2
Server version: 5.0.95 Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.
mysql(root@localhost:(none))>use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql(root@localhost:test)>

这里也给出需要的一些个选项供大家参考。

未分类

MySQL查询优化–调整内部变量的详解

MySQL是如此的开放,所以可轻松地进一步调整其缺省设置以获得更优的性能及稳定性。需要优化的一些关键变量如下:

改变索引缓冲区长度(key_buffer)

一般,该变量控制缓冲区的长度在处理索引表(读/写操作)时使用。MySQL使用手册指出该变量可以不断增加以确保索引表的最佳性能,并推荐使用与系统内存25%的大小作为该变量的值。这是MySQL十分重要的配置变量之一,如果你对优化和提高系统性能有兴趣,可以从改变 key_buffer_size变量的值开始。

改变表长(read_buffer_size)

当一个查询不断地扫描某一个表,MySQL会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小。如果你认为连续扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。

设定打开表的数目的最大值(table_cache)

该变量控制MySQL在任何时候打开表的最大数目,由此能控制服务器响应输入请求的能力。它跟max_connections变量密切相关,增加 table_cache值可使MySQL打开更多的表,就如增加max_connections值可增加连接数一样。当收到大量不同数据库及表的请求时,可以考虑改变这一值的大小。

对缓长查询设定一个时间限制(long_query_time)

MySQL带有“慢查询日志”,它会自动地记录所有的在一个特定的时间范围内尚未结束的查询。这个日志对于跟踪那些低效率或者行为不端的查询以及寻找优化对象都非常有用。long_query_time变量控制这一最大时间限定,以秒为单位。

以上讨论并给出用于分析和优化SQL查询的三种工具的使用方法,以此提高你的应用程序性能。使用它们快乐地优化吧!

MySQL查询优化之explain的深入解析

在分析查询性能时,考虑EXPLAIN关键字同样很管用。EXPLAIN关键字一般放在SELECT查询语句的前面,用于描述MySQL如何执行查询操作、以及MySQL成功返回结果集需要执行的行数。explain 可以帮助我们分析 select 语句,让我们知道查询效率低下的原因,从而改进我们查询,让查询优化器能够更好的工作。

一、MySQL 查询优化器是如何工作的

MySQL 查询优化器有几个目标,但是其中最主要的目标是尽可能地使用索引,并且使用最严格的索引来消除尽可能多的数据行。最终目标是提交 SELECT 语句查找数据行,而不是排除数据行。优化器试图排除数据行的原因在于它排除数据行的速度越快,那么找到与条件匹配的数据行也就越快。如果能够首先进行最严格的测试,查询就可以执行地更快。

EXPLAIN 的每个输出行提供一个表的相关信息,并且每个行包括下面的列:

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

未分类

extra 中出现以下 2 项意味着 MYSQL 根本不能使用索引,效率会受到重大影响。应尽可能对此进行优化。

未分类

下面来举一个例子来说明下 explain 的用法。

先来一张表:

CREATE TABLE IF NOT EXISTS `article` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`author_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned NOT NULL,
`views` int(10) unsigned NOT NULL,
`comments` int(10) unsigned NOT NULL,
`title` varbinary(255) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`id`)
);

再插几条数据:

INSERT INTO `article`
(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES
(1, 1, 1, 1, '1', '1'),
(2, 2, 2, 2, '2', '2'),
(1, 1, 3, 3, '3', '3');

需求:

查询 category_id 为 1 且 comments 大于 1 的情况下,views 最多的 article_id。

先查查试试看:

EXPLAIN
SELECT author_id
FROM `article`
WHERE category_id = 1 AND comments > 1
ORDER BY views DESC
LIMIT 1G

看看部分输出结果:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: article
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 3
        Extra: Using where; Using filesort
1 row in set (0.00 sec)

很显然,type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。

嗯,那么最简单的解决方案就是加索引了。好,我们来试一试。查询的条件里即 where 之后共使用了 category_id,comments,views 三个字段。那么来一个联合索引是最简单的了。

ALTER TABLE `article` ADD INDEX x ( `category_id` , `comments`, `views` );

结果有了一定好转,但仍然很糟糕:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: article
         type: range
possible_keys: x
          key: x
      key_len: 8
          ref: NULL
         rows: 1
        Extra: Using where; Using filesort
1 row in set (0.00 sec)

type 变成了 range,这是可以忍受的。但是 extra 里使用 Using filesort 仍是无法接受的。但是我们已经建立了索引,为啥没用呢?这是因为按照 BTree 索引的工作原理,先排序 category_id,如果遇到相同的 category_id 则再排序 comments,如果遇到相同的 comments 则再排序 views。当 comments 字段在联合索引里处于中间位置时,因comments > 1 条件是一个范围值(所谓 range),MySQL 无法利用索引再对后面的 views 部分进行检索,即 range 类型查询字段后面的索引无效。

那么我们需要抛弃 comments,删除旧索引:

DROP INDEX x ON article;

然后建立新索引:

ALTER TABLE `article` ADD INDEX y ( `category_id` , `views` ) ;

接着再运行查询:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: article
         type: ref
possible_keys: y
          key: y
      key_len: 4
          ref: const
         rows: 1
        Extra: Using where
1 row in set (0.00 sec)

可以看到,type 变为了 ref,Extra 中的 Using filesort 也消失了,结果非常理想。

再来看一个多表查询的例子。

首先定义 3个表 class 和 room。

CREATE TABLE IF NOT EXISTS `class` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `book` (
`bookid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`bookid`)
);
CREATE TABLE IF NOT EXISTS `phone` (
`phoneid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`card` int(10) unsigned NOT NULL,
PRIMARY KEY (`phoneid`)
) engine = innodb;

然后再分别插入大量测试数据。插入数据的php脚本:

<?php
$link = mysql_connect("localhost","root","870516");
mysql_select_db("test",$link);
for($i=0;$i<10000;$i++)
{
    $j   = rand(1,20);
    $sql = " insert into class(card) values({$j})";
    mysql_query($sql);
}
for($i=0;$i<10000;$i++)
{
    $j   = rand(1,20);
    $sql = " insert into book(card) values({$j})";
    mysql_query($sql);
}
for($i=0;$i<10000;$i++)
{
    $j   = rand(1,20);
    $sql = " insert into phone(card) values({$j})";
    mysql_query($sql);
}
mysql_query("COMMIT");
?>

然后来看一个左连接查询:

explain select * from class left join book on class.card = book.cardG

分析结果是:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
2 rows in set (0.00 sec)

显然第二个 ALL 是需要我们进行优化的。

建立个索引试试看:

ALTER TABLE `book` ADD INDEX y ( `card`);
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ref
possible_keys: y
          key: y
      key_len: 4
          ref: test.class.card
         rows: 1000
        Extra: 
2 rows in set (0.00 sec)

可以看到第二行的 type 变为了 ref,rows 也变成了 1741*18,优化比较明显。这是由左连接特性决定的。LEFT JOIN 条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引。

删除旧索引:

DROP INDEX y ON book;

建立新索引。

ALTER TABLE `class` ADD INDEX x ( `card`);

结果

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
2 rows in set (0.00 sec)

基本无变化。

然后来看一个右连接查询:

explain select * from class right join book on class.card = book.card;

分析结果是:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ref
possible_keys: x
          key: x
      key_len: 4
          ref: test.book.card
         rows: 1000
        Extra: 
2 rows in set (0.00 sec)

优化较明显。这是因为 RIGHT JOIN 条件用于确定如何从左表搜索行,右边一定都有,所以左边是我们的关键点,一定需要建立索引。

删除旧索引:

DROP INDEX x ON class;

建立新索引。

ALTER TABLE `book` ADD INDEX y ( `card`);

结果

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
2 rows in set (0.00 sec)

基本无变化。

最后来看看 inner join 的情况:

explain select * from class inner join book on class.card = book.card;

结果:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ref
possible_keys: x
          key: x
      key_len: 4
          ref: test.book.card
         rows: 1000
        Extra: 
2 rows in set (0.00 sec)

删除旧索引:

DROP INDEX y ON book;

结果

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
2 rows in set (0.00 sec)

建立新索引。

ALTER TABLE `class` ADD INDEX x ( `card`);

结果

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
2 rows in set (0.00 sec)

综上所述,inner join 和 left join 差不多,都需要优化右表。而 right join 需要优化左表。

我们再来看看三表查询的例子

添加一个新索引:

ALTER TABLE `phone` ADD INDEX z ( `card`);
ALTER TABLE `book` ADD INDEX y ( `card`);
explain select * from class left join book on class.card=book.card left join phone on book.card = phone.card;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: class
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20000
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: book
         type: ref
possible_keys: y
          key: y
      key_len: 4
          ref: test.class.card
         rows: 1000
        Extra: 
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: phone
         type: ref
possible_keys: z
          key: z
      key_len: 4
          ref: test.book.card
         rows: 260
        Extra: Using index
3 rows in set (0.00 sec)

后 2 行的 type 都是 ref 且总 rows 优化很好,效果不错。

MySql 中的 explain 语法可以帮助我们改写查询,优化表的结构和索引的设置,从而最大地提高查询效率。当然,在大规模数据量时,索引的建立和维护的代价也是很高的,往往需要较长的时间和较大的空间,如果在不同的列组合上建立索引,空间的开销会更大。因此索引最好设置在需要经常查询的字段中。

mysql 时间戳与日期格式的相互转换

1、UNIX时间戳转换为日期用函数: FROM_UNIXTIME()

select FROM_UNIXTIME(1156219870);

输出:2006-08-22 12:11:10

2、日期转换为UNIX时间戳用函数: UNIX_TIMESTAMP()

Select UNIX_TIMESTAMP(‘2006-11-04 12:23:00’);

输出:1162614180

Select UNIX_TIMESTAMP(NOW());

输出当前时间戳

例:mysql查询当天的记录数:

$sql=”select * from message Where DATE_FORMAT(FROM_UNIXTIME(chattime),’%Y-%m-%d’) = DATE_FORMAT(NOW(),’%Y-%m-%d’) order by id desc”;

PHP方式转换:

UNIX时间戳转换为日期用函数: date()

date(‘Y-m-d H:i:s’, 1156219870);

日期转换为UNIX时间戳用函数:strtotime()

strtotime(‘2010-03-24 08:15:42’);

MySQL 数据库优化之–开启慢查询日志

【摘要】如何找到执行效率比较低的 SQL 语句是对于开发人员来说能够及时的将该条 SQL 进行修改,能够有效的对 MySQL 数据库进行优化。

可以通过以下方式来定位执行效率低的 SQL 语句:

可以通过慢查询日志定位那些执行效率较低的 SQL,用mysqld 写一个包含所有执行时间超过 long_query_time 秒的 SQL 日志文件。依次我们可以了解哪些 SQL 语句查询效率低。

windows 下具体的设置方法如下:

1、打开 MySQL 的配置文件 my.ini 文件,然后找到 [mysqld] 标签;

2、在 [mysqld] 标签下面加上以下内容:

# 开启慢查询日志
slow_query_log = true
# 慢查询日志保存路径及文件
slow_query_log_file = "H:/wamp/bin/mysql/mysql5.6.17/slow.log"
# 超过 3s 的查询
long_query_time = 3

未分类

3、保存并关闭,然后重启 MySQL 即可。接下来我们可以使用 select sleep(10); 来模拟慢查询状态,然后查看记录:

select sleep(10);

未分类

然后我们可以打开该慢查询日志 slow.log:

未分类

这样就将查询时间超过 3s 的 SQL 语句记录到日志文件中了。

MySQL索引选择性

什么是索引选择性?

既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。

第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。

另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:

Index Selectivity = Cardinality / #T

显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。

什么是Cardinality(基数)?

索引可以用来提升数据的查询速度,但是并不是在所有的查询条中出现的列都需要添加索引,对于什么时候添加B+树索引,一般的经验是,在访问表中很少一部分数据时使用B+数索引才有意义。对于性别字段、地区字段、类型字段,它们可取值的范围很小,称为低选择性。如:

select * from student where sex=’M’

按性别进行查询时,可取值的范围一般只有’M’,’F’。因此上述SQL语句得到的结果可能是该表50%的数据,这时添加B+树索引是完全没有必要的。相反,如果某个字段的取值范围很广,几乎没有重复,即属于高选择性,则此时使用B+树索引是最合适的。例如,对于姓名字段,基本上在一个应用中不允许重名的出现。

索引的选择性是指索引列唯一值的数目与表中记录数的比例,如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。一个索引的选择性越接近于1,这个索引的效率就越高。如果是使用基于cost的最优化,优化器不应该使用选择性不好的索引。如果是使用基于rule的最优化,优化器在确定执行路径时不会考虑索引的选择性,当然除非是唯一性索引(唯一性索引属于高选择性),并且不得不手工优化查询以避免使用非选择性的索引。

怎么查看索引是否属于高选择性呢?可以通过SHOW INDEX结果中的列Cardinality来观察。Cardinality值非常关键,表示索引中不重复记录数量的预估值。同时需要注意的是,Cardinality是一个预估值。同时需要注意的是,Cardinality是一个预估值,而不是一个准确值,基本上用户也不可能得到一个准确的值。在实际应用中,Cardinality/n_rows_in_table应尽可能地接近1。如果非常小,那么用户需要考虑是否还有必要创建这个索引。故在访问高选择性属性的字段并从表中取出很少一部分数据时,对这个字段添加B+树索引是非常有必要的。使用执行计划(explain)可以用来查看这个SQL语句需要查询的行数。

Innodb存储引擎是如何统计Cardinality(基数)?

我们知道了Cardinality表示选择性,建立索引的前提是列中的数据是高选择性的,这对数据库来说才具有实际意义。然而数据库是怎样来统计Cardinality信息的呢?因为MySQL数据库中有各种不同的存储引擎,而每种存储引擎对于B+树索引的实现又各不相同,所以对Cardinality的统计是放在存储引擎层进行的。

此外需要考虑到的是,在生产环境中,索引的更新操作可能是非常频繁的。如果每次索引在发生操作时就对其进行Cardinality的统计,那么将会给数据库带来很大的负担。另外需要考虑的是,如果一张表的数据非常大,如一张表有50G的数据,那么统计一次Cardinality信息所需要的时间可能非常长。这在生产环境下,也是不能接受的。因此,数据库对于Cardinality的统计都是通过采样的方法来完成的。

在InnoDB存储引擎中,Cardinality统计信息的更新发生在两个操作中:INSERT和UPDATE。根据前面的叙述,不可能在每次发生INSERT和UPDATE时就去更新Cardinality信息,这样会增加数据库系统的负荷,同时对于大表的统计,时间上也不允许数据库这样去操作。因此,InnoDB存储引擎内部对更新Cardinality信息的策略为:

  • 表中1/16的数据已发送过变化。
  • stat_modified_counter>2000000000。

第一种策略为自上次统计Cardinality信息后,表中1/16的数据已经发生过变化,这时需要更新Cardinality信息。第二种情况考虑的是,如果对表中某一行数据频繁地进行更新操作,这时表中的数据实际并没有增加,实际发生变化的还是这一行数据,则第一种更新策略就无法适用这种情况。故在InnoDB存储引擎内部有一个计数器stat_modified_counter,用来表示发生变化的次数。当stat_modified_counter,用来表示发生变化的次数,当stat_modified大于2000000000时,同样需要更新Cardinality信息。

接着考虑InnoDB存储引擎内部是怎样来进行Cardinality信息的统计和更新操作的?同样是通过采样的方法,默认InnoDB存储引擎对8个叶子节点(Leaf Page)进行采样,采样的过程如下:

  • 取得B+树索引中叶子节点的数量,记为A。
  • 随机取得B+树索引中8个叶子节点,统计每个页不同记录的个数,即为P1,P2,…,P8。
  • 根据采样信息给出Cardinality的预估值:Cardinality = (P1+P2+…+P8) * A/8。

通过上述的说明可以发现,在InnoDB存储引擎中,Cardinality值是通过对8个叶子节点预估而得到的,不是一个世纪精确的值。再者,每次对Cardinality值的统计,都是通过随机取8个叶子节点得到的,这同时又暗示了另一个Cardinality对象,即每次得到的Cardinality值可能是不同的。如:SHOW INDEX FROM OrderDetails语句会触发MySQL数据库对于Cardinality值的统计,所以可能会出现此表没有任何数据变化,但是你多次执行SHOW INDEX FROM OrderDetails看到的Cardinality值不同。因为每次执行都会触发MySQL数据对于Cardinality值的统计,随机选取8个叶子节点进行分析。所以如果遇到这个并不是InnoDB存储因为的Bug,只是随机采样而导致的。

当然,有一种情况使得用户每次观察到的索引Cardinality值都是一样的,那就是表足够小,表的叶子节点数小于或者等于8个。这时即使随机采样,也总是会采取到这些页,因此每次得到的Cardinality值是相同的。

当执行SQL语句ANALYZE TABLE、SHOW TABLE STATUS、SHOW INDEX以及访问INFORMATION_SCHEMA架构下的表TABLES和STATISTICS时会导致InnoDB存储引擎去重新计算索引的Cardinality值。若表中的数据量非常大,并且表中存在多个辅助索引时,执行上述这些操作可能会非常慢。虽然用户可能并不希望去更新Cardinality值。

InnoDB 1.2版本提供了以下参数对Cardinality统计进行设置,这些参数如下:

mysql> show global variables like '%innodb_stats%';
+--------------------------------------+-------------+
| Variable_name                        | Value       |
+--------------------------------------+-------------+
| innodb_stats_auto_recalc             | ON          |
| innodb_stats_include_delete_marked   | OFF         |
| innodb_stats_method                  | nulls_equal |
| innodb_stats_on_metadata             | OFF         |
| innodb_stats_persistent              | ON          |
| innodb_stats_persistent_sample_pages | 20          |
| innodb_stats_sample_pages            | 8           |
| innodb_stats_transient_sample_pages  | 8           |
+--------------------------------------+-------------+
8 rows in set (0.02 sec)

innodb_stats_auto_recalc

开启与否只会影响persistent类型的统计。

innodb_stats_method

用来判断如何对待索引中出现的NULL值记录,该参数默认值为nulls_equal,表示将NULL值记录视为相同的记录。其有效值还有nulls_unequal,nulls_ignored,分别表示将NULL值记录视为不同的记录和忽略NULL值记录。例如某页中索引记录为NULL、NULL、1、2、2、3、3、3,在参数innodb_stats_method的默认设置下,该页的Cardinality为4;若值为nulls_unequal,则该页的Cardinality为5;若值为nulls_ignored,则Cardinality为3。

innodb_stats_sample_pages

在InnoDB 1.2版本之前,用来设置统计Cardinality时每次采样的数量,默认值:8,在InnoDB 1.2版本时被innodb_stats_transient_sample_pages参数取代。

innodb_stats_on_metadata

当通过命令SHOW TABLE STATUS、SHOW INDEX及访问INFORMATION_SCHEMA架构下的表TABLES和STATISTICS时,是否需要重新计算索引的Cardinality值。默认值:OFF,InnoDB 1.2版本提供。

innodb_stats_persistent

是否将命令ANALYZE TABLE计算得到的Cardinality值存放到磁盘上,存放mysql.innodb_index_stats表中。若是,则这样的好处是可以减少重新计算每个索引的Cardinality值,例如当MySQL数据库重启时。此外,用户也可以通过命令CREATE TABLE和ALTER TABLE的选项STATS_PERSISTENT来对每张表进行控制,默认值:ON,InnoDB 1.2版本提供。

innodb_stats_persistent_sample_pages

若参数innodb_stats_persistent设置为ON,该参数表示ANALYZE TABLE更新Cardinality值时每次采样页的数量。默认值:20,InnoDB 1.2版本提供。

innodb_stats_transient_sample_pages

该参数用来取代之前版本的参数innodb_stats_sample_pages,表示每次采样页的数量。默认值:8,InnoDB 1.2版本提供。

索引选择性实践

为了讨论索引策略,需要一个数据量不算小的数据库作为示例。本文选用MySQL官方文档中提供的示例数据库之一:employees。这个数据库关系复杂度适中,且数据量较大。下图是这个数据库的E-R关系图(引用自MySQL官方手册):

未分类

MySQL官方文档中关于此数据库的页面为https://dev.mysql.com/doc/employee/en。里面详细介绍了此数据库,并提供了下载地址和导入方法,如果有兴趣导入此数据库到自己的MySQL可以参考文中内容。

显然选择性的取值范围为(0, 1],其公式Index Selectivity = Cardinality / #T, 选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,如测试数据库中用到的employees.titles表,如果title字段经常被单独查询,是否需要建索引,我们看一下它的选择性:

mysql> SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;
+-------------+
| Selectivity |
+-------------+
|      0.0000 |
+-------------+
1 row in set (0.29 sec)

title的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建索引。

有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使用。

从上图可以看到employees表只有一个索引,那么如果我们想按名字搜索一个人,就只能全表扫描了:

mysql> EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 300363 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)

如果频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建索引。有两种选择,建或<first_name, last_name>,看下两个索引的选择性:

mysql> SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.0042 |
+-------------+
1 row in set (0.30 sec)

mysql> SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9313 |
+-------------+
1 row in set (0.87 sec)

显然选择性太低,<first_name, last_name>选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法(对于VARCHAR索引最长可以有768字节)?可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 3)>,看看其选择性:

mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+
1 row in set (0.69 sec)

选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:

mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9007 |
+-------------+
1 row in set (0.75 sec)

这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,这时我们把这个前缀索引建上。

先开启profile再执行一遍按名字查询,比较分析一下与建索引前的结果:

mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> SELECT SQL_NO_CACHE * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  18454 | 1955-02-28 | Eric       | Anido     | M      | 1988-07-18 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.17 sec)

mysql> ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));
Query OK, 0 rows affected (1.23 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT SQL_NO_CACHE * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  18454 | 1955-02-28 | Eric       | Anido     | M      | 1988-07-18 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.00 sec)

mysql> set profiling=0;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> SHOW PROFILES;
+----------+------------+----------------------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                                        |
+----------+------------+----------------------------------------------------------------------------------------------+
|        1 | 0.17215550 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido'              |
|        2 | 1.20597425 | ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4)) |
|        3 | 0.00076825 | SELECT SQL_NO_CACHE * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
+----------+------------+----------------------------------------------------------------------------------------------+
7 rows in set, 1 warning (0.00 sec)

性能的提升是显著的,查询速度提高了200多倍。

前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。

从 SQL Server 迁移到 MySQL 要注意什么?

本文基于使用 Navicat 进行数据传输:

未分类

配置源数据库和目标数据库:

未分类

切换到“高级”。如果是同类型数据库,会有“包含自动递增”选项,但是,从 SQL Server 迁移到 MySQL 则没有:

未分类

未分类

传输完成后,我们需要对目标数据库作以下调整:

未分类

其它我没有使用到的字段类型暂未列出。

MySQL 中将用于外键约束的主键设置为自动递增

当主键用于其它表的外键约束时,我们无法更改该主键:

1833 - Cannot change column 'id': used in a foreign key constraint 'FK_xxx' of table 'xxx'

未分类

可以先禁用外键检查再设置自增:

set foreign_key_checks = 0;
ALTER TABLE `<table>` MODIFY COLUMN `id` int(11) NOT NULL AUTO_INCREMENT FIRST;

执行完后,foreign_key_checks 会自动恢复为 1。