带您理解SQLSERVER是如何执行一个查询的

带您理解SQLSERVER是如何执行一个查询的

连接方式和请求

如果你是一个开发者,并且你的程序使用SQLSERVER来做数据库的话

你会想知道当你用你的程序执行一个查询的时候实际发生了什么事情

我希望这篇文章能够帮你写出更好的数据库应用程序和帮你更深入了解遇到的数据库性能问题

SQLSERVER是一个C/S模型的平台。唯一和数据库交互的方式只有发送包含数据库命令的请求到数据库服务器端。

客户端和数据库通信的协议使用一种叫做TDS的协议(Tabular Data Sream)

园子里的文章:

如果你用微软的Network Monitor工具来抓取SQL Server和客户端之间的网络包

你会看到使用的是TDS协议

在Description那一列

TDS:Response,Version=7.1......

TDS:SQLBatch,Version=7.1.......

那四个SSL连接是客户端登录SQLSERVER前做的加密连接(这里不管你有没有用SSL加密数据传输,SQLSERVER都会在登录前加密

用户发过来的用户名和密码,而登录了之后才使用您配置的SSL证书来加密客户端和SQLSERVER往来的数据)

SQLSERVER都会加密客户端发过来的用户名和密码(使用SQL验证不是使用Windows验证)

大家可以留意一下SQL ERRORLOG里在SQLSERVER启动的时候的日志

会看到一句:A self-generated certificate was sccessfully loaded for encryption

默认情况下SQL Server会自动生成一个证书并使用这个证书来对客户端登录SQLSERVER的时候的连接做SSL加密

登录了SQLSERVER之后,就不会对连接/所传输的数据做加密了

而且SQL Server自动生成的证书。每次SQL Server启动时,它自动生成的证书都是不一样的

MSDN是这样描述的:Tabular Data Stream协议,应用程序能够使用下面的几种已经实现了TDS协议的驱动程序里的其中一种

驱动程序来连接数据库,包括:

the CLR managed SqlClient

OleDB

ODBC

JDBC

PHP Driver for SQL Server

开源的 FreeTDS 实现

当你的应用程序命令数据库如何去做的时候会通过TDS协议向数据库发送一个请求

发送的请求本身能携带下面几种格式的信息

(1)批处理请求

这种请求类型只会包含一个需要执行的批处理TSQL文本。这种类型的请求不能带有参数,不过,TSQL批处理脚本里

能包含本地变量的定义。这种类型的请求一般都是使用SQLCLIENT驱动程序发送的,

当你使用SqlCommand 对象调用下面语句的任何一个的时候,并且没有传入任何参数

SqlCommand.ExecuteReader()

SqlCommand.ExecuteNonQuery()

SqlCommand.ExecuteScalar()

SqlCommand.ExecuteXmlReader()

当你用SQL PROFILER监视你会看到一个:SQL:BatchStarting 事件类型

(2)远程过程调用请求

这个请求类型包含带有若干个参数的存储过程。

当你用SQL PROFILER监视你会看到一个:RPC:Starting 事件类型

(3) Bulk Load大容量装载请求

大容量装载请求是一种特别的使用bulk insert操作符的请求,

例如使用

BCP.EXE工具(我们常说的BCP命令)

bulk insert语句

IRowsetFastLoad OleDB 接口

C#里面的SqlBulkcopy类

大容量装载请求跟其他类型的请求是不同的,

因为请求通过TDS协议传送到SQLSERVER的时候,还未传送完毕,SQLSERVER就开始执行请求所要做的操作了

(一般来说,整个请求的数据包全部发送到SQLSERVER那里,SQLSERVER认为是完整的数据包才开始执行请求)

但是大容量装载请求不一样,数据包里包含有大量的数据,这些数据是附着在请求里的,如果要把整个请求传送完毕

SQLSERVER才开始执行请求,那不知道要等到何年何月了???

这样允许SQLSERVER开始执行请求,并且开始消费掉数据流中所插入的数据

下面是比较久以前的一张图片,大家可以参考一下,图片内容对于现在的SQLSERVER不一定正确

连接模块(模块化)


任务(Tasks)和工作者(Workers)

在一个完整的TDS请求到达SQLSERVER数据库引擎的时候,SQLSERVER会创建一个任务(task)去处理请求

想查询当前SQLSERVER里面的所有请求,可以使用sys.dm_exec_requests 这个DMV视图

任务(Tasks)

上面提到的任务会被创建用来处理请求,一直到请求处理完毕为止。

