热门标签 | HotTags
当前位置:  开发笔记 > 数据库 > 正文

记一次SQLServer的分页优化兼谈谈使用Row

最近有项目反应,在服务器CPU使用较高的时候,我们的事件查询页面非常的慢,查询几条记录竟然要4分钟甚至更长,而且在翻第二页的时候也是要这么多的时间,这肯定是不能接受的,也是让现场用SQLServerProfiler把语句抓取了上来。用ROW_NUMBER()进行分页我

最近有项目反应,在服务器CPU使用较高的时候,我们的事件查询页面非常的慢,查询几条记录竟然要4分钟甚至更长,而且在翻第二页的时候也是要这么多的时间,这肯定是不能接受的,也是让现场用 SQLServerProfiler 把语句抓取了上来。 用ROW_NUMBER()进行分页 我

最近有项目反应,在服务器CPU使用较高的时候,我们的事件查询页面非常的慢,查询几条记录竟然要4分钟甚至更长,而且在翻第二页的时候也是要这么多的时间,这肯定是不能接受的,也是让现场用SQLServerProfiler把语句抓取了上来。

用ROW_NUMBER()进行分页

我们看看现场抓上来的分页语句:

select top 20 a.*,ag.Name as AgentServerName,,d.Name as MgrObjTypeName,l.UserName as userName 
from eventlog as a 
	left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
	left join addrnode as c on b.AddrId=c.Id 
	left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
	left join eventdir as e on a.EventBm=e.Bm 
	left join agentserver as ag on a.AgentBm=ag.AgentBm 
	left join loginUser as l on a.cfmoper=l.loginGuid 
