一、索引

1、索引介绍

一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到复杂的查询操作,通过索引可以加速查询。

    索引在MySQL中也叫做“键”,是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时,索引对于性能的影响愈发重要。

索引是应用程序设计和开发的一个重要方面。若索引太多,应用程序的性能可能会受到影响。而索引太少,对查询性能又会产生影响,要找到一个平衡点,这对应用程序的性能至关重要。

2、索引原理

本质:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。


    我们把数据存储在磁盘上,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分。访问一次磁盘的时间,即一次磁盘IO的时间约等于9ms左右,所以减少磁盘IO是优化的关键。


3、聚集索引与辅助索引

在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一个键值的行记录时最多只需要2到4次IO,这倒不错。因为当前一般的机械硬盘每秒至少可以做100次IO,2~4次的IO意味着查询时间只需要0.02~0.04秒。


数据库中的B+树索引可以分为聚集索引(clustered index)和辅助索引(secondary index)


聚集索引与辅助索引的对比:

                相同的是:不管是聚集索引还是辅助索引,其内部都是B+树的形式,即高度是平衡的,叶子结点存放着所有的数据。

                不同的是:叶子结点存放的是否是一整行的信息


(1)聚集索引


聚集索引的好处之一:它对主键的排序查找和范围查找速度非常快,叶子节点的数据就是用户所要查询的数据。如用户需要查找一张表,查询最后的10位用户信息,由于B+树索引是双向链表,所以用户可以快速找到最后一个数据页,并取出10条记录


聚集索引的好处之二:范围查询(range query),即如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可


(2)辅助索引

辅助索引的叶子节点不包含行记录的全部数据。

叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含一个书签(bookmark)。该书签用来告诉InnoDB存储引擎去哪里可以找到与索引相对应的行数据。


由于InnoDB存储引擎是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。

辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引,但只能有一个聚集索引。

当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶子级别的指针获得只想主键索引的主键,然后再通过主键索引来找到一个完整的行记录。


4、总结

(1) 一定是为搜索条件的字段创建索引,比如select * from s1 where id = 333;就需要为id加上索引

(2) 在表中已经有大量数据的情况下,建索引会很慢,且占用硬盘空间,建完后查询速度加快

(3) 需要注意的是:innodb表的索引会存放于s1.ibd文件中,而myisam表的索引则会有单独的索引文件table1.MYI


5、使用索引

并不是说我们创建了索引就一定会加快查询速度,若想利用索引达到预想的提高查询速度的效果,我们在添加索引时,必须遵循以下问题:

(1)范围问题,或者说条件不明确,条件中出现这些符号或关键字:>、>=、<、<=、!= 、between...and...、like、

(2)尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录

(3)=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

(4)索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,因为b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)

(5) and/or

# 1、and与or的逻辑

条件1 and 条件2: 所有条件都成立才算成立,但凡要有一个条件不成立则最终结果不成立

条件1 or 条件2: 只要有一个条件成立则最终结果就成立


# 2、and的工作原理

条件:

a = 10 and b = 'xxx' and c > 3 and d = 4

索引:

制作联合索引(d, a, b, c)

工作原理:

对于连续多个and:mysql会按照联合索引,从左到右的顺序找一个区分度高的索引字段(这样便可以快速锁定很小的范围),加速查询,即按照d— > a->b->c的顺序


# 3、or的工作原理

条件:

a = 10 or b = 'xxx' or c > 3 or d = 4

索引:

制作联合索引(d, a, b, c)


工作原理:

对于连续多个or:mysql会按照条件的顺序,从左到右依次判断,即a->b->c->d


(6)最左前缀匹配原则,对于组合索引mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配(指的是范围大了,有索引速度也慢),如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

(7)其他

- 使用函数

select * from tb1 where reverse(email) = 'egon';


- 类型不一致

如果列是字符串类型,传入条件是必须用引号引起来,不然...

select * from tb1 where email = 999;


# 排序条件为索引,则select字段必须也是索引字段,否则无法命中

- order by

select name from s1 order by email desc;

当根据索引排序时候,select查询的字段如果不是索引,则速度仍然很慢

select email from s1 order by email desc;

特别的:如果对主键排序,则还是速度很快:

select * from tb1 order by nid desc;


- 组合索引最左前缀

如果组合索引为:(name, email)

name and email - - 命中索引

name - - 命中索引

email - - 未命中索引


- count(1)或count(列)代替count(*) 在mysql中没有差别了

- create index xxxx on tb(title(19))                # text类型,必须制定长度


6、注意事项

