当前位置: 首页 > 编程日记 > 正文

查询在应用程序运行得很慢, 但在SSMS运行得很快的原因探究

原文:查询在应用程序运行得很慢, 但在SSMS运行得很快的原因探究

查询在应用程序运行得很慢,

但在SSMS运行得很快的原因探究

-理解性能疑点

1      引言

内容来自http://www.sommarskog.se/query-plan-mysteries.html(Slow in the Application, Fast in SSMS?)

【看国内没有人好好翻译过这篇。全部翻译实在太长了,就挑主要的意译了。如果要看完整版,还是建议读原文。先翻译了一半,先解决了眼前的问题。剩下的一般看以后有时间再继续翻译了。】

【另外贴图太麻烦了,带图版看下载 http://pan.baidu.com/s/1SMQy】

2      介绍

经常有困惑的发帖者到SQL Server的论坛上提问,为什么有些查询及存储过程在应用程序运行得超慢,但把SQL Batch放到SQL Server Management Studio运行就超快(根据我遇到的情况,运行速度相差千倍)。也有这样的情况:把存储过程中的某句查询分离出来单独运行,有可能比在存储过程中运行的速度快或慢很多。

下面会介绍SQL Server如何编译一段存储过程,什么是变量嗅探(parameter sniffering),SQL Server如何使用缓存。

本文的范例使用的是Northwind范例数据库。

2.1      假设

本文不是初学者向的。你可以不了解查询计划。

3      SQL Server编译存储过程的方式

本章说明了SQL Server如何编译一段存储过程,以及如何使用查询缓存。

适用于:没有使用存储过程,而是直接提交SQL语句的应用程序。

3.1      什么是存储过程

这个问题背后的隐含问题是“什么对象自带查询计划”。SQL Server对下列的对象构建查询计划:

l  存储过程

l  可扩展用户自定义函数

l  表变量函数

l  触发器

SQL Server不会给视图或表内嵌函数(inline-table function)建立查询计划。

如果存储过程有嵌套,例如:

CREATE PROCECURE Outer_sp AS

...

EXEC Inner_sp

...

该情况下Outer_sp和Inner_sp的执行计划是相互独立的,Outer_sp不包含Inner_sp的执行计划。动态SQL也是如此。

3.2      SQL Server如何生成查询计划

3.2.1      概述

当创建存储过程(函数,触发器),SQL Server首先会检查语法,没有用到不存在的列。在这个时候还没有建立任何查询计划。

当执行的时候,计划被创建。对于其中的每一句查询,SQL Server会根据收集到的数据来进行优化该句查询,但不会分析存储过程的整个过程。

3.2.2      参数和变量

给一个范例

CREATE PROCEDURE List_orders_1 AS

SELECT * FROM Orders WHERE OrderDate > '20000101'

go

CREATE PROCEDURE List_orders_2 @fromdate datetime AS

SELECT * FROM Orders WHERE OrderDate > @fromdate

go

CREATE PROCEDURE List_orders_3 @fromdate datetime AS

DECLARE @fromdate_copy datetime

SELECT @fromdate_copy = @fromdate

SELECT * FROM Orders WHERE OrderDate > @fromdate_copy

Go

然后执行

EXEC List_orders_1

EXEC List_orders_2 '20000101'

EXEC List_orders_3 '20000101'

允许“包含真实执行计划(Include Actual Execution Plan)”,就可以看到前两个存储过程执行计划是一样的:

而第三个存储过程是:

(Clustered Index Scan等同于Table Scan)

当查看详细情况时,可以发现“预估行数(Estimated Number of Rows)”,对于前两个查询来说是1,对于第三个查询是249。当返回少量数据时,Index Seek + Key Lookup很有效率;但符合条件的查询结果变多时,SQL Server很有可能需要多次访问同一数据页。在极端情况下,如果要返回某一数据页里的所有行,Table Scan比Index Seek + Key Lookup效率高得多。以刚才的范例而言,Orders表的总行数是830行,SQL Server预估会返回249行时,它判断在这种情况下Table scan是最好的选择。

3.2.3      预估行数是怎么来的?

第一个查询中日期是一个常数。SQL Server分析了Orders表,里面所有的数据都是来自于2000年之前。但毕竟统计只是统计,SQL Server并不能确信这点,所以会预估返回1条。