例如:如果请求是一个批处理请求类型的请求,任务(Tasks)会执行整个SQL批处理,不会只负责执行SQL批处理里的单独一条SQL语句

在SQL批处理里的单独的一条SQL语句不会创建一个新的任务(Tasks)。

当然,在SQL批处理里的单独的一条SQL语句有可能会并行执行(通常使用MAXDOP,或Degree Of Parallelism)

在这种情况下,任务(Tasks)会再生新的子任务(sub-Tasks)去并行执行这个单独的SQL语句。

如果请求返回了批处理所要的完整的结果集,并且结果集已经被客户端从SQLSERVER的结果集缓存里取走

并且你在C#代码里dispose 了SqlDataReader,你会在sys.dm_os_tasks这个DMV视图里看到你的请求

所用的任务(Tasks)。

当一个新的请求到达SQLSERVER服务器端的时候,并且这时候任务(Tasks)已经被创建出来去处理这个请求了

如果这时候任务(Tasks)处于挂起(PENDING)状态,现阶段SQLSERVER还未知道这个请求的实际内容,

那么,被创建出来的任务必须首先去执行这个请求,并且数据库引擎也要分配一个工作者(Worker)去处理这个请求

工作者(Workers)

工作者(Workers)是SQLSERVER线程池里的一些线程,一些 工作者(Workers)/工作线程在SQLSERVER

初始化的时候就被创建出来,而另一些工作者(Workers)会根据需求而创建,当创建的数量达到max workerthreads

这个配置值的时候就不能再创建了,下图显示为0,他并不是说可以创建无限的工作者(Workers)

我的机器的配置是酷睿i3,双核四线程,那么,如果max worker threads配置为0

最大的工作者(Workers)数目可以达到256个

实际上,只有工作者(Workers)才真正执行SQL代码。

工作者(Workers)每时每刻都等待那些已经传送进去SQLSERVER的请求的任务(Tasks)

从被挂起(PENDING)状态成为可以运行的状态,每个工作者(Workers)只会处理和执行一个任务(Tasks)

这时候,工作者(Workers)会一直处于工作状态,并一直被占用,直到他的工作完成为止(taskfinishes)

如果当前没有可用的工作者(Workers)供给正在处于挂起状态的任务(Tasks)使用的话,那么这个任务(Tasks)

只能一直等待直到那些已经在执行/运行的任务(Tasks)执行完毕,另外,工作者(Workers)在处理完一个

任务(Tasks)之后也会继续处理下一个处于挂起状态的任务(Tasks)。

对于一个SQL批处理请求,工作者(Workers)会处理那个携带着那个SQL批处理的任务(Tasks)

并且会执行SQL批处理里面的每条SQL语句。

有人就会问了:一个SQL批处理里的SQL语句不就是并行执行吗?

(=> 请求request =>任务 task =>工作者 worker),一个批处理请求进来,多个工作者去处理这个批处理请求里的每条SQL语句,

这显然就是SQLSERVER的并发处理SQL语句嘛

很多人都会有这个想法,实际上是错误的,实际上这些SQL语句也是串行执行的,这些SQL语句的执行只能由

一个单独的线程(工作者 worker)来执行,线程(工作者 worker)在执行完一个SQL语句之后才能执行下一个SQL语句,

当SQL批处理内部的SQL语句使用了并行提示MAXDOP>1来执行SQL语句 ,这会造成创建子任务(sub-tasks),

每个子任务(sub-tasks)也是通过上面所说的那个循环去执行的:任务创建出来之后会处于挂起状态,

其他的(工作者 worker)必须去处理这个子任务(sub-tasks)

你会在sys.dm_os_workers这个DMV视图里看到SQLSERVER当前的工作者 worker列表和他们的当前状态


解释(Parsing)和编译(Compilation)

一旦一个任务(task)开始执行一个请求,第一件要做的事情就是:去理解请求里面的内容

在这一步,SQLSERVER的行为更像一个代码解释的虚拟机(类似于JVM):在请求(request)里面的TSQL代码将会被逐一解释

并且会生成一棵抽象语法树去处理这个请求。整个批处理请求会被解释和编译,如果在这一步发生错误,

SQLSERVER会给出编译/解释错误的提示,这个请求也会被终止不会执行,任务(task)和工作者(worker)都会被释放,

释放出来的工作者(worker)会继续处理下一个被挂起的任务(task)。

SQL语言和TSQL(SQLSERVER里叫TSQL,ORACLE里叫PLSQL)语言是一种高等的描述性语言