- 避免使用select *

- count(1)或count(列) 代替 count(*)

- 创建表时尽量时 char 代替 varchar

- 表的字段顺序固定长度的字段优先

- 组合索引代替多个单列索引(经常使用多个条件查询时)

- 尽量使用短索引

- 使用连接(JOIN)来代替子查询(Sub-Queries)

- 连表时注意条件类型需一致

- 索引散列值(重复少)不适合建索引,例:性别不适合


7、 联合索引


联合索引时指对表上的多个列合起来做一个索引。联合索引的创建方法与单个索引的创建方法一样,不同之处在仅在于有多个索引列

mysql> create table t(

    -> a int,

    -> b int,

    -> primary key(a),

    -> key idx_a_b(a,b)

    -> );


8、覆盖索引


InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录。


使用覆盖索引的一个好处是:辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作

覆盖索引的另外一个好处是对某些统计问题而言的。

对于(a,b)形式的联合索引,一般是不可以选择b中所谓的查询条件。但如果是统计操作,并且是覆盖索引,则优化器还是会选择使用该索引

二、慢查询

1、慢查询优化的基本步骤:

        0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE

        1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高

        2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)

        3.order by limit 形式的sql语句让排序的表优先查

        4.了解业务方使用场景

        5.加索引时参照建索引的几大原则

        6.观察结果,不符合预期继续从0分析


2、慢日志管理

慢日志

- 执行时间 > 10

- 未命中索引

- 日志文件路径


配置:

- 内存

show variables like '%query%';

show variables like '%queries%';

set global 变量名 = 值

- 配置文件

mysqld - -defaults - file = 'E:\mysql-5.7.16-winx64\mysql-5.7.16-winx64\my-default.ini'


my.conf内容:

slow_query_log = ON

slow_query_log_file = E: / ....


注意:修改配置文件之后,需要重启服务


3、mysql日志管理

(1)bin-log

#1. 启用

# vim /etc/my.cnf

[mysqld]

log-bin[=dir\[filename]]

# service mysqld restart

#2. 暂停

//仅当前会话

SET SQL_LOG_BIN=0;

SET SQL_LOG_BIN=1;

#3. 查看

查看全部:

# mysqlbinlog mysql.000002

按时间:

# mysqlbinlog mysql.000002 --start-datetime="2012-12-05 10:02:56"

# mysqlbinlog mysql.000002 --stop-datetime="2012-12-05 11:02:54"

# mysqlbinlog mysql.000002 --start-datetime="2012-12-05 10:02:56" --stop-datetime="2012-12-05 11:02:54"


按字节数:

# mysqlbinlog mysql.000002 --start-position=260

# mysqlbinlog mysql.000002 --stop-position=260

# mysqlbinlog mysql.000002 --start-position=260 --stop-position=930

#4. 截断bin-log(产生新的bin-log文件)

a. 重启mysql服务器

b. # mysql -uroot -p123 -e 'flush logs'

#5. 删除bin-log文件

# mysql -uroot -p123 -e 'reset master'


(2)查询日志

启用通用查询日志

# vim /etc/my.cnf

[mysqld]

log[=dir\[filename]]

# service mysqld restart                  #修改配置文件后需要重启服务


(3)慢查询日志

启用慢查询日志

# vim /etc/my.cnf

[mysqld]

log-slow-queries[=dir\[filename]]

long_query_time=n

# service mysqld restart                   #修改配置文件后需要重启服务

MySQL 5.6:

slow-query-log=1

slow-query-log-file=slow.log

long_query_time=3

查看慢查询日志

测试:BENCHMARK(count,expr)

SELECT BENCHMARK(50000000,2*3);


三、pymysql模块

pip3 install pymysql       #安装模块

1、创建表

import pymysql
conn=pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='',
    db='test'
)
cursor=conn.cursor()       #拿游标
sql='''
create table info(
    id int primary key auto_increment,
    username char(16),
    password char(20)
);
'''
cursor.execute(sql)        #提交sql
conn.commit()
cursor.close()           #关闭(游标)
conn.close()             #断开连接

2、插入数据

import pymysql
conn=pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='',
    db='test'
)
cursor=conn.cursor()       #拿游标
# sql='insert into info(username,password) values("root","123456");'          #插入单条数据
# cursor.execute(sql)

sql='insert into info(username,password) values(%s,%s);'
res=cursor.executemany(sql,[("root","123456"),("lhf","12356"),("eee","156")])    #插入多条记录,执行sql语句,返回sql影响成功的行数
print (res)
cursor.close()
conn.commit()            #数据的增、删、改必须要提交
conn.close()