where a.OrderNo not in  (
	select top 0 OrderNo  
	from eventlog  as a 
		left join mgrobj as b on a.MgrObjId=b.Id 
		left join addrnode as c on b.AddrId=c.Id  
	where 1=1 and a.AlarmTime>=&#39;2014-12-01 00:00:00&#39; and a.AlarmTime<=&#39;2014-12-26 23:59:59&#39; 
		and b.AddrId in (&#39;02109000&#39;,……,&#39;02109002&#39;) 
	order by  AlarmTime desc 
	)  
and 1=1 and a.AlarmTime>=&#39;2014-12-01 00:00:00&#39; and a.AlarmTime<=&#39;2014-12-26 23:59:59&#39; 
	and b.AddrId in (&#39;02109000&#39;,……,&#39;02109002&#39;) 
order by  AlarmTime DESC

这是典型的使用两次top来进行分页的写法,原理是:先查出pageSize*(pageIndex-1)(T1)的记录数,然后再TopPageSize条不在T1中的记录,就是当前页的记录。这种查询效率不高主要是使用了not in。参考我之前文章《程序猿是如何解决SQLServer占CPU100%的》提到的:“对于不使用SARG运算符的表达式,索引是没有用的”

那么改为使用ROW_NUMBER分页:

WITH cte AS(
	select a.*,ag.Name as AgentServerName,d.Name as MgrObjTypeName,l.UserName as userName,b.AddrId
			,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
		from eventlog as a WITH(FORCESEEK) 
			left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
			left join addrnode as c on b.AddrId=c.Id 
			left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
			left join eventdir as e on a.EventBm=e.Bm 
			left join agentserver As ag on a.AgentBm=ag.AgentBm 
			left join loginUser as l on a.cfmoper=l.loginGuid 
		where a.AlarmTime>=&#39;2014-12-01 00:00:00&#39; and a.AlarmTime<=&#39;2014-12-26 23:59:59&#39; 
			AND b.AddrId in (&#39;02109000&#39;,……,&#39;02109002&#39;)
)
SELECT * FROM cte WHERE RowNo BETWEEN 1 AND 20;

执行时间从14秒提升到5秒,这说明Row_Number分页还是比较高效的,而且这种写法比top top分页优雅很多。

“欺骗”查询引擎让查询按你的期望去查询

但是为什么查询20条记录竟然要5秒呢,尤其在这个表是加上了时间索引的情况下——参考《程序猿是如何解决SQLServer占CPU100%的》中提到的索引。

我尝试去掉这句AND b.AddrId in ('02109000',……,'02109002'),结果不到1秒就把538条记录查询出来了,而加上地点限制这句,结果是204行。为什么结果集不大,花费的时间却相差这么多呢?查看执行计划,发现走的是另外的索引,而非时间索引。

把这个疑问放到了SQLServer群上,很快,高桑给了回复:要想达到跟去掉地点限制这句的效果,就使用AdddrId+'' in

什么意思?一时没看明白,是高桑没看懂我的语句?很快,有人补充,要欺骗查询引擎。“欺骗”?还是不懂,不过我照做了,把上述cte的语句原封不动的Copy出来,然后把这句AND b.AddrId in ('02109000',……,'02109002')更改为了AND b.AddrId+'' in ('02109000',……,'02109002'),一点执行,神了!!!不到1秒就执行完了。在把执行计划一对,果然走的是时间索引:

后来回味了一下,记起之前看到的查询引擎优化原理,如果你的条件中带有运算符或者使用函数等,则查询引擎会放弃优化,而执行表扫描。脑袋突然转过来了,在使用b.AddrId+''前查询引擎尝试把mgrObj表加入一起做优化,那么两个表联查,会导致预估的记录数大大增加,而使用了b.AddrId+'',查询引擎则会先按时间索引把记录刷选出来,这样就达到了效果,即强制先做cte在执行in条件,而不是在cte中进行in条件刷选。原来如此!有时候,查询引擎过度的优化,会导致相反的效果,而你如果能够知道优化的原理,那么就可以通过一些小的技巧让查询引擎按你的期望去进行优化

ROW_NUMBER()分页在页数较大时的问题

事情到这里,还没完。后面同事又跟我反应,查询到后面的页数,又卡了!what?我重新执行上述语句,把时间范围放到2011-12-01到2014-12-26,记录数限制为为19981到20000,果然,查询要30秒左右,查看执行计划,都是一样的,为什么?

高桑怀疑是key lookup过多导致的,建议先分页取出rid 再做key lookup。不懂这么一句是什么意思。把执行计划和IO打印出来:

看看IO,很明显,主要是越到后面的页数,其他的几个关联表读取的页数就越多。我推测,在Row_Number分页的时候,如果有表连接,则按排序一致到返回的记录数位置,前面的记录都是要参与表连接的,这就导致了越到后面的分页,就越慢,因为要扫描的关联表就越多。

难道就没有了办法了吗?这个时候宋桑英勇的站了出来:“你给表后加一个forceseek提示可破”。这真是犹如天籁之音,马上进行尝试。

使用forceseek提示可以强制表走索引

查了下资料:

SQL Server2008中引入的提示ForceSeek,可以用它将索引查找来替换索引扫描

那么,就在eventlog表中加上这句看看会怎样?

果然,查询计划变了,开始提示,缺少了包含索引。赶紧加上,果然,按这个方式进行查询之后查询时间变为18秒,有进步!但是查看IO,跟上面一样,并没有变少。不过,总算学会了一个新的技能,而宋桑也很热心说晚上再帮忙看看。

把其他没参与where的表放到cte外面

根据上面的IO,很快,又有人提到,把其他left join的表放到cte外面。这是个办法,于是把除eventlogmgrobjaddrnode的表放到外面,语句如下:

WITH cte AS(
	select a*,b.AddrId,b.Name as MgrObjName,b.MgrObjTypeId          
			,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
		from eventlog as a
			left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm 
			left join addrnode as c on b.AddrId=c.Id 
		where a.AlarmTime>=&#39;2011-12-01 00:00:00&#39; and a.AlarmTime<=&#39;2014-12-26 23:59:59&#39; 
			AND b.AddrId+&#39;&#39; in (&#39;02109000&#39;,……,&#39;02109002&#39;)
)
SELECT a.* 
	,ag.Name as AgentServerName
	,d.Name as MgrObjTypeName,l.UserName as userName
FROM cte a left join eventdir as e on a.EventBm=e.Bm 
			left join mgrobjtype as d on a.MgrObjTypeId=d.Id 
			left join agentserver As ag on a.AgentBm=ag.AgentBm 
			left join loginUser as l on a.cfmoper=l.loginGuid 
WHERE RowNo BETWEEN 19980 AND 20000;

果然有效,IO大大减少了,然后速度也提升到了16秒。

表 &#39;loginuser&#39;。扫描计数 1,逻辑读取 63 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;agentserver&#39;。扫描计数 1,逻辑读取 1617 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobjtype&#39;。扫描计数 1,逻辑读取 126 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;eventdir&#39;。扫描计数 1,逻辑读取 42 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;addrnode&#39;。扫描计数 1,逻辑读取 119997 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;Worktable&#39;。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;eventlog&#39;。扫描计数 1,逻辑读取 5027 次,物理读取 3 次,预读 5024 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobj&#39;。扫描计数 1,逻辑读取 24 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

我们看到,addrNode表还是扫描计数很大。那还能不能提升,这个时候,我想到了,先把addrNodemgrobjmgrobjtype三个表联合查询,放到一个临时表,然后再和eventloginner join,然后查询结果再和其他表做left join,这样还能减少IO。

使用临时表存储分页记录在进行表连接减少IO

IF OBJECT_ID(&#39;tmpMgrObj&#39;) IS NOT NULL DROP TABLE tmpMgrObj
SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName 
	INTO tmpMgrObj  
	FROM dbo.mgrobj m
		INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
	WHERE AddrId IN(&#39;02109000&#39;,……,&#39;02109002&#39;);
WITH cte AS(
	select a.*,b.AddrId,b.MgrObjTypeId          
			,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
			,ag.Name as AgentServerName
	,d.Name as MgrObjTypeName,l.UserName as userName
		from eventlog as a
			INNER join tmpMgrObj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
			left join mgrobjtype as d on b.MgrObjTypeId=d.Id 
			left join agentserver As ag on a.AgentBm=ag.AgentBm 
			left join loginUser as l on a.cfmoper=l.loginGuid 
	WHERE AlarmTime>&#39;2011-12-01 00:00:00&#39; AND AlarmTime<=&#39;2014-12-26 23:59:59&#39;
) 
SELECT * FROM cte WHERE RowNo BETWEEN 19980 AND 20000
IF OBJECT_ID(&#39;tmpMgrObj&#39;) IS NOT NULL DROP TABLE tmpMgrObj

这次查询仅用了10秒。我们来看看IO:

表 &#39;Worktable&#39;。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobj&#39;。扫描计数 1,逻辑读取 24 次,物理读取 2 次,预读 23 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;addrnode&#39;。扫描计数 1,逻辑读取 6 次,物理读取 3 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
----------
表 &#39;loginuser&#39;。扫描计数 0,逻辑读取 24 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;Worktable&#39;。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;eventlog&#39;。扫描计数 93,逻辑读取 32773 次,物理读取 515 次,预读 1536 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;tmpMgrObj&#39;。扫描计数 1,逻辑读取 3 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobjtype&#39;。扫描计数 1,逻辑读取 6 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;agentserver&#39;。扫描计数 1,逻辑读取 77 次,物理读取 2 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

除了eventlog之外,其他的表的IO大大减少,有木有?

强制使用hash join

经网友提示,在大的页数时,可以强制使用hash join来减少IO,而且经过尝试,可以通过建立两个子查询来避免使用临时表。经过调整,最终优化的SQL语句如下:

SELECT  *
	,ag.Name AS AgentServerName
	, l.UserName AS userName
FROM    ( 
	SELECT    a.*,ROW_NUMBER() OVER (ORDER BY AlarmTime DESC) AS RowNo
		, b.AddrName , b.Name AS MgrObjName
	FROM
		(SELECT    * 
			FROM      eventlog
			WHERE     AlarmTime>= &#39;2011-12-01 00:00:00&#39; AND AlarmTime<&#39;2014-12-26 23:59:59&#39;) AS a
		INNER HASH JOIN (
			SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName,t.Name AS MgrObjTypeName
			FROM dbo.mgrobj m
				INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
				INNER JOIN dbo.mgrobjtype t ON m.MgrObjTypeId=t.Id
			WHERE AddrId IN(&#39;02109000&#39;,……,&#39;02109002&#39;)
		) AS b ON a.MgrObjId=b.Id AND a.AgentBM=b.AgentBm
		
) tmp 
	LEFT JOIN agentserver AS ag ON tmp.AgentBm = ag.AgentBm
	LEFT JOIN eventdir AS e ON tmp.EventBm = e.Bm
	LEFT JOIN loginUser AS l ON tmp.cfmoper = l.loginGuid
WHERE tmp.RowNo BETWEEN 190001 AND 190020

在大的分页的时候,通过hash查询,不必扫描前面的页数,可以大大减少IO,但是,由于hash join是强制性的,所以使用的时候要注意,我这里应该是个特例。

查询分析器的提示:“警告: 由于使用了本地联接提示,联接次序得以强制实施。”

我们来看看对应情况下的IO:

表 &#39;eventlog&#39;。扫描计数 5,逻辑读取 5609 次,物理读取 34 次,预读 5636 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;Worktable&#39;。扫描计数 3,逻辑读取 375 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;Worktable&#39;。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobj&#39;。扫描计数 5,逻辑读取 24 次,物理读取 8 次,预读 40 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;mgrobjtype&#39;。扫描计数 1,逻辑读取 6 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;addrnode&#39;。扫描计数 3,逻辑读取 18 次,物理读取 6 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;loginuser&#39;。扫描计数 1,逻辑读取 60 次,物理读取 2 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;eventdir&#39;。扫描计数 1,逻辑读取 40 次,物理读取 0 次,预读 0 次,lob 逻辑读取 30 次,lob 物理读取 0 次,lob 预读 0 次。
表 &#39;agentserver&#39;。扫描计数 1,逻辑读取 1540 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

这次的IO表现非常的好,没有因为查询后面的页数增大而导致较大的IO,查询时间从没有使用hash join的50秒提升为只需12秒,查询时间的开销应该耗费了在hash查找上了。

再看看对应的查询计划,这个时候,主要是因为排序的开销较大。

我们再看看他的预估的和执行的区别,为什么会让排序占如此大的开销?

很明显,预估的时候只需对刷选的结果排序,但是实际执行是对前面所有的页数进行了排序,最终排序占了大部分的开销。那么,这种情况能破吗?请留下您的回复!

其他优化参考

在另外的群上讨论时,发现使用ROW_NUMBER分页查询到后面的页数会越来越慢的这个问题的确困扰了不少的人。

有的人提出,谁会这么无聊,把页数翻到几千页以后?一开始我也是这么想的,但是跟其他人交流之后,发现确实有这么一种场景,我们的软件提供了最后一页这个功能,结果……当然,一种方法就是在设计软件的时候,就去掉这个最后一页的功能;另外一种思路,就是查询页数过半之后,就反向查询,那么查询最后一页其实也就是查询第一页。

还有一些人提出,把查询出来的内容,放到一个临时表,这个临时表中的加入自增Id的索引,这样,可以通过辨别Id来进行快速刷选记录。这也是一种方法,我打算稍后尝试。但是这种方法也是存在问题的,就是无法做到通用,必须根据每个表进行临时表的构建,另外,在超大数据查询时,插入的记录过多,因为索引的存在也是会慢的,而且每次都这么做,估计CPU也挺吃紧。但是不管怎么样,这是一种思路。

你有什么好的建议?不妨把你的想法在评论中提出来,一起讨论讨论。

总结

现在,我们来总结下在这次优化过程中学习到什么内容:

  • 在SQLServer中,ROW_NUMBER的分页应该是最高效的了,而且兼容SQLServer2005以后的数据库
  • 通过“欺骗”查询引擎的小技巧,可以控制查询引擎部分的优化过程
  • ROW_NUMBER分页在大页数时存在性能问题,可以通过一些小技巧进行规避

    • 尽量通过cte利用索引
    • 把不参与where条件的表放到分页的cte外面
    • 如果参与where条件的表过多,可以考虑把不参与分页的表先做一个临时表,减少IO
    • 在较大页数的时候强制使用hash join可以减少io,从而获得很好的性能
  • 使用with(forceseek)可以强制查询因此进行索引查询

最后,感谢SQLServer群的高桑、宋桑、肖桑和其他群友的大力帮助,这个杜绝吹水的群非常的棒,让我这个程序猿学到了很多数据库的知识!

注:经网友提示,2015-01-07 09:15做以下更新

  • 可以在记录数超过10000条,则采用hash join强制进行hash连接,减少IO(感谢27楼riccc)
  • 去掉最先给定的结果中采用left join而不是inner join的连接——left join的结果相当于没有用上addrId in ()的条件(感谢32楼夏浩)

参考文章

  • 曲演杂坛--蛋疼的ROW_NUMBER函数
  • 为什么超长列表数据的翻页技术实现复杂

推荐阅读
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • 构建基于BERT的中文NL2SQL模型:一个简明的基准
    本文探讨了将自然语言转换为SQL语句(NL2SQL)的任务,这是人工智能领域中一项非常实用的研究方向。文章介绍了笔者在公司举办的首届中文NL2SQL挑战赛中的实践,该比赛提供了金融和通用领域的表格数据,并标注了对应的自然语言与SQL语句对,旨在训练准确的NL2SQL模型。 ... [详细]
  • 本文详细介绍了HTML中标签的使用方法和作用。通过具体示例,解释了如何利用标签为网页中的缩写和简称提供完整解释,并探讨了其在提高可读性和搜索引擎优化方面的优势。 ... [详细]
  • 数据库内核开发入门 | 搭建研发环境的初步指南
    本课程将带你从零开始,逐步掌握数据库内核开发的基础知识和实践技能,重点介绍如何搭建OceanBase的开发环境。 ... [详细]
  • 本文深入探讨 MyBatis 中动态 SQL 的使用方法,包括 if/where、trim 自定义字符串截取规则、choose 分支选择、封装查询和修改条件的 where/set 标签、批量处理的 foreach 标签以及内置参数和 bind 的用法。 ... [详细]
  • 使用C#开发SQL Server存储过程的指南
    本文介绍如何利用C#在SQL Server中创建存储过程,涵盖背景、步骤和应用场景,旨在帮助开发者更好地理解和应用这一技术。 ... [详细]
  • 本文探讨了适用于Spring Boot应用程序的Web版SQL管理工具,这些工具不仅支持H2数据库,还能够处理MySQL和Oracle等主流数据库的表结构修改。 ... [详细]
  • 本文详细介绍了如何通过多种编程语言(如PHP、JSP)实现网站与MySQL数据库的连接,包括创建数据库、表的基本操作,以及数据的读取和写入方法。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • 在使用 DataGridView 时,如果在当前单元格中输入内容但光标未移开,点击保存按钮后,输入的内容可能无法保存。只有当光标离开单元格后,才能成功保存数据。本文将探讨如何通过调用 DataGridView 的内置方法解决此问题。 ... [详细]
  • 本文详细介绍了如何在 Linux 平台上安装和配置 PostgreSQL 数据库。通过访问官方资源并遵循特定的操作步骤,用户可以在不同发行版(如 Ubuntu 和 Red Hat)上顺利完成 PostgreSQL 的安装。 ... [详细]
  • 如何在PostgreSQL中查看数据表
    本文将指导您使用pgAdmin工具连接到PostgreSQL数据库,并展示如何浏览和查找其中的数据表。通过简单的步骤,您可以轻松访问所需的表结构和数据。 ... [详细]
  • 利用存储过程构建年度日历表的详细指南
    本文将介绍如何使用SQL存储过程创建一个完整的年度日历表。通过实例演示,帮助读者掌握存储过程的应用技巧,并提供详细的代码解析和执行步骤。 ... [详细]
author-avatar
咪了眼的小迷糊
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有