当一个SQL语句很复杂的时候,试想一下,一个SELECT 语句伴随着多个JOIN

1 USE [GPOSDB]

2 GO

3 SELECT * FROM [dbo].[CT_Append] AS a

4 INNER JOIN

5 [dbo].[CT_FuelingData] AS b

6 ON a.[VC_A_CardNO]=b.[VC_FD_Cardno]

7 INNER JOIN

8 [dbo].[CT_Dis_FuelingData] AS d

9 ON a.[VC_A_CardNO]=d.[VC_FD_Cardno]

10 INNER JOIN

11 [dbo].[CT_InhouseCard] AS e

12 ON e.[VC_IC_CardNO]=d.[VC_FD_Cardno]

13 INNER JOIN

14 [dbo].[CT_OuterCard] AS f

15 ON f.[VC_OC_CardNO]=a.[VC_A_CardNO]

编译好的TSQL批处理不会产生可执行代码(executable code,类似可执行的二进制的exe文件),

这里更像本地CPU指令,甚至于类似C#的CLI指令或者JAVA的JVM bytecode

不过,这里会产生用于访问表数据的执行计划(query plans),这些执行计划描述了如何去访问表和索引,

如何去搜索和定位表里面的行数据,如何根据SQL批处理里的SQL语句去做数据操作。

例如:一个执行计划会描述一种数据访问路径-》访问在t表上的索引idx1,定位到关键字为‘k’的那行记录,

最后返回a列和b列这两列数据。

另外:开发者通常都会犯一个普遍的错误

在一个TSQL语句里写很多的条件选择,通常这些条件选择都会用在带有OR 的where子句里

例如:cola=@parameterOR @parameter IS NULL

对于开发者一定要避免这种情况。

这个时候,编译一定要得出一种通用的执行计划,无论任何参数代入到这个执行计划里都能得出最优的结果

在TSQL里的参数化(Dynamic Search Conditions)

例如下面SQL语句:

1 SET STATISTICSPROFILE ON

2 GO

3 INSERT INTO [dbo].[SystemPara] ( [ParaValue], [Name], [Description] )

4 VALUES ( '2', -- ParaValue -varchar(50)

5 '3', -- Name - varchar(50)

6 '4' -- Description -varchar(50)

7 )

当你打开SET STATISTICS PROFILE ON开关的时候,你会在Argument列和DefinedValues列看到

SQLSERVER会将输入的值2,3,4赋值到Expr1004,Expr1005,Expr1006这三个变量里

并做一些类型转换,Expr1004=CONVERT_IMPLICIT(VARCHAR(50),[@1],0)

2这个值会代入都@1变量里,然后通过类型转换赋值给Expr1004

recordno这一列也是,通过getidentity((277576027),(14),null)函数获得自增值

然后赋值给Expr1003

那么,在SQLSERVER的执行计划里,大家可以想象成如下样子

1 INSERT INTO [dbo].[SystemPara] ([RecordNo], [ParaValue],[Name], [Description] )

2 VALUES(Expr1003,Expr1004,Expr1005,Expr1006)

将实际的值先赋值给@1,@2,@3,@4 再通过类型转换赋值给Expr1003,Expr1004,Expr1005,Expr1006

Expr1003=类型转换(@1)

Expr1004=类型转换(@2)

Expr1005=类型转换(@3)

Expr1006=类型转换(@4)

为什麽SQLSERVER不直接使用下面的执行计划呢?

1 INSERT INTO [dbo].[SystemPara] ([RecordNo], [ParaValue],[Name], [Description] )

2 VALUES(1,2,3,4)

还要类型转换,参数代入这麽麻烦,SQLSERVER不是有病吗???

这里涉及到执行计划重用,如果使用上面的执行计划,编译的时间是很快,但是

如果我插入的值是:9,8,6,7

1 INSERT INTO [dbo].[SystemPara] ([RecordNo], [ParaValue],[Name], [Description] )

2 VALUES(9,8,6,7)

SQLSERVER不能重用上次的执行计划,又要重新生成执行计划,您说这样的效率。。。。。。。。


优化(Optimization)

刚才说到选择一种数据访问路径(执行计划),现在继续说一个请求(request)的生命周期的下一步:优化

在SQLSERVER里面,优化意味着从多个选择条件中选择最佳的数据访问路径。

考虑一下,如果你有一个简单的涉及到两个表的join查询,每个表都有额外的索引,

这里就有4种可选的执行方案,去访问表中的数据