3、查询数据操作

import pymysql
conn=pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='',
    db='test'
)
cursor=conn.cursor(pymysql.cursors.DictCursor)        #数据以字典形式显示
# cursor=conn.cursor()                                    #数据以元组形式显示
sql='select * from info'
rows=cursor.execute(sql)
print(cursor.fetchone())             #取出第一条数据 {'id': 1, 'username': 'root', 'password': '123456'}
# print(cursor.fetchmany(3))          #可以指定显示数据的条数
#print(cursor.fetchall())            #显示所有的数据

cursor.scroll(0,mode='absolute')        # 相对绝对位置移动
print(cursor.fetchone())              #显示第一条数据{'id': 1, 'username': 'root', 'password': '123456'}
cursor.scroll(1,mode='relative')        # 相对当前位置移动
print(cursor.fetchone())              #显示第三条数据{'id': 3, 'username': 'eee', 'password': '156'}
cursor.close()
conn.close()

4、execute()之sql注入

根本原理:就根据程序的字符串拼接name='%s',我们输入一个xxx' -- haha,用我们输入的xxx加'在程序中拼接成一个判断条件name='xxx' -- haha'

(1)之前的版本

import pymysql
user=input('user>>: ').strip()
pwd=input('password>>: ').strip()
conn=pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='',
    db='test'
)
cursor=conn.cursor(pymysql.cursors.DictCursor)
sql='select id from info where username="%s" and password="%s"' %(user,pwd)
print(sql)
#用户名为:eee"  -- aa  ,密码为空,这时候sql就select * from info where username="eee" -- aa" and password="",登录成功
#用户名为:xxx" or 1=1 -- aa ,密码为空,这时候sql就select * from info where username="xxx" or 1=1 -- aa" and password=""登录成功
rows=cursor.execute(sql)
if rows:
    print('登录成功')
else:
    print('用户名或密码错误')
cursor.close()
conn.commit()
conn.close()


(2)修改后的版本

import pymysql
user=input('user>>: ').strip()
pwd=input('password>>: ').strip()
conn=pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='',
    db='test'
)
cursor=conn.cursor(pymysql.cursors.DictCursor)
sql='select * from user where username=%s and password=%s'
rows=cursor.execute(sql,(user,pwd))
if rows:
    print('登录成功')
else:
    print('用户名或密码错误')
cursor.close()
conn.commit()
conn.close()
#修改后,pymysql会处理掉用户输入的特殊字符,就解决了sql的注入问题

5、获取插入的最后一条数据的自增ID

import pymysql
conn=pymysql.connect(host='127.0.0.1',user='root',password='',database='test')
cursor=conn.cursor()
sql='insert into info(name,password) values("xxx","123");'
rows=cursor.execute(sql)
print(cursor.lastrowid)              #在插入语句后查看
conn.commit()
cursor.close()
conn.close()

四、视图

视图是一个虚拟表,只有表结构,没有数据

    使用视图我们可以把查询过程中的临时表摘出来,用视图去实现,这样以后再想操作该临时表的数据时就无需重写复杂的sql了,直接去视图中查找即可,但视图有明显地效率问题,并且视图是存放在数据库中的,如果我们程序中使用的sql过分依赖数据库中的视图,即强耦合,那就意味着扩展sql极为不便,因此并不推荐使用

我们不应该修改视图中的记录,而且在涉及多个表的情况下是根本无法修改视图中的记录的。


五、触发器

1、创建触发器

# 插入前

CREATE TRIGGER tri_before_insert_tb1 BEFORE INSERT ON tb1 FOR EACH ROW

BEGIN

    ...

END


# 插入后

CREATE TRIGGER tri_after_insert_tb1 AFTER INSERT ON tb1 FOR EACH ROW

BEGIN

    ...

END


# 删除前

CREATE TRIGGER tri_before_delete_tb1 BEFORE DELETE ON tb1 FOR EACH ROW

BEGIN

    ...

END


# 删除后

CREATE TRIGGER tri_after_delete_tb1 AFTER DELETE ON tb1 FOR EACH ROW

BEGIN

    ...

END


# 更新前

CREATE TRIGGER tri_before_update_tb1 BEFORE UPDATE ON tb1 FOR EACH ROW

BEGIN

    ...

END


# 更新后

CREATE TRIGGER tri_after_update_tb1 AFTER UPDATE ON tb1 FOR EACH ROW

BEGIN

    ...

END


2、插入后触发触发器

