SQL Server 致程序员(容易忽略的错误)

概述 html

由于天天须要审核程序员发布的SQL语句,因此收集了一些程序员的一些常见问题,还有一些平时收集的其它一些问题,这也是不少人容易忽视的问题,在之后收集到的问题会补充在文章末尾,欢迎关注,因为收集的问题不少是针对于生产数据,测试且数据量比较大,这里就不把数据共享出来了,你们理解意思就行。 程序员

 

步骤 sql

大小写

大写T-SQL 语言的全部关键字都使用大写,规范要求。 数据库

使用“;”

使用“;”做为 Transact-SQL 语句终止符。虽然分号不是必需的,但使用它是一种好的习惯,对于合并操做MERGE语句的末尾就必需要加上“;” 缓存

(cte表表达式除外) 安全

数据类型

避免使用ntext、text 和 image 数据类型,用 nvarchar(max)、varchar(max) 和 varbinary(max)替代 服务器

后续版本会取消ntext、text 和 image 该三种类型 网络

 

查询条件不要使用计算列

复制代码
例如year(createdate)=2014,使用createdate>=20140101and createdate<=20141231’来取代。 
复制代码
IF OBJECT_ID('News','U') IS NOT NULL DROP TABLE News GO CREATE TABLE News (ID INT NOT NULL PRIMARY KEY IDENTITY(1,1), NAME NVARCHAR(100) NOT NULL, Createdate DATETIME NOT NULL ) GO CREATE NONCLUSTERED INDEX [IX1_News] ON [dbo].[News] ( [Createdate] ASC ) INCLUDE ( [NAME]) WITH (STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO GO INSERT INTO News(NAME,Createdate) VALUES( '新闻','2014-08-20 00:00:00'),( '新闻','2014-08-20 00:00:00'),( '新闻','2014-08-20 00:00:00'),( '新闻','2014-08-20 00:00:00')
复制代码

---使用计算列查询(走的是索引扫描) ide

SELECT ID,NAME,Createdate FROM News WHERE YEAR(Createdate)=2014

---不使用计算列(走的是索引查找) 性能

SELECT ID,NAME,Createdate FROM News WHERE CreateDate>='2014-01-01 00:00:00' and CreateDate<'2015-01-01 00:00:00'

对比两个查询显然绝大部分状况下走索引查找的查询性能要高于走索引扫描,特别是查询的数据库不是很是大的状况下,索引查找的消耗时间要远远少于索引扫描的时间,若是想详细了解索引的体系结构能够查看了我前面写的几篇关于汇集、非汇集、堆的索引体系机构的文章。

复制代码

 

请参看:http://www.cnblogs.com/chenmh/p/3780221.html

 请参看:http://www.cnblogs.com/chenmh/p/3782397.html

 

建表时字段不容许为null


      发现不少人在建表的时候不会注意这一点,在接下来的工做中当你须要查询数据的时候你每每须要在WHERE条件中多加一个判断条件IS NOT NULL,这样的一个条件不只仅增长了额外的开销,并且对查询的性能产生很大的影响,有可能就由于多了这个查询条件致使你的查询变的很是的慢;还有一个比较重要的问题就是容许为空的数据可能会致使你的查询结果出现不许确的问题,接下来咱们就举个例子讨论一下。

复制代码
T-SQL是三值逻辑(true,flase,unknown) IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL); ----查询没有订单的顾客 SELECT Customerid FROM DBO.Customer WHERE Customerid NOT IN(SELECT custid FROM OrderS); ---分析为何查询结果没有数据 /* 由于true,flase,unknown都是真值 由于not in 是须要结果中返回flase值,not true=flase,not flase=flase,not unknown=unknown 由于null值是unknown因此not unknownn没法判断结果是什么值因此不能返回数据 */ --能够将查询语句修改成 SELECT Customerid FROM DBO.Customer WHERE Customerid NOT IN(SELECT custid FROM OrderS WHERE custid is not null); --或者使用EXISTS,由于EXISTS是二值逻辑只有(true,flase)因此不存在未知。 SELECT Customerid FROM DBO.Customer A WHERE NOT EXISTS(SELECT custid FROM OrderS WHERE OrderS.custid=A.Customerid ); ---in查询能够返回值,由于in是true,子查询true,flase,unknown都是真值因此能够返回子查询的true SELECT Customerid FROM DBO.Customer WHERE Customerid IN(SELECT custid FROM OrderS);
复制代码

 

分组统计时避免使用count(*)