因为有这麽多的可选方案,查询复杂度已经比较高了,如果这时候表中的索引继续增多的话,查询复杂度有可能以指数的方式增长

再加上JOIN联接本来就有三种联接方式:nested loops join、merge join、hash join

可想而知,优化这个名词在SQLSERVER里是多么重要,SQLSERVER使用一个查询优化器来预估这中间要消耗的时间,IO,CPU

查询优化器会考虑各种执行方案,SQLSERVER会尽力基于每种执行方案的开销去作出评估,然后尽可能选择一个开销最低的

执行方案。SQLSERVER首先会计算在现有的表数据量下各种执行方案各自需要多少的开销。为了选出一个开销最低的执行方案,

SQLSERVER需要知道做联接的每张表的数据量和表里面各个字段的数据的分布,这就需要靠统计信息

因为统计信息本来就是用来统计这些数据的。另外一个要考虑的因素就是,每种执行方案所需要的CPU消耗和内存消耗

综合以上各种因素,SQLSRVER会在每种执行方案里算出一个cost值

SQLSERVER会在这些执行方案里选出一个cost值最低的执行方案作为执行计划执行

大家看一下,SQLSERVER要对上面各种因素进行考虑,这里考虑是需要时间的,所以为什麽SQLSERVER

需要将执行计划缓存到内存里以便将来继续使用这个执行计划,就是为了节省编译时间

将来同样的请求进入到SQLSERVER,并且这些请求能够在CACHE里找到一个已经编译了和优化了的执行计划

他们就能跳过查询优化器的优化阶段

这里一定要注意:同样的请求进来SQLSERVER的时候,无论CACHE里有没有可以重用的执行计划,SQLSERVER都需要

对请求里的SQL语句进行解析,所以我上面才说:就是为了节省编译时间 而不是 就是为了节省解析/编译时间

解释和编译模块(模块化)


执行(Execution)

一旦查询优化器选择了一个执行计划,请求(request)就可以开始执行了。执行计划会被翻译成为一棵实际的执行树

每个树节点都是一个操作符,所有操作符都会实现一个有3个方法的抽象接口,分别是open(), next(), close()

如果阁下是C#程序员或者是JAVA程序员,一定不难理解什么是接口,什么是方法,什么是抽象接口

MSDN里有相关的资料:Showplan 逻辑运算符和物理运算符参考

查询计划是由物理运算符组成的一个树(执行树)

逻辑运算符 逻辑运算符描述了用于处理语句的关系代数操作。 换言之,逻辑运算符从概念上描述了需要执行哪些操作。

物理运算符 物理运算符实施由逻辑运算符描述的操作。 每个物理运算符都是一个执行某项操作的对象或例程。 例如,某些物理运算符可访问表、索引或视图中的列或行。其他物理运算符执行其他操作,如计算、聚合、数据完整性检查或联接。 物理运算符具有与其关联的开销。 物理运算符初始化、收集数据,然后关闭。 具体来讲,物理运算符可以响应下列三种方法调用: Init():Init() 方法使物理运算符初始化自身并设置所有需要的数据结构。 尽管一个物理运算符通常只接收一次 Init() 调用,但也可以接收许多次调用。 GetNext():GetNext() 方法使物理运算符获得数据的第一行或后续行。 物理运算符可以不接收 GetNext() 调用,也可以接收许多次调用。 Close():Close() 方法使物理运算符执行某些清除操作,然后关闭。 一个物理运算符只接收一个 Close() 调用。 GetNext() 方法返回一个数据行,它的调用次数作为 ActualRows 显示在使用 SET STATISTICS PROFILE ON 或 SET STATISTICSXML ON 生成的显示计划输出中。 有关这些 SET 选项的详细信息,请参阅 SET STATISTICS PROFILE (Transact-SQL) 和 SETSTATISTICS XML (Transact-SQL)。

文中说的操作符实际上指的就是物理运算符:三个方法指的是open()=init(),next()=getnext(),close()=close()

每个物理运算符就是调用自己的三个方法

在SQLSERVER执行请求的过程中,执行树的根节点会不断循环的调用open(),然后重复调用next()直到返回false值

最后调用close()。树的根节点的运算符会依次调用他的子节点的同样的运算符,而子节点又会依次调用他的子节点的同样的运算符

一直调用下去。在树的叶子节点一般都会是读取表数据或表索引的物理运算符。而执行树的中间节点一般都是一些实现不同数据操作的运算符