#准备表
CREATE TABLE cmd (
    id INT PRIMARY KEY auto_increment,
    USER CHAR (32),
    priv CHAR (10),
    cmd CHAR (64),
    sub_time datetime, #提交时间
    success enum ('yes', 'no') #0代表执行失败
);
CREATE TABLE errlog (
    id INT PRIMARY KEY auto_increment,
    err_cmd CHAR (64),
    err_time datetime
);
#创建触发器
delimiter //                    #定义sql结束符
CREATE TRIGGER tri_after_insert_cmd AFTER INSERT ON cmd FOR EACH ROW
BEGIN
    IF NEW.success = 'no' THEN #等值判断只有一个等号
            INSERT INTO errlog(err_cmd, err_time) VALUES(NEW.cmd, NEW.sub_time) ; #必须加分号
      END IF ;                  #必须加分号
END//                            #sql语句结束
delimiter ;                     #sql结束符修改为之前的分号
#往表cmd中插入记录,触发触发器,根据IF的条件决定是否插入错误日志
INSERT INTO cmd (
    USER,
    priv,
    cmd,
    sub_time,
    success
)
VALUES
    ('egon','0755','ls -l /etc',NOW(),'yes'),
    ('egon','0755','cat /etc/passwd',NOW(),'no'),
    ('egon','0755','useradd xxx',NOW(),'no'),
    ('egon','0755','ps aux',NOW(),'yes');
#查询错误日志,发现有两条
mysql> select * from errlog;
+----+-----------------+---------------------+
| id | err_cmd         | err_time            |
+----+-----------------+---------------------+
|  1 | cat /etc/passwd | 2017-09-14 22:18:48 |
|  2 | useradd xxx     | 2017-09-14 22:18:48 |
+----+-----------------+---------------------+

特别的:NEW表示即将插入的数据行,OLD表示即将删除的数据行。

#触发器无法由用户直接调用,而是由于对表的【增/删/改】操作被动引发的


六、事务

事务用于将某些操作的多个SQL作为原子性操作,一旦有某一个出现错误,即可回滚到原来的状态,从而保证数据库数据完整性。


create table user(
id int primary key auto_increment,
name char(32),
balance int
);
insert into user(name,balance)
values
('wsb',1000),
('egon',1000),
('ysb',1000);
#原子操作
start transaction;
update user set balance=900 where name='wsb'; #买支付100元
update user set balance=1010 where name='egon'; #中介拿走10元
update user set balance=1090 where name='ysb'; #卖家拿到90元
commit;

#出现异常,回滚到初始状态
比如:卖家应该拿到90元,出现异常而没有拿到90元,需要回到之前的数据
rollback;                                      #回滚操作,在数据提交前有效,提交后无法回滚
commit;                                         #提交数据
mysql> select * from user;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | wsb  |    1000 |
|  2 | egon |    1000 |
|  3 | ysb  |    1000 |
+----+------+---------+


七、存储过程

1、存储过程介绍

存储过程包含了一系列可执行的sql语句,存储过程存放于MySQL中,通过调用它的名字可以执行其内部的一堆sql


使用存储过程的优点:

        #1. 用于替代程序写的SQL语句,实现程序与sql解耦

        #2. 基于网络传输,传别名的数据量小,而直接传sql数据量大

使用存储过程的缺点:

        #1. 程序员扩展功能不方便


程序与数据库结合使用的三种方式

#方式一:

    MySQL:存储过程

    程序:调用存储过程

#方式二:

    MySQL:

    程序:纯SQL语句

#方式三:

    MySQL:

    程序:类和对象,即ORM(本质还是纯SQL语句)


2、创建无参的存储过程

delimiter //
create procedure p1()
BEGIN
    select * from blog;
    INSERT into blog(name,sub_time) values("xxx",now());
END //
delimiter ;
#在mysql中调用
call p1()
#在python中基于pymysql调用
cursor.callproc('p1')
print(cursor.fetchall())

3、创建有参的存储过程

对于存储过程,可以接收参数,其参数有三类:

(1)in          #仅用于传入参数用

delimiter //
create procedure p2( in n1 int, in n2 int )
BEGIN
    select * from blog where id > n1;
END //
delimiter;
# 在mysql中调用
call p2(3, 2)
# 在python中基于pymysql调用
cursor.callproc('p2', (3, 2))
print(cursor.fetchall())


(2)out        #仅用于返回值用

delimiter //
create procedure p3(
    in n1 int,
    out res int
)
BEGIN
    select * from blog where id > n1;
    set res = 1;