复制代码
IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL); 例如:须要统计每个顾客的订单数量 ---若是使用count(*) SELECT Customerid,COUNT(*) FROM Customer TA LEFT JOIN OrderS TB ON TA.Customerid=TB.custid GROUP BY Customerid ;

实际状况customerid=3是没有订单的,数量应该是0,可是结果是1

----正确的方法是使用count(custid) SELECT Customerid,COUNT(custid) FROM Customer TA LEFT JOIN OrderS TB ON TA.Customerid=TB.custid GROUP BY Customerid;

 

复制代码

 

子查询的表加上表别名

复制代码
IF OBJECT_ID('DBO.Customer','U') IS NOT NULL DROP TABLE DBO.Customer GO CREATE TABLE DBO.Customer (Customerid int not null ); GO IF OBJECT_ID('DBO.OrderS','U') IS NOT NULL DROP TABLE DBO.OrderS GO CREATE TABLE DBO.OrderS (Orderid int not null, custid int); GO INSERT INTO Customer VALUES(1),(2),(3); INSERT INTO OrderS VALUES(1,1),(2,2),(3,NULL);
复制代码

你们发现下面语句有没有什么问题,查询结果是怎样呢?

SELECT Customerid FROM Customer WHERE Customerid IN(SELECT Customerid FROM OrderS WHERE Orderid=2 );


正确查询结果下查询出的结果是没有customerid为3的值

为何结果会这样呢?

你们仔细看应该会发现子查询的orders表中没有Customerid字段,因此SQL取的是Customer表的Customerid值做为相关子查询的匹配字段。

因此咱们应该给子查询加上表别名,若是加上表别名,若是字段错误的话会有错误标示

 正确的写法:

SELECT Customerid FROM Customer WHERE Customerid IN(SELECT tb.custid FROM OrderS tb WHERE Orderid=2 );

创建自增列时单独再给自增列添加惟一约束

复制代码
复制代码
USE tempdb CREATE TABLE TEST (ID INT NOT NULL IDENTITY(1,1), orderdate date NOT NULL DEFAULT(CURRENT_TIMESTAMP), NAME NVARCHAR(30) NOT NULL, CONSTRAINT CK_TEST_NAME CHECK(NAME LIKE '[A-Za-z]%' ) ); GO INSERT INTO tempdb.DBO.TEST(NAME) VALUES('A中'),('a名'),('Aa'),('ab'),('AA'),('az'); ----4.插入报错后,自增值依旧增长 INSERT INTO tempdb.DBO.TEST(NAME) VALUES(''); GO SELECT IDENT_CURRENT('tempdb.DBO.TEST'); SELECT * FROM tempdb.DBO.TEST; ---插入正常的数据 INSERT INTO tempdb.DBO.TEST(NAME) VALUES('cc'); SELECT IDENT_CURRENT('tempdb.DBO.TEST') SELECT * FROM tempdb.DBO.TEST; ----5.显示插入自增值 SET IDENTITY_INSERT tempdb.DBO.TEST ON INSERT INTO tempdb.DBO.TEST(ID,NAME) VALUES(8,'A中'); SET IDENTITY_INSERT tempdb.DBO.TEST OFF ----会发现ID并非根据自增值排列的,并且根据插入的顺序排列的 SELECT IDENT_CURRENT('tempdb.DBO.TEST'); SELECT * FROM tempdb.DBO.TEST; ----6.插入重复的自增值 SET IDENTITY_INSERT tempdb.DBO.TEST ON INSERT INTO tempdb.DBO.TEST(ID,NAME) VALUES(8,'A中'); SET IDENTITY_INSERT tempdb.DBO.TEST OFF SELECT IDENT_CURRENT('tempdb.DBO.TEST') SELECT * FROM tempdb.DBO.TEST; ---因此若是要保证ID是惟一的,单单只设置自增值不行,须要给字段设置主键或者惟一约束 DROP TABLE tempdb.DBO.TEST;
复制代码

 

复制代码

 

查询时必定要制定字段查询

l  查询时必定不能使用”*”来代替字段来进行查询,不管你查询的字段有多少个,就算字段太多没法走索引也避免了解析”*”带来的额外消耗。

l  查询字段值列出想要的字段,避免出现多余的字段,字段越多查询开销越大并且可能会由于多列出了某个字段而引发查询不走索引。

建立测试数据库