例如:过滤表数据、join连接、对数据排序。那些使用并行的查询会使用一个特别的运算符叫做:ExchangeOprators(交换操作)

交换操作运算符在执行的过程中会使用多线程(tasks =>workers),调用每个线程去执行子树的执行计划,

然后聚合这些运算符的输出结果,在这个过程中会使用典型的(多生产者《-》一个消费者模式)。

关于Exchange Oprators(交换操作) 可以参考这篇文章:SQLServer 2000中的并行处理和执行计划中的位图运算符

我们使用 SET STATISTICS PROFILE ON 就可以看到执行树,下面是一些列的名称,更详细的就不说了,网上有很多资料

NodeId:树节点

Parent:父节点

PhysicalOp:物理运算符

LogicalOp:逻辑运算符

这种执行树的执行模型不单只应用于查询,插入,删除,更新的执行都是同样利用执行树来执行的

插入记录、删除记录、更新记录都会有相应的运算符

一个执行树没有子树的情况

一个执行树具有子树的情况

如果执行树具有子树,他的执行方式也是从子树的叶子节点开始执行,一直执行到树的根节点

特别要介绍一下,这些运算符也有停止-继续的行为特性,意思是说除非他们的子节点运算符已经吸收完所有的输入,他们才能产生输入

例如:排序运算符,排序运算符在最初调用next()函数的时候不会返回任何结果因为这时候他的子节点还没有读取完所有数据,

这时候需要停止执行next()函数(每个运算符创建出来就会不停调用next函数),直到他的子节点读取完所有数据他才能对这些数据

进行排序(继续调用next()函数),取出结果集并排序

如果数据已经缓存在内存里了,SQLSERVER就不需要去磁盘里取数据,直接在内存里取数据,内存里的这块空间,

SQLSERVER官方术语叫:Buffer pool

而在内存里缓存执行计划的这块空间,SQLSERVER官方术语叫:Plan Cache

执行模块(模块化)

结果(Results)

在执行完毕之后,SQLERVER会将结果集返回给客户端应用程序

当执行到执行树的根节点的时候,根节点通常负责将结果集写入到网络缓冲区(network buffers)

然后将这些结果集发送回客户端。一个完整的结果集还没有创建完毕,一部分的结果首先会存放到中间存储(内存或磁盘)

然后逐段逐段发送给客户端,例如一个SQL语句查询的结果需要返回10条记录,有3条记录已经生成好了,可以返回给客户端了

SQLSERVER首先将这3条记录放入中间存储(内存或磁盘),也可以叫网络缓冲区,等客户端来取走这3条记录,如此类推。

返回结果集给客户端的时候,SQLSERVER用的是网络流控制协议。

如果客户端没有积极地将这些结果集取走(例如调用SqlDataReader.Read())。最终会导致网络流控制组件不得不阻塞

结果集发送端并且会挂起查询的执行。

只有网络流控制组件协调和缓解了网络资源的需求(网络没有阻塞),查询才会恢复,并且继续生成结果集

不知道大家有没有遇到过等待类型:ASYNC_NETWORK_IO的等待

上图里,客户端二就要等待,在SQLSRVER里查询就会显示ASYNC_NETWORK_IO类型的等待

有趣的是,OUTPUT参数的返回,OUTPUT参数的值会被插入到返回给客户端的结果集的网络数据流中。

当请求完成的时候,OUTPUT参数值只能在查询执行的最后写到结果集中,这就是为什麽OUTPUT参数值

只有当所有的结果集都返回了才能检查OUTPUT参数的

查询执行过程中要赋予的内存(Query Execution Memory Grant)

一些运算符需要固定的内存去执行他们的工作。排序运算符为了进行排序需要内存去存储输入到排序运算符的数据

Hash join和hash聚合必须建立大型的hash表去执行他们的工作。执行计划知道那些未完成的运算符需要多少内存

根据运算符类型,预估的行记录,运算符必须要处理统计信息提供给他的表中的字段的大小。

那些在执行计划里的运算符所需要的总的内存我们通常称为内存赋予

试想一下,当非常多的并发查询被执行的时候,因为大量的昂贵的运算符(这些运算符一般都需要很多内存,所以称之为昂贵的)

需要请求内存,在同一时间里面他们能够用尽计算机的内存。

为了阻止这种情况的发生,SQLSERVER使用一种叫“资源信号量”的东西。这个东西能够确保正在执行的查询的总内存分配不会超过

当前计算机中的内存总和。当总的内存分配就快耗尽当前服务器里的可用内存的时候,正在执行的查询必须要等待那些就快执行完毕