END //
delimiter ;
#在mysql中调用
set @res=0;                             #0代表假(执行失败),1代表真(执行成功)
call p3(3,@res);
select @res;
#在python中基于pymysql调用
cursor.callproc('p3',(3,0))               #0相当于set @res=0
print(cursor.fetchall())                  #查询select的查询结果
cursor.execute('select @_p3_0,@_p3_1;') #@p3_0代表第一个参数,@p3_1代表第二个参数,即返回值
print(cursor.fetchall())

(3)inout        #既可以传入又可以当作返回值

delimiter //
create procedure p4(
    inout n1 int
)
BEGIN
    select * from blog where id > n1;
    set n1 = 1;
END //
delimiter ;
#在mysql中调用
set @x=3;
call p4(@x);
select @x;
#在python中基于pymysql调用
cursor.callproc('p4',(3,))
print(cursor.fetchall())                     #查询select的查询结果
cursor.execute('select @_p4_0;')
print(cursor.fetchall())

4、事务的应用

delimiter //
create PROCEDURE p5(OUT p_return_code tinyint)
BEGIN
    DECLARE exit handler for sqlexception
    BEGIN
        -- ERROR
        set p_return_code = 1;
        rollback;
    END;
    DECLARE exit handler for sqlwarning
    BEGIN
        -- WARNING
        set p_return_code = 2;
        rollback;
    END;
    START TRANSACTION;
        DELETE from tb1; #执行失败
        insert into blog(name,sub_time) values('yyy',now());
    COMMIT;
    -- SUCCESS
    set p_return_code = 0; #0代表执行成功
END //
delimiter ;
#在mysql中调用存储过程
set @res=123;
call p5(@res);
select @res;
#在python中基于pymysql调用存储过程
cursor.callproc('p5',(123,))
print(cursor.fetchall()) #查询select的查询结果
cursor.execute('select @_p5_0;')
print(cursor.fetchall())

5、执行存储过程

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import pymysql
conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='123', db='t1')
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
# 执行存储过程
cursor.callproc('p1', args=(1, 22, 3, 4))
# 获取执行完存储的参数
cursor.execute("select @_p1_0,@_p1_1,@_p1_2,@_p1_3")
result = cursor.fetchall()
conn.commit()
cursor.close()
conn.close()
print(result)

6、函数

date_format函数

(1)基本使用

mysql> SELECT DATE_FORMAT('2009-10-04 22:23:00', '%W %M %Y');

        -> 'Sunday October 2009'

mysql> SELECT DATE_FORMAT('2007-10-04 22:23:00', '%H:%i:%s');

        -> '22:23:00'

mysql> SELECT DATE_FORMAT('1900-10-04 22:23:00',

    ->                 '%D %y %a %d %m %b %j');

        -> '4th 00 Thu 04 10 Oct 277'

mysql> SELECT DATE_FORMAT('1997-10-04 22:23:00',

    ->                 '%H %k %I %r %T %S %w');

        -> '22 22 10 10:23:00 PM 22:23:00 00 6'

mysql> SELECT DATE_FORMAT('1999-01-01', '%X %V');

        -> '1998 52'

mysql> SELECT DATE_FORMAT('2006-06-00', '%d');

        -> '00'


(2) 准备表和记录

CREATE TABLE blog (
    id INT PRIMARY KEY auto_increment,
    NAME CHAR (32),
    sub_time datetime
);
INSERT INTO blog (NAME, sub_time)
VALUES
    ('第1篇','2015-03-01 11:31:21'),
    ('第2篇','2015-03-11 16:31:21'),
    ('第3篇','2016-07-01 10:21:31'),
    ('第4篇','2016-07-22 09:23:21'),
    ('第5篇','2016-07-23 10:11:11'),
    ('第6篇','2016-07-25 11:21:31'),
    ('第7篇','2017-03-01 15:33:21'),
    ('第8篇','2017-03-01 17:32:21'),
    ('第9篇','2017-03-01 18:31:21');

(3) 提取sub_time字段的值,按照格式后的结果即"年月"来分组

SELECT DATE_FORMAT(sub_time,'%Y-%m'),COUNT(1) FROM blog GROUP BY DATE_FORMAT(sub_time,'%Y-%m');
#结果
+-------------------------------+----------+
| DATE_FORMAT(sub_time,'%Y-%m') | COUNT(1) |
+-------------------------------+----------+
| 2015-03                       |        2 |
| 2016-07                       |        4 |
| 2017-03                       |        3 |
+-------------------------------+----------+