第二个查询中,针对的是一个变量,或者用更精确的表述:是一个参数。当执行优化时,SQL Server知道这次是以2000-1-1作为参数来调用的。由于不会进行整体过程分析,SQL Server并不能肯定执行的时候一定会用2000-1-1作为参数。所以它用输入值来做了个预估,得到了第一个查询相同的结果:1行。这种策略被称为参数嗅探(parameter sniffing)。

第三个查询中,输入值被复制到一个局部变量中。但当SQL Server建立计划时,它并不知道这个参数可能的值。因此它采用了通用的假设:30%命中率。830的30%命中率是249。

3.2.4      关键点

l  SQL Server会完全信任查询中的常数。如果预计到不会返回结果,甚至会跳过某些表。

l  对于参数,SQL Server不知道运行时的值,会在编译查询时,对输入的值进行嗅探。

l  对于局部变量,SQL Server不知道运行时的值,会使用通用假设。

3.3      将查询计划放入缓存

如果SQL Server(限定OLTP)每次运行一次都要编译一次存储过程,很有可能要消耗大量CPU资源,特别是有很多用户同时执行简单的短时查询的时候。

因此SQL Server会为存储过程缓存查询计划,直到这些缓存被清除。会清除缓存的情况包括:

l  缓存满了,而且一段时间内没用过

l  执行了ALTER PROCEDURE

l  执行了sp_recompile

l  执行了DBCC FREEPROCCACHE

l  SQL Server重启

l  改变了某些配置参数

重新建立查询计划时,会重新嗅探。如果这次遇到一个不同的值,会重新生成查询计划。

有些情况下会重新建立部分语句重新编译,包括:

l  改变涉及的表定义

l  涉及的表丢弃或新增或修改一个索引

l  新建或更新涉及的表的统计

l  对涉及的表运行了sp_recompile

3.4      不同配置下的不同计划

并不是所有人都可以使用缓存中的查询计划。看一个范例:

CREATE PROCEDURE List_orders_6 AS

SELECT *

FROM   Orders

WHERE  OrderDate > '12/01/1998'

go

SET DATEFORMAT dmy

go

EXEC List_orders_6

go

SET DATEFORMAT mdy

go

EXEC List_orders_6

go

12/01/1998这个日期在“日月年”和“月日年”下都说得通。第一次执行会返回很多订单,而第二次执行会返回空结果。两者的执行计划也不相同。第一次执行是Clustered Index Scan(返回很多结果下的最佳选择),第二次执行是Index Seek + Key Lookup(返回空结果时的最佳结果)。

是否SET DATEFORMAT导致了重编译?非也,SQL Server没有那么傻。执行的用户很可能是有不同的时间格式,而计划缓存中的存储过程并非针对会话或用户的,而是全局的针对所有连接的用户。所以SQL Server是创建了第二个缓存记录。

如果要更深入地看缓存这个主题,可以看Books Online的sys.dm_exec_paln_attributes这一主题。

在会对执行计划产生影响的列中,set_options是一个很关键的属性。只要set_options不同,就会编译不同的计划。

3.5      默认设置

SET ON/OFF选项的存在是由于一个历史原因。在很早之前的黑暗年代,SQL Server存在很多与ANSI标准冲突的行为。从SQL Server 6.5版开始,微软引入了SET选项,允许用户采用ANSI标准。从SQL Server 7开始,微软改变了对于ODBC和OLE DB API的默认选项。

Applications using
ADO .Net, ODBC or OLE DB

SSMS
Query Analyzer

SQLCMD,
OSQL, BCP,
SQL
 Server Agent

ISQL
DB-Library

ANSI_NULL_DFLT_ON

ON

ON

ON

OFF

ANSI_NULLS

ON

ON

ON

OFF

ANSI_PADDING

ON

ON

ON

OFF

ANSI_WARNINGS

ON

ON

ON

OFF

CONACT_NULLS_YIELD_NULL

ON

ON

ON

OFF

QUOTED_IDENTIFIER

ON

ON

OFF

OFF

ARITHABORT

OFF

ON

OFF

OFF

从上面的表格可以看到,你的应用程序是以ARITHABORT OFF的选项来运行的。但在SSMS中运行时,ARITHABORT是ON。所以你无法重复使用应用程序用户所使用的缓存。所以应用程序和SSMS是用完全两套缓存。这回答了文章开头的那个问题:“为什么应用程序中很慢,而在SSMS中执行得很快”。当然还可能有一些其他理由导致,不过最常见的理由就是ARITHABORT这个设置。(如果你只是想知道原因,读到这里就可以结束了。但如果你还想知道如何解决性能问题,再继续读下去)