复制代码
CREATE TABLE [Sales].[Customer]( [CustomerID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, [PersonID] [int] NULL, [StoreID] [int] NULL, [TerritoryID] [int] NULL, [AccountNumber] AS (isnull('AW'+[dbo].[ufnLeadingZeros]([CustomerID]),'')), [rowguid] [uniqueidentifier] ROWGUIDCOL NOT NULL, [ModifiedDate] [datetime] NOT NULL, CONSTRAINT [PK_Customer_CustomerID] PRIMARY KEY CLUSTERED ( [CustomerID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
复制代码

建立索引

复制代码
CREATE NONCLUSTERED INDEX [IX1_Customer] ON [Sales].[Customer] ( [PersonID] ASC ) INCLUDE ( [StoreID], [TerritoryID], [AccountNumber], [rowguid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO
复制代码

查询测试

复制代码
---使用SELECT * 查询 SET STATISTICS IO ON SET STATISTICS TIME ON SELECT * FROM [Sales].[Customer] WHERE PersonID=1; SET STATISTICS TIME OFF SET STATISTICS IO OFF
复制代码

因为建的索引‘IX1_Customer’没有包含ModifiedDate字段,因此须要经过键查找去汇集索引中获取该字段的值

复制代码
---列出须要的字段查询,由于字段不包含不须要的列,因此走索引 SET STATISTICS IO ON SET STATISTICS TIME ON SELECT CustomerID, [PersonID] ,[StoreID] ,[TerritoryID] ,[AccountNumber] ,[rowguid] FROM [Sales].[Customer] WHERE PersonID=1; SET STATISTICS TIME OFF SET STATISTICS IO OFF
复制代码

因为查询语句中没有对ModifiedDate字段进行查询,因此只走索引查找就能够查询到须要的数据,因此建议在查询语句中列出你须要的字段而不是为了方便用*来查询全部的字段,若是真的

须要查询全部的字段也一样建议把全部的字段列出来取代‘*’。

 

使用存储过程的好处

  1. 减小网络通讯量。调用一个行数很少的存储过程与直接调用SQL语句的网络通讯量可能不会有很大的差异,但是若是存储过程包含上百行SQL语句,那么其性能绝对比一条一条的调用SQL语句要高得多。
  2. 执行速度更快。有两个缘由:首先,在存储过程建立的时候,数据库已经对其进行了一次解析和优化。其次,存储过程一旦执行,在内存中就会保留一份这个存储过程缓存计划,这样下次再执行一样的存储过程时,能够从内存中直接调用。
  3. 更强的适应性:因为存储过程对数据库的访问是经过存储过程来进行的,所以数据库开发人员能够在不改动存储过程接口的状况下对数据库进行任何改动,而这些改动不会对应用程序形成影响。
  4. 布式工做:应用程序和数据库的编码工做能够分别独立进行,而不会相互压制。
  5. 更好的封装移植性。
  6. 安全性,它们能够防止某些类型的 SQL 插入攻击。
复制代码
PROCEDURE [dbo].[SPSalesPerson] (@option varchar(50)) AS BEGIN SET NOCOUNT ON IF @option='select' BEGIN SELECT [DatabaseLogID] ,[PostTime] ,[DatabaseUser] ,[Event] ,[Schema] ,[Object] ,[TSQL] ,[XmlEvent] FROM [dbo].[DatabaseLog] END IF @option='SalesPerson' BEGIN SELECT [BusinessEntityID] ,[TerritoryID] ,[SalesQuota] ,[Bonus] ,[CommissionPct] ,[SalesYTD] ,[SalesLastYear] ,[rowguid] ,[ModifiedDate] FROM [Sales].[SalesPerson] WHERE BusinessEntityID<300 END SET NOCOUNT OFF END
复制代码
复制代码
EXEC SPSalesPerson @option='select' EXEC SPSalesPerson @option='SalesPerson' DBCC FREEPROCCACHE----清空缓存 ---测试两个查询是否都走了缓存计划 SELECT usecounts,size_in_bytes,cacheobjtype,objtype,TEXT FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st; --执行计划在第一次执行SQL语句时产生,缓存在内存中,这个缓存的计划一直可用,直到 SQL Server 从新启动,或直到它因为使用率较低而溢出内存。  默认状况下,存储过程将返回过程当中每一个语句影响的行数。若是不须要在应用程序中使用该信息(大多数应用程序并不须要),请在存储过程当中使用 SET NOCOUNT ON 语句以终止该行为。根据存储过程当中包含的影响行的语句的数量,这将删除客户端和服务器之间的一个或多个往返过程。尽管这不是大问题,但它能够为高流量应用程序的性能产生负面影响。
复制代码

判断一条查询是否有值

复制代码
--如下四个查询都是判断链接查询无记录时所作的操做 ---性能最差消耗0.8秒 SET STATISTICS IO ON SET STATISTICS TIME ON DECLARE @UserType INT ,@Status INT SELECT @UserType=COUNT(c.Id) FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000' IF(@UserType=0) BEGIN SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF go ----性能较好消耗0.08秒  SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT c.Id FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000') BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF go ----性能较好消耗0.08秒  SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT top 1 c.id FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13400000000' ORDER BY NEWID() ) BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF GO ---性能和上面的同样0.08秒 SET STATISTICS IO ON SET STATISTICS TIME ON IF NOT EXISTS(SELECT 1 FROM Customerfo t INNER JOIN Customer c ON c.Id=t.CustomerId WHERE c.customerTel='13410700660' ) BEGIN DECLARE @Status int SET @Status = 2 PRINT @Status END SET STATISTICS TIME OFF SET STATISTICS IO OFF 这里说一下SELECT 1,以前由于有程序员误认为查询SELECT 1不管查询的数据有多少只返回一个1,其实不是这样的,和查询字段是同样的意思只是有多少记录就返回多少个1,1也不是查询的第一个字段。
复制代码

 理解TRUNCATE和DELETE的区别

复制代码
---建立表Table1 IF OBJECT_ID('Table1','U') IS NOT NULL DROP TABLE Table1 GO CREATE TABLE Table1 (ID INT NOT NULL, FOID INT NOT NULL) GO --插入测试数据 INSERT INTO Table1 VALUES(1,101),(2,102),(3,103),(4,104) GO ---建立表Table2 IF OBJECT_ID('Table2','U') IS NOT NULL DROP TABLE Table2 GO CREATE TABLE Table2 ( FOID INT NOT NULL) GO
--插入测试数据
INSERT INTO Table2 VALUES(101),(102),(103),(104) GO SELECT * FROM Table1 GO SELECT * FROM Table2 GO 
在Table1表中建立触发器,当表中的数据被删除时同时删除Table2表中对应的FOID
复制代码
CREATE TRIGGER TG_Table1 ON Table1 AFTER DELETE AS BEGIN DELETE FROM TA FROM Table2 TA INNER JOIN deleted TB ON TA.FOID=TB.FOID END GO
复制代码
复制代码
---测试DELETE删除操做 DELETE FROM Table1 WHERE ID=1 GO ---执行触发器成功,Table2表中的FOID=101的数据也被删除 SELECT * FROM Table1 GO SELECT * FROM Table2
复制代码
复制代码
---测试TRUNCATE删除操做 TRUNCATE TABLE Table1 GO ---Table2中的数据没有被删除 SELECT * FROM Table1 GO SELECT * FROM Table2
复制代码

复制代码
复制代码
---查看TRUNCATE和DELETE的日志记录状况 CHECKPOINT GO SELECT * FROM fn_dblog(NULL,NULL) GO DELETE FROM Table2 WHERE FOID=102 GO SELECT * FROM fn_dblog(NULL,NULL)
复制代码

在第四行记录有一个lop_delete_rows,lcx_heap的删除操做日志记录
复制代码
----TRUNCATE日志记录 CHECKPOINT GO SELECT * FROM fn_dblog(NULL,NULL) GO TRUNCATE TABLE Table2 GO SELECT * FROM fn_dblog(NULL,NULL) GO
复制代码

 TRUNCATE操做没有记录删除日志操做

主要的缘由是由于TRUNCATE操做不会激活触发器,由于TRUNCATE操做不会记录各行的日志删除操做,因此当你须要删除一张表的数据时你须要考虑是否应该若有记录日志删除操做,而不是根据我的的习惯来操做。

 

事务的理解

复制代码
---建立表Table1 IF OBJECT_ID('Table1','U') IS NOT NULL DROP TABLE Table1 GO CREATE TABLE Table1 (ID INT NOT NULL PRIMARY KEY, Age INT NOT NULL CHECK(Age>10 AND Age<50)); GO ---建立表Table2 IF OBJECT_ID('Table2','U') IS NOT NULL DROP TABLE Table2 GO CREATE TABLE Table2 ( ID INT NOT NULL) GO
复制代码

1.简单的事务提交

复制代码
BEGIN TRANSACTION INSERT INTO Table1(ID,Age) VALUES(1,20) INSERT INTO Table1(ID,Age) VALUES(2,5) INSERT INTO Table1(ID,Age) VALUES(2,20) INSERT INTO Table1(ID,Age) VALUES(3,20) COMMIT TRANSACTION GO ---第二条记录没有执行成功,其余的都执行成功 SELECT * FROM Table1
因此并非事务中的任意一条语句报错整个事务都会回滚,其它的可执行成功的语句依然会执行成功并提交。
复制代码

2.TRY...CATCH

复制代码
DELETE FROM Table1 BEGIN TRY BEGIN TRANSACTION INSERT INTO Table1(ID,Age) VALUES(1,20) INSERT INTO Table1(ID,Age) VALUES(2,20) INSERT INTO Table1(ID,Age) VALUES(3,20) INSERT INTO Table3 VALUES(1) COMMIT TRANSACTION END TRY BEGIN CATCH ROLLBACK TRANSACTION END CATCH ----从新打开一个回话执行查询,发现因为存在对象出错BEGIN CATCH并无收到执行报错,且事务一直处于打开状态,没有被提交,也没有执行回滚。 SELECT * FROM Table1 ---若是事务已经提交查询XACT_STATE()的状态值是0,或者执行DBCC OPENTRAN SELECT XACT_STATE() DBCC OPENTRAN ---手动执行提交或者回滚操做 ROLLBACK TRANSACTION 
复制代码
TRY...CATCH不会返回对象错误或者字段错误等类型的错误

想详细了解TRY...CATCH请参考http://www.cnblogs.com/chenmh/articles/4012506.html

 

3.打开XACT_ABORT

复制代码
SET XACT_ABORT ON BEGIN TRANSACTION INSERT INTO Table1(ID,Age) VALUES(1,20) INSERT INTO Table1(ID,Age) VALUES(2,20) INSERT INTO Table1(ID,Age) VALUES(3,20) INSERT INTO Table3 VALUES(1) COMMIT TRANSACTION SET XACT_ABORT OFF ---事务所有执行回滚操做(对象table3是不存在报错,可是也回滚全部的提交,跟上面的TRY...CATCH的区别) SELECT * FROM Table1 
复制代码

复制代码
---查询是否有打开事务 SELECT XACT_STATE() DBCC OPENTRAN
未查询到有打开事务

当 SET XACT_ABORT 为 ON 时,若是执行 Transact-SQL 语句产生运行时错误,则整个事务将终止并回滚。

当 SET XACT_ABORT 为 OFF 时,有时只回滚产生错误的 Transact-SQL 语句,而事务将继续进行处理。若是错误很严重,那么即便 SET XACT_ABORT 为 OFF,也可能回滚整个事务。OFF 是默认设置。

编译错误(如语法错误)不受 SET XACT_ABORT 的影响。

复制代码

      因此咱们应该根据本身的需求选择正确的事务。

    

修改字段NOT NULL的过程

复制代码
在Address表中的有一个Address字段,该字段容许为NULL,如今须要将其修改成NOT NULL. BEGIN TRANSACTION SET QUOTED_IDENTIFIER ON SET ARITHABORT ON SET NUMERIC_ROUNDABORT OFF SET CONCAT_NULL_YIELDS_NULL ON SET ANSI_NULLS ON SET ANSI_PADDING ON SET ANSI_WARNINGS ON COMMIT BEGIN TRANSACTION GO CREATE TABLE dbo.Tmp_Address ( ID int NOT NULL, Address nvarchar(MAX) NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE dbo.Tmp_Address SET (LOCK_ESCALATION = TABLE) GO IF EXISTS(SELECT * FROM dbo.Address) EXEC('INSERT INTO dbo.Tmp_Address (ID, Address) SELECT ID, Address FROM dbo.Address WITH (HOLDLOCK TABLOCKX)') GO DROP TABLE dbo.Address GO EXECUTE sp_rename N'dbo.Tmp_Address', N'Address', 'OBJECT' GO COMMIT ---从上面就是一个重置字段为非空的过程,从上面的语句咱们能够看到首先要建立一张临时表在临时表中Address字段建成了NOT NULL,而后将原表中的数据插入到临时表当中,最后修改表名,你们能够想一下若是我要修改的表有几千万数据,那这个过程该多么长并且内存一会儿就会增长不少,因此你们建表的时候就要养成设字段为NOT NULL --当你要向现有的表中增长一个字段的时候你也要不容许为NULL,能够用默认值替代空 Alter Table Address Add MemberType smallint Not Null Default (1)
复制代码

 

 

总结

后面收集到相似的问题会补充在文章的末尾,文章持续更新中....,欢迎关注讨论。

  

若是以为文章对你们有所帮助,麻烦给个推荐,谢谢!!

 

备注:

    做者:pursuer.chen

    博客:http://www.cnblogs.com/chenmh

本站点全部文章都是原创,欢迎你们转载;但转载时必须注明文章来源,且在文章开头明显处给明连接,不然保留追究责任的权利。

《欢迎交流讨论》

相关文章
相关标签/搜索