十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
有点麻烦的感觉,如列数据为 123 按你的意思是要得到 11这样的数据?这列的方差又是什么意思,读书少呀,看不懂
创新互联主要从事网站建设、网站制作、网页设计、企业做网站、公司建网站等业务。立足成都服务文成,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:13518219792
很多应用往往只展示最新或最热门的几条记录,但为了旧记录仍然可访问,所以就需要个分页的导航栏。然而,如何通过MySQL更好的实现分页,始终是比较令人头疼的问题。虽然没有拿来就能用的解决办法,但了解数据库的底层或多或少有助于优化分页查询。
我们先从一个常用但性能很差的查询来看一看。
SELECT *
FROM city
ORDER BY id DESC
LIMIT 0, 15
这个查询耗时0.00sec。So,这个查询有什么问题呢?实际上,这个查询语句和参数都没有问题,因为它用到了下面表的主键,而且只读取15条记录。
CREATE TABLE city (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
city varchar(128) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
真正的问题在于offset(分页偏移量)很大的时候,像下面这样:
SELECT *
FROM city
ORDER BY id DESC
LIMIT 100000, 15;
上面的查询在有2M行记录时需要0.22sec,通过EXPLAIN查看SQL的执行计划可以发现该SQL检索了100015行,但最后只需要15行。大的分页偏移量会增加使用的数据,MySQL会将大量最终不会使用的数据加载到内存中。就算我们假设大部分网站的用户只访问前几页数据,但少量的大的分页偏移量的请求也会对整个系统造成危害。Facebook意识到了这一点,但Facebook并没有为了每秒可以处理更多的请求而去优化数据库,而是将重心放在将请求响应时间的方差变小。
对于分页请求,还有一个信息也很重要,就是总共的记录数。我们可以通过下面的查询很容易的获取总的记录数。
SELECT COUNT(*)
FROM city;
然而,上面的SQL在采用InnoDB为存储引擎时需要耗费9.28sec。一个不正确的优化是采用 SQL_CALC_FOUND_ROWS,SQL_CALC_FOUND_ROWS 可以在能够在分页查询时事先准备好符合条件的记录数,随后只要执行一句 select FOUND_ROWS(); 就能获得总记录数。但是在大多数情况下,查询语句简短并不意味着性能的提高。不幸的是,这种分页查询方式在许多主流框架中都有用到,下面看看这个语句的查询性能。
SELECT SQL_CALC_FOUND_ROWS *
FROM city
ORDER BY id DESC
LIMIT 100000, 15;
这个语句耗时20.02sec,是上一个的两倍。事实证明使用 SQL_CALC_FOUND_ROWS 做分页是很糟糕的想法。
下面来看看到底如何优化。文章分为两部分,第一部分是如何获取记录的总数目,第二部分是获取真正的记录。
高效的计算行数
如果采用的引擎是MyISAM,可以直接执行COUNT(*)去获取行数即可。相似的,在堆表中也会将行数存储到表的元信息中。但如果引擎是InnoDB情况就会复杂一些,因为InnoDB不保存表的具体行数。
我们可以将行数缓存起来,然后可以通过一个守护进程定期更新或者用户的某些操作导致缓存失效时,执行下面的语句:
SELECT COUNT(*)
FROM city
USE INDEX(PRIMARY);
获取记录
下面进入这篇文章最重要的部分,获取分页要展示的记录。上面已经说过了,大的偏移量会影响性能,所以我们要重写查询语句。为了演示,我们创建一个新的表“news”,按照时事性排序(最新发布的在最前面),实现一个高性能的分页。为了简单,我们就假设最新发布的新闻的Id也是最大的。
CREATE TABLE news(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(128) NOT NULL
) ENGINE=InnoDB;
一个比较高效的方式是基于用户展示的最后一个新闻Id。查询下一页的语句如下,需要传入当前页面展示的最后一个Id。
SELECT *
FROM news WHERE id $last_id
ORDER BY id DESC
LIMIT $perpage
查询上一页的语句类似,只不过需要传入当前页的第一个Id,并且要逆序。
SELECT *
FROM news WHERE id $last_id
ORDER BY id ASC
LIMIT $perpage
上面的查询方式适合实现简易的分页,即不显示具体的页数导航,只显示“上一页”和“下一页”,例如博客中页脚显示“上一页”,“下一页”的按钮。但如果要实现真正的页面导航还是很难的,下面看看另一种方式。
SELECT id
FROM (
SELECT id, ((@cnt:= @cnt + 1) + $perpage - 1) % $perpage cnt
FROM news
JOIN (SELECT @cnt:= 0)T
WHERE id $last_id
ORDER BY id DESC
LIMIT $perpage * $buttons
)C
WHERE cnt = 0;
通过上面的语句可以为每一个分页的按钮计算出一个offset对应的id。这种方法还有一个好处。假设,网站上正在发布一片新的文章,那么所有文章的位置都会往后移一位,所以如果用户在发布文章时换页,那么他会看见一篇文章两次。如果固定了每个按钮的offset Id,这个问题就迎刃而解了。Mark Callaghan发表过一篇类似的博客,利用了组合索引和两个位置变量,但是基本思想是一致的。
如果表中的记录很少被删除、修改,还可以将记录对应的页码存储到表中,并在该列上创建合适的索引。采用这种方式,当新增一个记录的时候,需要执行下面的查询重新生成对应的页号。
SET p:= 0;
UPDATE news SET page=CEIL((p:= p + 1) / $perpage) ORDER BY id DESC;
当然,也可以新增一个专用于分页的表,可以用个后台程序来维护。
UPDATE pagination T
JOIN (
SELECT id, CEIL((p:= p + 1) / $perpage) page
FROM news
ORDER BY id
)C
ON C.id = T.id
SET T.page = C.page;
现在想获取任意一页的元素就很简单了:
SELECT *
FROM news A
JOIN pagination B ON A.id=B.ID
WHERE page=$offset;
还有另外一种与上种方法比较相似的方法来做分页,这种方式比较试用于数据集相对小,并且没有可用的索引的情况下—比如处理搜索结果时。在一个普通的服务器上执行下面的查询,当有2M条记录时,要耗费2sec左右。这种方式比较简单,创建一个用来存储所有Id的临时表即可(这也是最耗费性能的地方)。
CREATE TEMPORARY TABLE _tmp (KEY SORT(random))
SELECT id, FLOOR(RAND() * 0x8000000) random
FROM city;
ALTER TABLE _tmp ADD OFFSET INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, DROP INDEX SORT,ORDER BY random;
接下来就可以向下面一样执行分页查询了。
SELECT *
FROM _tmp
WHERE OFFSET = $offset
ORDER BY OFFSET
LIMIT $perpage;
简单来说,对于分页的优化就是。。。避免数据量大时扫描过多的记录。
1.Trim(A1)
清除掉字符串前后空格。
MySQL有同名函数,Python有近似函数strip。
如果要清除所有空格,用substitute(A1,“ ”,“”)
2.Concatenate(A1,A2……)
合并单元格中的内容
或"我""很""好" = 我很好
MySQL有近似函数concat
3.Replace(指定字符串,哪个位置开始替换,替换几个字符,替换成什么)
MySQL中有同名函数,Python中有同名函数
4.Left/Right(指定字符串,截取长度)
Mid(指定字符串,开始位置,截取长度)
MySQL中有同名函数
5.Len/Lenb
返回字符串的长度,在len中,中文计算为一个,在lenb中,中文计算为两个。
MySQL中有同名函数,Python中有同名函数。
6.Find(要查找字符,指定字符串,第几个字符)
查找某字符串出现的位置,可以指定为第几次出现
MySQL中有近似函数 find_in_set,Python中有同名函数
7.Text
将数值转化为指定的文本格式
1.Lookup(查找的值,值所在的位置,返回相应位置的值)
2.Vlookup(查找的值,哪里找,找哪个位置的值,是否精准匹配)
3.Index(查找的区域,区域内第几行,区域内第几列)
和Match组合,功能强大
4.Match(查找指定的值,查找所在区域,查找方式的参数)
5.Row
返回单元格所在的行
Column
返回单元格所在的列
6.Offset(指定点,偏移多少行,偏移多少列,返回多少行,返回多少列)
if,and,or,is系列
MySQL中有同名函数,Python中有同名函数
1.Sum/Sumif/Sumifs、Count/Countif/Countifs、max、min
MySQL中有同名函数,Python中有同名函数
2.Sumproduct
统计总和相关
MySQL中有同名函数
3.Rank(A2,A2:A23)
排序,返回指定值在引用区域的排名,重复值同一排名。
SQL中有近似函数row_number() 。
4.Rand/Randbetween
常用随机抽样,前者返回0~1之间的随机值,后者可以指定范围。
MySQL中有同名函数。
5.Averagea
求平均值,也有Averageaif,Averageaifs
MySQL中有同名函数,python有近似函数mean。
6.Substotal(引用区域,参数)
汇总型函数,将平均值、计数、最大最小、相乘、标准差、求和、方差等参数化,换言之,只要会了这个函数,上面的都可以抛弃掉了。
7.Int/Round
取整函数,int向下取整,round按小数位取数。
round(3.1415,2) =3.14 ;
round(3.1415,1)=3.1
1.Year、Month、Day、Date(年,月,日)、Now、Today
2.Weekday(指定时间,参数)
参数为1代表从星期日开始算作第一天
3.Datedif(开始日期,结束日期,参数)
日期计算函数,计算两日期的差。参数决定返回的是年还是月等。
MySQL中有近似函数 DateDiff。
4.同时显示日期和时间:自定义格式为yyyy-m-d AAAA
快捷键,格式转换(数值、文本、时间),数组,分列(SQL和Python中有类似的spilt ( )函数),合并单元格,数据透视表(聚合功能有一点类似SQL中的gorup by,python中则有更为强大的pandas.pivot_table( )),删除重复项(功能类似SQL中的distinct ,python中的set),单元格条件格式,冻结首行首列(在Python和R中,read_csv函数,会有一个专门的参数header=true,来判断是否读取表头作为columns的名字),自定义下拉菜单(数据有效性),查找公式错误,分组和分级显示,分析工具库,第三方应用
快速合并多列数据:把列拉宽-选取数据-填充-两端对齐
方差是有函数的,
例如:
SQL SELECT
2 sale_item,
3 VARIANCE(sale_money)
4 FROM
5 sale_report
6 GROUP BY
7 sale_item;
SALE VARIANCE(SALE_MONEY)
---- --------------------
A 0
B 11.9203372
C 77.5865272
注: Oracle 里面是 VARIANCE 函数, SQL Server 是 VAR 函数, MySQL 是 VAR_SAMP 函数。
但是要求你的数据是 一行一行的
而你的数据, 是一列一列的。
也就是你需要先作 列行转换
我这里假设你的数据库是 SQL Server
先
SELECT 序号, '6' AS 月份, [6月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '7' AS 月份, [7月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '8' AS 月份, [8月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '9' AS 月份, [9月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '10' AS 月份, [10月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '11' AS 月份, [11月] AS 数据 FROM 表 ;
上面的查询, 应该就会形成一个这样格式的结果:
序号 月份 数据
1 6 134.8
1 7 134.4
1 8 133.4
1 9 134.5
1 10 133.7
1 11 133.7
其他数据略...
这样就可以使用那个函数来处理了
SELECT
序号,
VAR ( 数据 ) AS 方差
FROM
(
SELECT 序号, '6' AS 月份, [6月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '7' AS 月份, [7月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '8' AS 月份, [8月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '9' AS 月份, [9月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '10' AS 月份, [10月] AS 数据 FROM 表 UNION ALL
SELECT 序号, '11' AS 月份, [11月] AS 数据 FROM 表
) subQuery
GROUP BY
序号
没有rando函数。只有rand和randn
1.rand()
生成(0,1)区间上均匀分布的随机变量。基本语法:
rand([M,N,P ...])
生成排列成M*N*P... 多维向量的随机数。如果只写M,则生成M*M矩阵;如果参数为[M,N]可以省略掉方括号。一些例子:
rand(5,1) %生成5个随机数排列的列向量,一般用这种格式
rand(5) %生成5行5列的随机数矩阵
rand([5,4]) %生成一个5行4列的随机数矩阵
生成的随机数大致的分布。
x=rand(100000,1);
hist(x,30);
由此可以看到生成的随机数很符合均匀分布。(视频教程会略提及hist()函数的作用)
2.randn()
生成服从标准正态分布(均值为0,方差为1)的随机数。基本语法和rand()类似。
randn([M,N,P ...])
生成排列成M*N*P... 多维向量的随机数。如果只写M,则生成M*M矩阵;如果参数为[M,N]可以省略掉方括号。一些例子:
randn(5,1) %生成5个随机数排列的列向量,一般用这种格式
randn(5) %生成5行5列的随机数矩阵
randn([5,4]) %生成一个5行4列的随机数矩阵
3、matlab中random函数——通用函数,求各分布的随机数据,其用法:
y = random('norm',A1,A2,A3,m,n)
式中:A1,A2,A3为分布的参数,m,n用来指定随机数的行和列,name的取值有相关的表格来参照。
例:产生一个3行4列均值为2、标准差为0.3的正态分布随机数:
y =random('norm',2,0.3,3,4)
y =
2.1613 2.2587 1.8699 2.8308
2.5502 2.0956 2.1028 1.5950
1.3223 1.6077 3.0735 2.9105