除了通过SET命令,ALTER DATABASE这句语句也可以让你设置某个数据库对某个SET选项始终保持为ON,以覆盖API的默认选项。但是,不能将某个SET选项始终设置为OFF,虽然从语法上是可行的。另外,如果你在SSMS中写查询时,明确指定了某个选项,也会覆盖数据库的默认选项。

那么多SET选项看上去有些让人头晕。但其实只要记住,这7个选项中,前六个只是用来做向后兼容,基本你不会需要把它们设置成OFF。

接下来我们看ARITHABORT。在SQL Server 2005和之后的版本,只要ANSI_WARNINGS是ON,ARITHABORT选项就没有影响。所以没必要将这个选项设置成ON。在SSMS中时,将SET ARITHABORT设置成OFF会比较方便。

这会改变你连接到SSMS时对于ARITHABORT的默认选项。这不会使你的应用程序运行得更快,但你至少不会因为在SSMS中的性能差异而感到困惑。

PS. 强烈建议不要改变ANSI页里的任何选项。

对于SQLCMD和OSQL,请习惯性地加个-I选项。

【我终于知道为什么我根据Stackoverflow上的回答胡乱运行了几句SQL后,.NET程序执行存储过程的速度也变快很多了:我先执行了ALTER PROCEDURE,导致执行计划重新编译,然后在查询中显式地SET ARITHABORT OFF。于是存储过程重新嗅探了新的参数,然后生成了新的执行计划。最后.NET程序以同样的参数执行存储过程,使用了SET ARITHABORT OFF对应的执行计划,所以执行的速度基本和我在SSMS中执行的速度相同了】

4      并非所有原因都是变量嗅探

在深入研究如何解决变量嗅探导致的性能问题之前,先介绍一下其他几种与变量嗅探无关,但也会导致SSMS和应用程序性能差异的情况。

4.1      变量与参数替换

有些人遇到过这种情况:同一句查询,在存储过程中运行得很慢,但是单独拎出来跑就执行得很快。关键问题是局部变量和参数。当查找问题原因时,他们把变量替换成了常数。但如我们之前所述,如果是常数而非变量的话,SQL Server能给出更精准的预估,生成一个更好的查询计划。

类似的情况还有将参数改成变量。比如下面这句查询:

CREATE PROCEDURE some_sp @par1 int AS

...

-- Some query that refers to @par1

如果改成了

DECLARE @par1 int

SELECT @par1 = 4711

-- query goes here

当@par1是局部变量时,SQL Server不知道@par1的值,会做一个标准值的假设。

理论的情况我们都清楚了。但如果你有一个1000行的存储过程,其中一句查询很慢。你如何判定?

有一个方法是将查询放到sp_executesql里,例如:

EXEC sp_executesql N'-- Some query that refers to @par1', N'@par1 int', 4711

(需要注意:如果有单引号的话要变成两个单引号。如果有局部变量的话需要赋值)

可以创建一个临时的存储过程,在存储过程中使用如上的方法替换有嫌疑的语句,运行后测试。

4.2      阻塞

别忘记有时候运行缓慢只是因为阻塞。可以再过3小时等阻塞结束之后再运行一次。如果你发现无论你加不加ARITHABORT,存储过程运行得都很快,就很可能是由于阻塞这个原因。

4.3      链接服务器(Linked Server)相关

本节的内容与SQL 2012 SP1之前的链接服务器有关。例如如下查询:

SELECT C.*

FROM   SOME_SERVER.Northwind.dbo.Orders O

JOIN   Customers C ON O.CustomerID = C.CustomerID

WHERE  O.OrderID > 20000

我以两个不同的用户登录分别运行过。第一个用户在两台服务器上都是sysadmin,第二个用户是只有SELECT权限的普通用户。

当以sysadmin来运行时的计划如下:

当以普通用户运行时,计划是不同的:

由于没有参数,显然不是参数嗅探的原因。可以看一下预计行数的差异:

当以sysadmin运行时,预计是1行,这个预估符合实际,因为实际上没有ID超过20000。但当以普通用户运行时,预估是249行。这是由于统计丢失了。