的查询去释放他们拥有的内存。

您可以查询sys.dm_exec_query_memory_grants这个DMV视图来获取当前的内存分配(请求的内存,分配了的内存)

当一个查询必须要等待内存的赋予/分配,在SQL PROFILER里可以看到Execution Warnings 事件类型

Execution Warnings 事件类型指出了当SQL语句或者存储过程执行的过程中的内存分配警告

这个事件类型能够监视必须要等待一秒或更多内存的某些查询,或者获取内存失败的查询

在SQLPROFILER里,一些与内存有关的事件类型

Exchange Spill 事件类型

Sort Warnings 事件类型:排序的时候所需内存不足

Hash Warning 事件类型

相关语句

1 select * fromsys.dm_exec_query_resource_semaphores

2

3

4 select * from sys.dm_exec_query_memory_grants

1 SELECT [session_id],

2 [request_id],

3 [start_time],

4 [status],

5 [command],

6 [wait_type],

7 [text_size],

8 [language] ,

9 [transaction_isolation_level],

10 [row_count],

11 [granted_query_memory],

12 [executing_managed_code]

13 FROM sys.[dm_exec_requests]

我如何利用这些信息(How can I use all this information)

上面的信息有可能帮您解决performance troubleshooting problems(性能问题)

一旦您明白了您的客户端正在发送多个请求到SQLSERVER,SQLSERVER端正在创建多个任务(task)去处理

您发给他的请求,性能的谜题就可以很简单地解决了:很多时候,您的任务不是正在执行(正在占领CPU)就是处于正在等待

每次等待,SQLSERVER都会依靠内部等待统计信息去收集等待的信息(等待什么和等了多久)。

利用收集回来的统计信息去解决性能瓶颈是非常好的方法


附上两张完整的图

总结

文中好像遗漏了Scheduler

Scheduler

对于每个逻辑CPU,SQLSERVER会有一个scheduler与之对应,在SQL层面上代表CPU对象,

只有拿到scheduler所有权的worker才能在这个逻辑CPU上运行

原文发布于微信公众号 - 数据库SQL(SQLdba)

原文发表时间:2015-04-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jed的技术阶梯

Kafka 消费者旧版低级 API

Kafka 消费者总共有 3 种 API,新版 API、旧版高级 API、旧版低级 API,新版 API 是在 kafka 0.9 版本后增加的,推荐使用新版 ...

923
来自专栏农夫安全

代码审计之命令执行漏洞

环境:windows + apache + mysql + php (phpstudy) 由于是在Windows下进行的测试,所以和Linux下的测试会有所不...

2536
来自专栏Python

Flask快速入门,知识整理

一、Flask介绍(轻量级的框架,非常快速的就能把程序搭建起来)   Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服...

3209
来自专栏农夫安全

代码审计之命名执行漏洞

环境:windows + apache + mysql + php (phpstudy) 由于是在Windows下进行的测试,所以和Linux下的测试会有所不...

2806
来自专栏数据和云

数据库时间出现'0000/00/00',难道我穿越了?

前几天有个朋友遇到一个问题,在做日期类型数据的运算的时候出现了‘0000-00-00’的结果,不得其解。你是否遇到过同样的问题呢?这样一个并不存在的时间点,难道...

2946
来自专栏令仔很忙

EasyUI----DataGrid 导出 Excel

最近做的安防项目有个功能,需要把DataGrid中的数据导出,下面通过代码一步一步的介绍; 首先在js中写一个扩展类,主要的功能是把DataGrid中的数据...

1131
来自专栏架构之路

追源索骥:透过源码看懂Flink核心框架的执行流程

写在最前:因为这篇博客太长,所以我把它转成了带书签的pdf格式,看起来更方便一点。想要的童鞋可以到我的公众号“老白讲互联网”后台留言flink即可获取。

4463
来自专栏PPV课数据科学社区

适用于 PHP 开发人员的 Python 基础知识

您是一名 PHP 开发人员。您在过去 五年(或更长时间)中可能一直都编写应用程序,您已经将许多想像变成了可能 — 电子商务系统、简单内容管理系统、Twitte...

33615
来自专栏好好学java的技术栈

java实现手机短信验证全过程

1033
来自专栏ascii0x03的安全笔记

SEED缓冲区溢出实验笔记

缓冲区溢出实验(Linux 32位) 参考教程与材料:http://www.cis.syr.edu/~wedu/seed/Labs_12.04/Software...

3575

扫码关注云+社区