当查询在本地服务器上运行时,查询优化器可以访问查询里涉及的所有表,没有权限检查。但对于链接服务器,由于没有跨服务器通信的安全协议,SQL Server只会使用标准的OLE DB接口。SQL Server对于其它实例,Oracle,文本文件都是如此。在上述的范例中,SQL Server实际进行了两个步骤:首先运行sp_table_statistics2_rowset来获取数据基数和数据密度信息。直到SQL Server 2012 RTM,运行DBCC SHOW_STATISTICS,你必须是一个sysadmin,db_owner或db_ddladmin的角色。

当以普通用户运行时,会产生一个权限错误。虽然这个错误没有传染性,但查询优化器因此没法获得统计信息,只能用默认假设。

所以当问题涉及链接服务器时,需要注意权限问题。可以采取的措施有:

l  添加db_ddladmin权限。但这个权限可以增删表,所以不推荐。

l  将登录账号映射到sp_adlinkedsvrlogin

l  把查询用OPENQUERY重写,强制在远程服务器上重新评估。

l  使用HINT。

l  重新考虑为什么一定要用链接服务器的方式,换复制等其他方法。

从SQL Server 2012 SP1开始,这个权限标准被放宽了。

5      收集信息以解决变量嗅探问题

在紧急情况下,有一个快速解决的方法可以搞定性能问题:

EXEC sp_recompile problem_sp

这可以清除查询缓存。下次运行的时候生成一条新的查询计划。如果问题不再发生就万事大吉了。

但更常见的展开是问题反复发生。在这种情况下,你不应该使用sp_recompile。

5.1      获得必要的依据

所有的性能调试都需要依据。你可以用来判断的依据有:

  1. 哪条语句很慢?
  2. 查询计划的差异在哪里?
  3. SQL Server嗅探了什么参数?
  4. 表定义和索引定义是什么?
  5. 统计是什么样的?有更新了么?

上面5个问题中,只有第三个是仅针对参数嗅探。其它的4个都是泛用的。

5.2      哪条语句很慢?

大多数情况下,问题只和一条语句有关。可以用SQL Profiler来看语句运行的持续时间。或者也可以用这篇(http://www.sommarskog.se/sqlutil/sqltrace.html)中提到的sqltrace存储过程。

5.3      通过SSMS获得查询计划和参数

典型步骤如下:

SET ARITHABORT ON

go

EXEC that_very_sp 4711, 123, 1

go

SET ARITHABORT OFF

go

EXEC that_very_sp 4711, 123, 1

可以在查询计划中查看属性,发现嗅探的参数

上图中的Parameter Compiled Value就是嗅探的参数。

此外还有SQL Sentry Plan Explorer(http://www.sqlsentry.net/plan-explorer/sql-server-query-view.asp)工具。

相关文章:

Spring Boot 2.X 实现文件上传(三)

使用 SpringBoot 项目完成单个、多个文件的上传处理&#xff0c;并将上传的文件保存到指定目录下。 代码演示案例 所有的 HTML 页面文件 index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>选…

Blender灯光照明与渲染视频教程 Skillshare – Blender: Product rendering for beginners

Blender灯光照明与渲染视频教程 Skillshare – Blender: Product rendering for beginners Skillshare – Blender: Product rendering for beginners 教程大小&#xff1a;2.2G 共33小节课 1920X1080 mp4视频 语言&#xff1a;英语中文字幕 在这门课程中&#xff0c;你将了…

Java学习总结:21

泛型 泛型的引出 例&#xff1a;向下转型的错误示范 package Project.Study.Genericity;class Point{ //定义坐标类private Object x; //可以保存任意数据private Object y; //可以保存任意数据public Object getX() {return x;}public Object getY() {return y;}pu…

Lab模式的妙用--人像处理

其实就是&#xff1a; 1.复制明度图层到RGB模式&#xff0c;降低透明度。 2.仿制图章磨皮 3.液化 4.可选颜色润饰皮肤。 转载于:https://www.cnblogs.com/wensu/p/4997579.html

springboot整合swagger2之最佳实践

来源:https://blog.lqdev.cn/2018/07/21/springboot/chapter-ten/ Swagger是一款RESTful接口的文档在线自动生成、功能测试功能框架。 一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化RESTful风格的Web服务&#xff0c;加上swagger-ui&#xff0c;可以有很好的…

转:动态链接库的全局变量问题

动态链接库的全局变量问题 请问动态链接库被外部多次调用时&#xff0c;每次调用中动态库自身的全局变量都会重新初始化吗&#xff1f;如果会那怎么来使我每次调用动态库时都能从上次的状态开始执行呢&#xff1f; 你这个问题得分被调用的是同一个进程还是不同的进程。如果是不…

排名前 16 的 Java 工具类

转载来自微信公众号&#xff1a;Java 技术栈。如有侵权&#xff0c;请联系作者删除&#xff01;&#xff01; 在 Java 中&#xff0c;工具类定义了一组公共方法&#xff0c;这篇文章将介绍 Java 中使用最频繁及最通用的 Java 工具类。以下工具类、方法按使用流行度排名&#xf…

c4d教程-太空火车站场景创作视频教程Skillshare – Create A Space Train Scene With Cinema 4D Redshift Render

c4d教程-太空火车站场景创作视频教程Skillshare – Create A Space Train Scene With Cinema 4D & Redshift Render 教程大小 1.66G 共15小节 1280X720 mp4 视频 语言&#xff1a;英语中文字幕 百度一下 云桥网络 平台huo取 教程&#xff01; Skillshare – Create A Spa…

Java学习总结:23

前言&#xff1a;断更挺久了&#xff0c;今天把之前的补上 Annotation 3种最为常用的Annotation定义&#xff0c;Override、Deprecated和SuppressWarnings 1.准确的覆写&#xff1a;Override 当进行方法覆写时&#xff0c;为了保证子类所覆写的方法的确是父类中定义过的方法…

Windows Live Writer发布测试

通过Windows Live Writer发布文章测试&#xff01;&#xff01;&#xff01;&#xff01; 转载于:https://www.cnblogs.com/passerlee/p/5000567.html

2022-2028年中国锂电池用聚烯烃隔膜行业市场发展调研及投资方向分析报告

【报告类型】产业研究 【报告价格】4500起 【出版时间】即时更新&#xff08;交付时间约3个工作日&#xff09; 【发布机构】智研瞻产业研究院 【报告格式】PDF版 本报告介绍了中国锂电池用聚烯烃隔膜行业市场行业相关概述、中国锂电池用聚烯烃隔膜行业市场行业运行环境、…

【莫队算法】bzoj3781 小B的询问

莫队经典。 开个数组维护a[i]出现的次数。 1 #include<cstdio>2 #include<cmath>3 #include<algorithm>4 using namespace std;5 int Num,CH[12],f,c;6 inline void R(int &x){7 c0;f1;8 for(;c<0||c>9;cgetchar())if(c-)f-1;9 for(x0…

计算机为什么需要十六进制?

我们知道内存是按照字节粒度来寻址的,因此采用的数字系统必须很好的表达一个字节,也就是8比特,从这个角度上看256进制(2^8)是最好的,因为一个256进制就是表达一个字节,但还是基于可读性的原因,256进制对于人类来说记忆负担过重,而16进制则刚刚好,一个16进制数字表示一个字节的一半(4个比特),两个16进制数字正好表示一个字节。每个十六进制中的数字代表4个比特,你可以非常直观的从十六进制中知道对应的二进制是啥,比如给定一个十六进制数,假设其最后一位是9,那为什么十六进制是二进制的好朋友呢?

JAVA 自定义函数式编程

​ 2.2)定义接口 void testInterface(String str);2.定义一个普通接口(TestInterface.java)​ 2.1)类上注明为函数式接口有且只能有一个方法;

7.1.1 [Enterprise Library]缓存应用程序块场景和目标

缓存应用程序块为解决开发人员在缓存数据时所面对的绝大多数任务而设计。这些任务根据场景进行了组织&#xff0c;每个场景都给出了一个真实世界情况的示例&#xff0c;如添加条目到缓存&#xff0c;讨论了情况所需要的缓存功能&#xff0c;并展示了完成任务的代码。 根据场景组…

中式古建筑su模型大全

中式古建筑su模型大全 sketchup草图大师古建塔亭子寺庙名楼民居古建筑中式su模型素材 sketchup模型 古代建筑 古代房屋 古镇 古代街景 古代商业街 古代园林 阁楼 寺庙 含各类古建筑模型合集su模型 文件解压后大小&#xff1a;13G 含预览图 百度一下 云桥网络 平台huo取 素材…

Java学习总结:24

Lambda表达式 Lambda表达式指的是应用在单一抽象方法(SAM)接口环境下的一种简化定义形式&#xff0c;可以用于解决匿名内部类的定义复杂问题。 Lambda表达式的语法&#xff1a; (参数)->方法体相当于子类覆写抽象方法的方法体 例&#xff1a;Lambda表达式入门操作 packa…

【转】Flask安装

Flask 依赖两个外部库&#xff1a;Werkzeug 和 Jinja2 。 Werkzeug 是一个 WSGI&#xff08;在 Web 应用和多种服务器之间的标准 Python 接口) 工具集。Jinja2 负责渲染模板。 那么如何在你的电脑上安装这一切&#xff1f;虽说条条大道通罗马&#xff0c;但是最强大的方式是 vi…

OpenFeign服务接口调用

OpenFeign是什么? Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。 OpenFeign能干什么? Feign和OpenFeign两者区别 OpenFeign使用步骤

完美解决网站PNG图标在IE6下的透明显示

现在IE6虽然有些落后&#xff0c;但不少xp用户还是首选IE6浏览器&#xff0c;一款经典长久也标准快速的浏览器必然会有忠实的用户&#xff0c;png图标IE6下默认是不支持的&#xff0c;可以通过js代码实现&#xff0c;下面由一叶扁舟为大家整理&#xff0c;代码可直接下载一、下…

WPF Layout System

http://blog.tangcs.com/2009/05/23/wpf-layout-system/转载于:https://www.cnblogs.com/WarrenTang/archive/2009/05/23/1487622.html

ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush

ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush ZBrush全面入门学习教程 Schoolism – Introduction to ZBrush 时长:8小时|视频:1920X1080 。教程大小 2.6G 语言&#xff1a;英语中文字幕 百度一下 云桥网络 平台huo取 教程&#xff01; 这个课程非常适合那些…

Java学习总结:25

内建函数式接口 对于可能出现的函数式接口的方法最多只有4类&#xff1a;有参数有返回值、有参数无返回值、无参数有返回值、判断真假。 为了简化开发者的定义以及实现操作的统一&#xff0c;Java提供了一个新的开发包&#xff1a;java.util.function&#xff0c;并且在这个包…

Uva 3767 Dynamic len(set(a[L:R])) 树套树

Dynamic len(set(a[L:R])) Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 https://uva.onlinejudge.org/index.php?optioncom_onlinejudge&Itemid8&pageshow_problem&problem3767 Description 给你n个数&#xff0c;m次操作 Q x y 询问[x1,y]有多少个不同的数…

2022-2028年中国锂电池设备行业深度调研及投资前景预测报告

【报告类型】产业研究 【报告价格】4500起 【出版时间】即时更新&#xff08;交付时间约3个工作日&#xff09; 【发布机构】智研瞻产业研究院 【报告格式】PDF版 本报告介绍了中国锂电池设备行业市场行业相关概述、中国锂电池设备行业市场行业运行环境、分析了中国锂电池…

Cocos Studio的动画系统介绍

Cocos Studio介绍Cocos Studio是一套基于Cocos2D-x的免费游戏开发工具集&#xff0c;它能帮助开发者快速创建游戏资源&#xff0c;将大部分繁琐的游戏开发工作使用编辑器来快速制作&#xff0c;进一步帮助游戏开发者减短开发周期、提高开发效率。Cocos Studio本身不光只是针对[…

机器学习实战源码数据集

链接&#xff1a;https://pan.baidu.com/s/1Ss7x60VXdyQFYW9aiKS0Lg 提取码&#xff1a;9xj6 github下载地址&#xff1a; 转载于:https://www.cnblogs.com/YukiNote/p/11286106.html

blender硬表面建模渲染终极教程

blender硬表面建模渲染终极教程 Gumroad - The ULTIMATE Guide to Hard Ops and Boxcutter Gumroad-硬操作和切箱机的终极指南 教程大小 6G 1920X1080分辨率 语言:英语中文字幕 含案例源文件 云桥网络 平台获取教程 本教程共包含两大部分 第一部分 硬操作和Boxcutter菜单…

Java学习总结:26

线程与进程 进程是程序的一次动态执行过程&#xff0c;它经历了从代码加载、执行到执行完毕的一个完整过程&#xff0c;这个过程也是进程本身从产生、发展到最终消亡的过程。 线程是比进程更小的执行单位&#xff0c;线程是在进程的基础上进行的进一步划分&#xff0c;一个进程…

UINavigationController技巧一——修改返回按钮的标题

UINavigationController 一般push到另一界面后&#xff0c;返回按钮标题便是上一页面的title&#xff0c;但是对于push的第一页或者是上一页面没有title的&#xff0c;返回按钮标题便是默认back&#xff0c;如图所示 在本页面修改title没有用&#xff0c;试了很多办法终于找到 …