sql server 视图_轻松搜索SQL Server –搜索目录视图

sql server 视图

The need to search through database schema for specific words or phrases is commonplace for any DBA. The ability to do so quickly, accurately, and completely is critical when developing new features, modifying existing code, or cleaning up the vestiges from an application’s ancient history.

在数据库模式中搜索特定单词或短语的需求对于任何DBA来说都是司空见惯的。 在开发新功能,修改现有代码或清除应用程序的悠久历史中的痕迹时,快速,准确和完全地执行此操作至关重要。

In this article, we review system catalog views and demonstrate how to use them in order to gather useful information about a wide variety of schema, objects, and components of your SQL Server. Some of the scripts presented here, such as those for foreign keys and indexes, will be extremely valuable tools on their own, without being used as part of a schema-search framework.

在本文中,我们将回顾系统目录视图并演示如何使用它们,以收集有关SQL Server的各种架构,对象和组件的有用信息。 此处提供的某些脚本(例如,用于外键和索引的脚本)本身将是非常有价值的工具,而不会用作模式搜索框架的一部分。

背景 (Background)

Nearly anyone in the software development field has a continuously growing and evolving set of tools that they rely on regularly. One of these tools that I turn to almost daily is the ability to quickly search through database schema. Many operations may lead me to need this tool, some include:

在软件开发领域,几乎任何人都拥有他们定期依赖的,不断增长和发展的工具集。 我几乎每天都会使用的这些工具之一就是能够快速搜索数据库架构。 许多操作可能会让我需要此工具,其中包括:

  • When dropping an old stored procedure, we want to check for other TSQL/database references.

    删除旧的存储过程时,我们要检查其他TSQL /数据库引用。
  • We’re adding a new column to a table and want to verify any objects that reference it table directly.

    我们正在向表中添加新列,并希望验证直接引用该表的所有对象。
  • We want to change the data type of a column and evaluate all places it is used to change the type there as well.

    我们要更改列的数据类型,并评估它用于更改其类型的所有位置。
  • When migrating a database to another server, we need to locate and update all references to it.

    将数据库迁移到另一台服务器时,我们需要查找并更新对该数据库的所有引用。
  • A stored procedure is returning bad data. We want to find other stored procs with the same broken logic.

    存储过程正在返回错误数据。 我们想找到逻辑相同的其他存储过程。

What we want is a readily available, easily customizable tool that can search through all types of objects, whether they be tables, views, linked servers, or indexes and return any objects that contain our search criteria. In the spirit of sharing things I love to use, this article will serve as an introduction to a variety of system/catalog views and how we can use them in order to learn a great deal about our server using minimal effort on our part.

我们想要的是一个易于使用,易于自定义的工具,可以搜索所有类型的对象,无论它们是表,视图,链接服务器还是索引,并返回包含我们搜索条件的任何对象。 本着共享我喜欢使用的东西的精神,本文将作为各种系统/目录视图的介绍,以及如何使用它们来以最小的努力来学习有关服务器的大量知识。

搜索解决方案的基础 (The Building Blocks of a Search Solution)

The list of objects that we want to search in SQL Server is long, but many require only a few lines of TSQL in order to determine if a search string is found or not. We’ll subdivide the remainder of this article into sections based on the objects we are searching and bring it all together at the end into a single script that will do our bidding.

我们要在SQL Server中搜索的对象列表很长,但是许多对象只需要几行TSQL即可确定是否找到了搜索字符串。 我们将根据要搜索的对象将本文的其余部分细分为多个部分,最后将所有内容放到一个单独的脚本中进行出价。

If you have a short attention span, or are already very familiar with the various system catalog views, feel free to skip ahead to where we bring it all together. The following sections explain all of the TSQL that will be used in the final script, and are provided as a solid reference to support what may otherwise seem a somewhat cryptic 250+ lines of TSQL. Some of these queries—especially those for indexes and foreign keys are extremely useful tools on their own…TLDR at your own risk!

如果您的关注时间很短,或者已经非常熟悉各种系统目录视图,请随时跳到我们将所有内容汇总在一起的位置。 以下各节说明了将在最终脚本中使用的所有TSQL,并提供了坚实的参考,以支持TSQL看起来有些神秘的250余行。 其中一些查询(尤其是对于索引和外键的查询)本身是极其有用的工具……TLDR风险自负!

数据库名称 (Database Names)

This is straight-forward, but certainly shouldn’t be ignored. If our search string happens to be found in the name of a database, we want to know about it! This can be accomplished using sys.databases, just like this:

这是直截了当的,但当然不应该忽略。 如果恰好在数据库名称中找到我们的搜索字符串,我们想知道它! 可以使用sys.databases来完成,就像这样:

 
SELECT
	databases.name AS database_name,
	'Database' AS object_type
FROM sys.databases
WHERE databases.name LIKE '%Adventure%';
 

Sys.databases includes a row for every database on the server, regardless of its current status. We’re including an object type in each query so that when we combine all of this TSQL later on, we’ll be able to tell the difference between different types of objects that happen to share the same name. On my local server, the results of the above query look like this:

Sys.databases在服务器上的每个数据库都包括一行,无论其当前状态如何。 我们在每个查询中都包含一个对象类型,以便稍后我们结合所有这些TSQL时,能够分辨碰巧共享相同名称的不同类型对象之间的区别。 在我的本地服务器上,以上查询的结果如下所示:

This is a server-wide search and only needs to be run once and will return information on all databases on your SQL Server.

这是服务器范围内的搜索,仅需要运行一次,并将返回有关SQL Server上所有数据库的信息。

登录名 (Logins)

Another quick & easy server-wide search is for server logins. We can similarly check for our search string using sys.syslogins:

在服务器范围内进行的另一种快速简便的搜索是服务器登录。 我们可以类似地使用sys.syslogins检查我们的搜索字符串:

 
SELECT
	syslogins.name AS login_name,
	'Server Login' AS object_type
FROM sys.syslogins
WHERE syslogins.name LIKE '%edward%';
 

This will return a row for any server login that contains our search string in the login name. In the case of my server, we get the following two results:

这将为任何服务器登录名返回一行,该服务器登录名中包含我们的搜索字符串。 对于我的服务器,我们得到以下两个结果:

The sys.syslogins view includes a column called loginname, but this is provided for backward-compatibility only and can be ignored for the sake of our search efforts.

sys.syslogins视图包括名为loginname的列,但这仅是为了向后兼容而提供,为了我们的搜索工作可以忽略。

工作 (Jobs)

A big pile of data we’d like to dig through are SQL Server Agent jobs. If our search text is located within a job, its description, or within any of its job steps, we definitely want to know about it. Job metadata is stored in MSDB, of which we will use dbo.sysjobs to gather information about job names and descriptions. dbo.sysjobsteps contains info on each job step, of which we will want to check both the step name and the contents of the command itself. We’ll run two queries that will return similar sets of job data (when the job name or description match) or job step data (when the job step metadata matches):

我们想挖掘的大量数据是SQL Server代理作业。 如果我们的搜索文本位于职位,职位描述或职位的任何步骤中,则我们绝对希望了解它。 作业元数据存储在MSDB中,我们将使用dbo.sysjobs收集有关作业名称和描述的信息。 dbo.sysjobsteps包含每个作业步骤的信息,我们将要检查步骤名称和命令本身的内容。 我们将运行两个查询,这些查询将返回相似的作业数据集(当作业名称或描述匹配时)或作业步骤数据(当作业步骤元数据匹配时):

 
SELECT
	sysservers.srvname AS server_name,
	sysjobs.name AS objectname,
	sysjobs.description AS object_description,
	sysjobs.enabled,
	'SQL Agent Job'
FROM msdb.dbo.sysjobs
INNER JOIN master.dbo.sysservers
ON srvid = originating_server_id
WHERE sysjobs.name LIKE '%verification%'
OR sysjobs.description LIKE '%verification%';
 
SELECT
	s.srvname AS server_name,
	sysjobs.name AS objectname,
	sysjobsteps.step_name,
	sysjobs.description AS object_description,
	sysjobsteps.command AS object_definition,
	sysjobs.enabled,
	'SQL Agent Job Step'
FROM msdb.dbo.sysjobs
INNER JOIN msdb.dbo.sysjobsteps
ON sysjobsteps.job_id = sysjobs.job_id
INNER JOIN master.dbo.sysservers s
ON s.srvid = sysjobs.originating_server_id
WHERE sysjobsteps.command LIKE '%verification%'
OR sysjobsteps.step_name LIKE '%verification%';
 

For our jobs, we return a bit more metadata than before, as we are interested in both the job itself, as well as the job steps associated with it. Note the use of a LEFT JOIN to dbo.sysjobsteps. In the event that a job has no steps defined, or none that match our search criteria, we still want to get search results on any matches to the job name or description. The results on my local server look like this:

对于我们的工作,我们返回的元数据比以前多了,因为我们对工作本身以及与之相关的工作步骤都感兴趣。 注意对dbo.sysjobsteps使用LEFT JOIN。 如果没有定义任何步骤,或者没有匹配搜索条件的工作,我们仍然希望获得与该工作名称或描述匹配的搜索结果。 我的本地服务器上的结果如下所示:

Including the various columns in the WHERE clause allows us to quickly see where we matched our search terms, providing more than enough information for us to conduct whatever additional research is required.

在WHERE子句中包括各个列,使我们能够快速查看匹配搜索词的位置,从而为我们提供了足够的信息来进行所需的其他研究。

链接服务器 (Linked Servers)

Searching for any linked servers that contain our search terms is straightforward and is also a single server-wide search:

搜索包含我们搜索词的任何链接服务器非常简单,而且也是单个服务器范围的搜索:

 
SELECT
	servers.name AS server_name,
	servers.data_source,
	'Linked Server' AS object_type
FROM sys.servers
WHERE servers.name LIKE '%2016%'
OR servers.data_source LIKE '%2016%';
 

This checks both the name of the linked server, as well as the data source definition for the search string. sys.servers contains quite a bit of additional data about our servers that can also be queried, such as security settings, provider type, and the last modified date. For the examples in this demo, I’m sticking to the basics in order to minimize complexity and the amount of data we need to collect along the way. The results of the above query are as follows:

这将检查链接服务器的名称以及搜索字符串的数据源定义。 sys.servers包含很多有关我们服务器的其他数据,例如安全设置,提供程序类型和最后修改日期,这些数据也可以查询。 对于本演示中的示例,我坚持使用基础知识,以最大程度地减少复杂性和在此过程中需要收集的数据量。 以上查询的结果如下:

I only have a single linked server on my local machine, but since it matches the search criteria, it’s returned as expected, along with the local instance name.

我的本地计算机上只有一个链接服务器,但是由于它符合搜索条件,因此将按预期方式返回,并带有本地实例名称。

桌子 (Tables)

Let’s move on to database-level objects. These are all defined per-database and should be searched in every database on a server, if you’re looking to be thorough and check everywhere for your search terms. If you only care about a specific database or set of databases, then you may only check those and ignore the rest.

让我们继续到数据库级对象。 这些都是针对每个数据库定义的,如果您希望了解透彻的内容并在各处检查搜索条件,则应在服务器上的每个数据库中进行搜索。 如果您只关心一个特定的数据库或一组数据库,则只能检查这些数据库,而忽略其余的数据库。

Searching tables is straight-forward:

搜索表很简单:

 
SELECT
	db_name() AS database_name,
	tables.name AS table_name,
	'Table' AS object_type
FROM sys.tables
WHERE tables.name LIKE '%Person%';
 

sys.tables provides information on every table in the database, including system objects, and makes for an easy search:

sys.tables提供有关数据库中每个表(包括系统对象)的信息,并使其易于搜索:

The database name is included as a convenience, which will make locating the object easier if we are collecting lots of this type of data for inspection later on.

包括数据库名称是为了方便,如果我们收集大量此类数据以供日后检查,这将使查找对象更加容易。

(Columns)

Searching for columns in a meaningful way involves checking back with the parent table to confirm the relationship we are identifying. Knowing that some column exists with a specific name isn’t very useful, we want to know the database, table, and column so that it’s easy to locate:

以有意义的方式搜索列涉及与父表进行核对,以确认我们正在确定的关系。 知道存在具有特定名称的列不是很有用,我们想知道数据库,表和列,以便于查找:

 
SELECT
	db_name() AS database_name,
	tables.name AS table_name,
	columns.name AS column_name,
	'Column' AS object_type
FROM sys.tables
INNER JOIN sys.columns
ON tables.object_id = columns.object_id
WHERE columns.name LIKE '%BusinessEntityID%';
 

This query introduces sys.columns, which provides a row per column in each different table in the database. The results of the above query provide us exactly what we are looking for:

该查询引入sys.columns ,该数据库在数据库的每个不同表中每列提供一行。 以上查询的结果为我们提供了我们所需要的:

Every BusinessEntityID in AdventureWorks is returned, all 18 of them! We have so far been omitting schema name from the result set, but you could easily add it in to any related object query with a join to sys.schemas:

返回AdventureWorks中的每个BusinessEntityID ,全部18个! 到目前为止,我们一直在结果集中省略模式名称,但是您可以通过加入sys.schemas轻松地将其添加到任何相关的对象查询中:

 
SELECT
	db_name() AS database_name,
	schemas.name AS schema_name,
	tables.name AS table_name,
	columns.name AS column_name,
	'Column' AS object_type
FROM sys.tables
INNER JOIN sys.columns
ON tables.object_id = columns.object_id
INNER JOIN sys.schemas
ON schemas.schema_id = tables.schema_id
WHERE columns.name LIKE '%BusinessEntityID%';
 

The results are the same as before, but with the added schema_name column:

结果与之前相同,但是增加了schema_name列:

The results are cut off to save some space here, but the added schema name is extremely useful in AdventureWorks as it makes heavy use of a variety of different schemas. We’ll include it in all examples going forward since it’s minimal extra effort for big informational gain!

为了节省空间而在此处截取了结果,但是添加的模式名称在AdventureWorks中非常有用,因为它大量使用了各种不同的模式。 我们将在以后的所有示例中都将其包括在内,因为它只需很少的额外努力即可获得大量信息!

模式 (Schemas)

While we are on the topic of schemas, we may as well search them as well:

当我们讨论模式主题时,我们也可以搜索它们:

 
SELECT
	db_name() AS database_name,
	schemas.name AS schema_name
FROM sys.schemas
WHERE schemas.name LIKE '%Person%';
 

This simple query returns any schemas whose names match our search terms.

这个简单的查询返回名称与我们的搜索词匹配的任何模式。

同义字 (Synonyms)

If you happen to use synonyms, then being able to search both their names and targets is very useful, and can be done solely with the sys.synonyms view:

如果您碰巧使用同义词,那么能够同时搜索它们的名称和目标非常有用,并且可以仅使用sys.synonyms视图来完成:

 
SELECT
	db_name() AS database_name,
	synonyms.name AS synonym_name,
	synonyms.base_object_name,
	'Synonym' AS object_type
FROM sys.synonyms
WHERE synonyms.name LIKE '%product%'
OR synonyms.base_object_name LIKE '%product%';
 

The results of this query are similar in style to those for the linked server search:

该查询的结果在样式上与链接服务器搜索的结果类似:

指标 (Indexes)

We’ll be searching indexes by two criteria: name and column list. Each requires a separate search as the method for each is quite different. Searching index names is quite straightforward:

我们将通过两个条件搜索索引:名称和列列表。 每种方法都需要单独搜索,因为每种方法都非常不同。 搜索索引名称非常简单:

 
SELECT
	db_name() AS database_name,
	tables.name AS table_name,
	indexes.name AS index_name,
	'Index' AS object_type
FROM sys.indexes
INNER JOIN sys.tables
ON tables.object_id = indexes.object_id
WHERE indexes.name LIKE '%ProductCategory%';
 

We join sys.indexes to sys.tables in the same manner that we did for sys.columns earlier. The result is a list of any index that is named using the search terms provided:

我们将sys.indexes加入到sys.tables中的方式与之前对sys.columns相同。 结果是使用提供的搜索词命名的任何索引的列表:

That was the easy part! The hard part is not only searching the index column lists, but returning meaningful data about each on a single result row. Given the choice, I’d prefer to not get a row per column, which will be extremely hard to decipher. In order to accomplish this, we’ll introduce sys.index_columns, and break this view up into key columns and include columns. Once we’ve separated between the types of index columns, we will concatenate those column lists into strings. The only quick and dirty way I came up with to do this in a single query was using XML. Dynamic SQL is also an option, but would provide a much longer and more complex query to work with. For the sake of this demo, we’ll use XML, but can certainly delve into wild dynamic SQL in a future article, as I do enjoy that sort of thing!

那是容易的部分! 困难的部分不仅在于搜索索引列列表,还在于在单个结果行上返回关于每个索引集的有意义的数据。 给定选择的余地,我宁愿每列都不要有一行,这将很难破解。 为了实现此目的,我们将引入sys.index_columns ,并将此视图分解为关键列和包含列。 一旦我们在索引列的类型之间进行了分离,就将那些列列表连接成字符串。 我在单个查询中想到的唯一快速而肮脏的方法是使用XML。 动态SQL也是一个选项,但是可以提供更长,更复杂的查询。 为了这个演示的目的,我们将使用XML,但是在以后的文章中肯定可以深入研究野生的动态SQL,因为我确实喜欢这种事情!

 
WITH CTE_INDEX_COLUMNS AS (
	SELECT
		db_name() AS database_name,
		TABLE_DATA.name AS table_name,
		INDEX_DATA.name AS index_name,
		STUFF(( SELECT ', ' + columns.name
				FROM sys.tables
				INNER JOIN sys.indexes
				ON tables.object_id = indexes.object_id
				INNER JOIN sys.index_columns
				ON indexes.object_id = index_columns.object_id
				AND indexes.index_id = index_columns.index_id
				INNER JOIN sys.columns
				ON tables.object_id = columns.object_id
				AND index_columns.column_id = columns.column_id
				WHERE INDEX_DATA.object_id = indexes.object_id
				AND INDEX_DATA.index_id = indexes.index_id
				AND index_columns.is_included_column = 0
				ORDER BY index_columns.key_ordinal
			FOR XML PATH('')), 1, 2, '') AS key_column_list,
			STUFF(( SELECT ', ' + columns.name
				FROM sys.tables
				INNER JOIN sys.indexes
				ON tables.object_id = indexes.object_id
				INNER JOIN sys.index_columns
				ON indexes.object_id = index_columns.object_id
				AND indexes.index_id = index_columns.index_id
				INNER JOIN sys.columns
				ON tables.object_id = columns.object_id
				AND index_columns.column_id = columns.column_id
				WHERE INDEX_DATA.object_id = indexes.object_id
				AND INDEX_DATA.index_id = indexes.index_id
				AND index_columns.is_included_column = 1
				ORDER BY index_columns.key_ordinal
			FOR XML PATH('')), 1, 2, '') AS include_column_list,
			'Index Column' AS object_type
	FROM sys.indexes INDEX_DATA
	INNER JOIN sys.tables TABLE_DATA
	ON TABLE_DATA.object_id = INDEX_DATA.object_id)
SELECT
	database_name,
	table_name,
	index_name,
	key_column_list,
	ISNULL(include_column_list, '') AS include_column_list
FROM CTE_INDEX_COLUMNS
WHERE CTE_INDEX_COLUMNS.key_column_list LIKE '%PurchaseOrderID%'
OR CTE_INDEX_COLUMNS.include_column_list LIKE '%PurchaseOrderID%';
 

While not as simple as our earlier queries, this gets the job done and is quite fast. The CTE is responsible for generating the key and include column lists. Once this work is done, the remainder of the query needs only to perform a comparison against those strings using out search criteria and we’re done! ISNULL is used on the include column list since an index must have key columns, but is under no obligation to contain any include columns. The results of this search on AdventureWorks are as follows:

虽然不像我们之前的查询那样简单,但是可以完成工作并且非常快。 CTE负责生成密钥并包括列列表。 完成这项工作后,剩下的查询只需要使用搜索条件对这些字符串进行比较即可完成! ISNULL用于包含列列表,因为索引必须具有键列,但没有义务包含任何包含列。 在AdventureWorks上的搜索结果如下:

Even on its own, this is a fairly useful tool! In addition to searching for indexes that have columns matching our search criteria, we could leave off the WHERE clause and use this TSQL to provide a complete list of all indexes in a database. This can be used for index research when looking to identify overlapping or duplicate indexes, or simply for documentation purposes.

即使单独使用,这也是一个相当有用的工具! 除了搜索具有与我们的搜索条件匹配的列的索引之外,我们还可以省略WHERE子句,并使用此TSQL提供数据库中所有索引的完整列表。 当寻找标识重叠或重复的索引时,可将其用于索引研究,或者仅用于文档目的。

服务代理队列 (Service Broker Queues)

Finding queues is quite easy, and can be done by querying the system view sys.service_queues:

查找队列非常容易,可以通过查询系统视图sys.service_queues来完成:

 
SELECT
	name,
	'Queue' AS object_type
FROM sys.service_queues
WHERE service_queues.name LIKE '%Test_Queue%';
 

The results will be any queues in the current database matching our search criteria, including any system objects that happen to be in the result set (of which there are none here):

结果将是当前数据库中符合我们搜索条件的所有队列,包括碰巧出现在结果集中的所有系统对象(此处不包含任何系统对象):

约束条件 (Constraints)

Constraints are enumerated in a handful of metadata tables, and for each one we will want to check both the constraint name and the definition to verify that if our search terms are found in either. Foreign key name checking can be accomplished with this query:

约束在少数元数据表中枚举,对于每一个元数据表,我们将要同时检查约束名称和定义,以验证是否在两个表中都找到了我们的搜索词。 外键名称检查可以通过以下查询完成:

 
SELECT
	schemas.name AS schema_name,
	objects.name AS parent_table,
	foreign_keys.name AS foreign_key_name,
	'Foreign Key' AS object_type
FROM sys.foreign_keys
INNER JOIN sys.schemas
ON foreign_keys.schema_id = schemas.schema_id
INNER JOIN sys.objects
ON objects.object_id = foreign_keys.parent_object_id
WHERE foreign_keys.name LIKE '%Customer%';
 

Here, we grab the schema and parent table name, in addition to the foreign key name. This leaves the guesswork out of locating the key once we have a name:

在这里,除了外键名称之外,我们还获取模式和父表名称。 一旦我们有了一个名字,就不用再去寻找密钥了:

Next, we’d like to verify if the search terms exist within the column list of a foreign key, which will be a similar process to what we did for index columns earlier:

接下来,我们要验证搜索项是否存在于外键的列列表中,这与我们之前对索引列所做的过程类似:

 
WITH CTE_FOREIGN_KEY_COLUMNS AS (
	SELECT
		parent_schema.name AS parent_schema,
		parent_table.name AS parent_table,
		referenced_schema.name AS referenced_schema,
		referenced_table.name AS referenced_table,
		foreign_keys.name AS foreign_key_name,
		STUFF(( SELECT ', ' + referencing_column.name
				FROM sys.foreign_key_columns
				INNER JOIN sys.objects
				ON objects.object_id = foreign_key_columns.constraint_object_id
				INNER JOIN sys.tables parent_table
				ON foreign_key_columns.parent_object_id = parent_table.object_id
				INNER JOIN sys.schemas parent_schema
				ON parent_schema.schema_id = parent_table.schema_id
				INNER JOIN sys.columns referencing_column
				ON foreign_key_columns.parent_object_id = referencing_column.object_id 
				AND foreign_key_columns.parent_column_id = referencing_column.column_id
				INNER JOIN sys.columns referenced_column
				ON foreign_key_columns.referenced_object_id = referenced_column.object_id
				AND foreign_key_columns.referenced_column_id = referenced_column.column_id
				INNER JOIN sys.tables referenced_table
				ON referenced_table.object_id = foreign_key_columns.referenced_object_id
				INNER JOIN sys.schemas referenced_schema
				ON referenced_schema.schema_id = referenced_table.schema_id
				WHERE objects.object_id = foreign_keys.object_id
				ORDER BY foreign_key_columns.constraint_column_id ASC
			FOR XML PATH('')), 1, 2, '') AS foreign_key_column_list,
		STUFF(( SELECT ', ' + referenced_column.name
				FROM sys.foreign_key_columns
				INNER JOIN sys.objects
				ON objects.object_id = foreign_key_columns.constraint_object_id
				INNER JOIN sys.tables parent_table
				ON foreign_key_columns.parent_object_id = parent_table.object_id
				INNER JOIN sys.schemas parent_schema
				ON parent_schema.schema_id = parent_table.schema_id
				INNER JOIN sys.columns referencing_column
				ON foreign_key_columns.parent_object_id = referencing_column.object_id 
				AND foreign_key_columns.parent_column_id = referencing_column.column_id
				INNER JOIN sys.columns referenced_column
				ON foreign_key_columns.referenced_object_id = referenced_column.object_id
				AND foreign_key_columns.referenced_column_id = referenced_column.column_id
				INNER JOIN sys.tables referenced_table
				ON referenced_table.object_id = foreign_key_columns.referenced_object_id
				INNER JOIN sys.schemas referenced_schema
				ON referenced_schema.schema_id = referenced_table.schema_id
				WHERE objects.object_id = foreign_keys.object_id
				ORDER BY foreign_key_columns.constraint_column_id ASC
			FOR XML PATH('')), 1, 2, '') AS referenced_column_list,
			'Foreign Key Column' AS object_type
	FROM sys.foreign_keys
	INNER JOIN sys.tables parent_table
	ON foreign_keys.parent_object_id = parent_table.object_id
	INNER JOIN sys.schemas parent_schema
	ON parent_schema.schema_id = parent_table.schema_id
	INNER JOIN sys.tables referenced_table
	ON referenced_table.object_id = foreign_keys.referenced_object_id
	INNER JOIN sys.schemas referenced_schema
	ON referenced_schema.schema_id = referenced_table.schema_id)
SELECT
	parent_schema,
	parent_table,
	referenced_schema,
	referenced_table,
	foreign_key_name,
	foreign_key_column_list AS foreign_key_column_list,
	referenced_column_list AS referenced_column_list
FROM CTE_FOREIGN_KEY_COLUMNS
WHERE CTE_FOREIGN_KEY_COLUMNS.foreign_key_column_list LIKE '%SpecialOfferID%'
OR CTE_FOREIGN_KEY_COLUMNS.referenced_column_list LIKE '%SpecialOfferID%';
 

The process is almost identical to before. While foreign keys typically are composed of single columns, they can contain multiples, and our goal here is to condense them into a comma-separated list for easy searching and viewing. As such, the TSQL above is very similar in form to our index column search in that we use two subqueries in order to generate column lists, and then join them to the main result set to collect the remainder of the columns we are interested in:

该过程几乎与以前相同。 尽管外键通常由单列组成,但它们可以包含多个列,我们的目标是将它们压缩为逗号分隔的列表,以便于搜索和查看。 因此,上面的TSQL在形式上与我们的索引列搜索非常相似,因为我们使用两个子查询来生成列列表,然后将它们与主要结果集连接起来以收集我们感兴趣的其余列:

The results are much easier to read than the query that generated them A full list of both referenced and referencing objects are provided in order to make understanding the foreign key as simple as possible. In the unfortunate event that column names differ between parent and child, this will allow us to see both side-by-side.

结果比生成它们的查询要容易得多。提供了被引用和引用对象的完整列表,以使对外键的理解尽可能简单。 不幸的是,父级和子级之间的列名不同,这将使我们能够并排看到两者。

Run without a WHERE clause, this query can also be a useful way on its own to quickly view all foreign keys in a database, with easy-to-read schema, table, and column lists.

在没有WHERE子句的情况下运行,此查询本身也可以是一种有用的方法,它可以使用易于理解的模式,表和列列表快速查看数据库中的所有外键。

It gets easier from here! Default constraints are completely defined within a single system view, sys.default_constraints. We’ll grab some additional data along the way so we know what table and column the constraint applies to, but the resulting TSQL remains relatively straight-forward:

从这里变得更容易! 默认约束完全在单个系统视图sys.default_constraints中定义 。 我们将沿途获取一些其他数据,以便我们知道约束适用于哪个表和列,但是生成的TSQL仍然相对简单:

 
SELECT
	default_constraints.name AS default_constraint_name,
	schemas.name AS parent_schema_name,
	objects.name AS parent_table_name,
	columns.name AS parent_column_name,
	default_constraints.definition AS default_definition,
	'Default Constraint' AS object_type
FROM sys.default_constraints
INNER JOIN sys.objects
ON objects.object_id = default_constraints.parent_object_id
INNER JOIN sys.schemas
ON objects.schema_id = schemas.schema_id
INNER JOIN sys.columns
ON columns.object_id = objects.object_id
AND columns.column_id = default_constraints.parent_column_id
WHERE default_constraints.name LIKE '%Quantity%'
OR default_constraints.definition LIKE '%Quantity%';
 

The results are a clean list of details for each default constraint:

结果是每个默认约束的详细清单:

We intentionally check both the name of the default constraint as well as the definition, as we may want to know if a specific default value matches our search terms.

我们有意检查默认约束的名称和定义,因为我们可能想知道特定的默认值是否与我们的搜索词匹配。

Check constraints are searched very similarly using the sys.check_constraints system view:

使用sys.check_constraints系统视图非常类似地搜索检查约束:

 
SELECT
	check_constraints.name AS check_constraint_name,
	schemas.name AS parent_schema_name,
	objects.name AS parent_table_name,
	check_constraints.definition AS check_definition,
	'Check Constraint' AS object_type
FROM sys.check_constraints
INNER JOIN sys.objects
ON objects.object_id = check_constraints.parent_object_id
INNER JOIN sys.schemas
ON objects.schema_id = schemas.schema_id
WHERE check_constraints.name LIKE '%Discount%'
OR check_constraints.definition LIKE '%Discount%';
 

This is similar to what we did for default constraints. Again, we check both the constraint name as well as the definition, and include some parent info to make finding the constraint easier. The output looks like this:

这类似于我们对默认约束所做的操作。 同样,我们同时检查约束名称和定义,并包括一些父信息以使查找约束更加容易。 输出看起来像这样:

DDL触发器 (DDL Triggers)

DDL triggers at the server level are defined separately from everything else discussed in this article and need to be handled separately. The basic metadata can be found in sys.server_triggers and the definitions in sys.server_sql_modules:

服务器级别的DDL触发器是与本文讨论的所有其他方面分开定义的,需要分别进行处理。 基本元数据可在sys.server_triggers中找到,而定义在sys.server_sql_modules中

 
SELECT
	server_triggers.name AS trigger_name,
	parent_class_desc AS trigger_type,
	server_sql_modules.definition AS trigger_definition,
	'Server Trigger' AS object_type
FROM sys.server_triggers
INNER JOIN sys.server_sql_modules
ON server_triggers.object_id = server_sql_modules.object_id
WHERE server_triggers.name LIKE '%Create%'
OR server_sql_modules.definition LIKE '%Create%';
 

The results are straight-forward and provide everything we need to find and potentially analyze a given trigger:

结果简单明了,并提供了我们发现和可能分析给定触发器所需的一切:

Database DDL triggers are a bit left out as well, as they get definitions in sys.sql_modules, but don’t get any associated sys.objects metadata. We’ll handle them completely separately:

数据库DDL触发器也被遗漏了一些,因为它们在sys.sql_modules中获取定义,但没有获取任何关联的sys.objects元数据。 我们将完全分别处理它们:

 
SELECT
	triggers.name AS trigger_name,
	triggers.parent_class_desc AS trigger_type,
	sql_modules.definition AS trigger_definition,
	'Database DDL Trigger' AS object_type
FROM sys.triggers
INNER JOIN sys.sql_modules
ON triggers.object_id = sys.sql_modules.object_id
WHERE parent_class_desc = 'DATABASE'
AND (triggers.name LIKE '%test%' OR sql_modules.definition LIKE '%test%');
 

This will only return database-scoped DDL triggers. Since we are getting other types of triggers and objects elsewhere, our WHERE clause filters down to database-level objects:

这将仅返回数据库作用域的DDL触发器。 由于我们在别处获取其他类型的触发器和对象,因此我们的WHERE子句向下过滤到数据库级对象:

存储过程,视图,函数,规则和触发器 (Stored Procedures, Views, Functions, Rules, and Triggers)

We can use sys.sql_modules and sys.objects to search all of these object types, including their definitions, all at once. sys.sql_modules provides object definitions for the following types of objects (with the sys.objects type in parenthesis):

我们可以使用sys.sql_modulessys.objects一次搜索所有这些对象类型,包括它们的定义。 sys.sql_modules提供以下对象类型的对象定义(括号中为sys.objects类型):

  • Stored Procedure (P)

    存储过程(P)
  • Replication Filter Procedure (RF),

    复制过滤程序(RF),
  • View (V)

    查看(V)
  • DML Trigger (TR)

    DML触发器(TR)
  • Scalar Function (FN)

    标量函数(FN)
  • Inline Table-Valued Function (IF)

    内联表值函数(IF)
  • SQL Table-Valued Function (TF)

    SQL表值函数(TF)
  • Rule (R)

    规则(R)

The resulting query is relatively simple compared to some of our adventures thus far:

与迄今为止的一些冒险活动相比,查询结果相对简单:

 
SELECT
	child_object.name AS object_name,
	parent_schema.name AS parent_schema_name,
	parent_object.name AS parent_object_name,
	sql_modules.definition AS object_definition,
    CASE child_object.type 
		WHEN 'P' THEN 'Stored Procedure'
		WHEN 'RF' THEN 'Replication Filter Procedure'
		WHEN 'V' THEN 'View'
		WHEN 'TR' THEN 'DML Trigger'
		WHEN 'FN' THEN 'Scalar Function'
		WHEN 'IF' THEN 'Inline Table Valued Function'
		WHEN 'TF' THEN 'SQL Table Valued Function'
		WHEN 'R' THEN 'Rule'
	END AS object_type
FROM sys.sql_modules
INNER JOIN sys.objects child_object
ON sql_modules.object_id = child_object.object_id
LEFT JOIN sys.objects parent_object
ON parent_object.object_id = child_object.parent_object_id
LEFT JOIN sys.schemas parent_schema
ON parent_object.schema_id = parent_schema.schema_id
WHERE child_object.name LIKE '%Purchase%'
OR sql_modules.definition LIKE '%Purchase%'
ORDER BY child_object.type_desc
 

The large CASE statement ensures that we provide a useful description of each type that we can read and understand. Feel free to adjust these if you have preferred nomenclature. The abbreviations that are checked are standard and are the same anywhere they are referenced in SQL Server’s metadata.

大的CASE语句确保我们提供了对我们可以阅读和理解的每种类型的有用描述。 如果您有偏好的术语,请随意调整。 选中的缩写是标准的,并且在SQL Server的元数据中引用的任何地方都是相同的。

The results include the parent object, if one exists, which will typically only be for DML triggers, which exist on a specific table:

结果包括父对象(如果存在),该父对象通常仅用于DML触发器(存在于特定表中):

Please note that I have intentionally not used sys.syscomments in order to gather data about the definitions of these objects. Sys.syscomments is deprecated and will be removed in a future version of SQL Server. As always, deprecated objects do not get the same level of maintenance and care from Microsoft that other objects receive, and therefore relying on them introduces risk and potential inaccuracies into our code. Please avoid this system view at all costs, and replace it if you rely on it in any of your existing code.

请注意,我有意不使用sys.syscomments来收集有关这些对象的定义的数据。 Sys.syscomments已过时,将在以后SQL Server版本中删除。 与往常一样,不推荐使用的对象无法像其他对象那样获得Microsoft提供的相同级别的维护和保养,因此依靠它们会在我们的代码中带来风险和潜在的不准确性。 请不惜一切代价避免使用该系统视图,如果您在任何现有代码中都依赖它,请予以替换。

These results are the last of what I would consider a standard SQL Search. The next section will cover the less common use cases of SSIS and SSRS.

这些结果是我认为是标准SQL搜索的最后一个结果。 下一部分将介绍SSIS和SSRS的较不常用用例。

SQL Server报告服务 (SQL Server Reporting Services)

Searching Reporting Services is only necessary if you have it installed, running, and have reports stored in it. If you do, then searching SSRS is only necessary on the report server itself. As a result, you may or may not need this TSQL, but it’s there if you do, and in our final search proc it will be configured as an option so that on systems without SSRS, no extra work is done.

仅当您已安装,正在运行并且已将报表存储在其中时,才需要搜索Reporting Services。 如果这样做,则仅在报表服务器本身上才需要搜索SSRS。 结果,您可能需要或可能不需要此TSQL,但是如果需要,它就在那儿,并且在我们的最终搜索过程中,它将被配置为一个选项,这样在没有SSRS的系统上就不会做任何额外的工作。

Everything we access when searching SSRS will be found specifically within the ReportServer database that is built when Reporting Services is first configured. If you named it anything besides ReportServer, then substitute the correct name in its place. We’ll search for two report-related objects: reports and subscriptions. If a report itself has a name, path, definition, or settings that match the search criteria, then results will be returned for those reports. If a subscription exists that has a description or settings that match the search string, then info on those subscriptions will be returned.

在搜索SSRS时,我们访问的所有内容都将在首次配置Reporting Services时建立的ReportServer数据库中找到。 如果您将其命名为ReportServer以外的其他名称,请替换为正确的名称。 我们将搜索两个与报表相关的对象:报表和订阅。 如果报告本身的名称,路径,定义或设置与搜索条件匹配,则将返回这些报告的结果。 如果存在具有与搜索字符串匹配的描述或设置的订阅,则将返回有关这些订阅的信息。

Our work is limited to two tables, the first of which is Catalog, which provides information about every SSRS object defined on this report server. This table includes a variety of information besides reports, such as SSRS folders, resources, datasets, and data sources. The following query will return information on reports, as well as those additional objects:

我们的工作仅限于两个表,第一个表是Catalog ,它提供有关在此报表服务器上定义的每个SSRS对象的信息。 该表除报告外还包括各种信息,例如SSRS文件夹,资源,数据集和数据源。 以下查询将返回有关报告以及其他对象的信息:

 
SELECT
		Catalog.Path AS SSRS_object_path,
		Catalog.Name AS SSRS_object_name,
		CONVERT(XML, CONVERT(VARBINARY(MAX), Catalog.content)) AS xml_definition,
		CONVERT(NVARCHAR(MAX), CONVERT(XML, CONVERT(VARBINARY(MAX), Catalog.content))) AS text_definition,
		CASE Catalog.Type
			WHEN 1 THEN 'Folder'
			WHEN 2 THEN 'Report'
			WHEN 3 THEN 'Resource'
			WHEN 4 THEN 'Linked Report'
			WHEN 5 THEN 'Data Source'
			WHEN 6 THEN 'Report Model'
			WHEN 7 THEN 'Report Part'
			WHEN 8 THEN 'Shared Dataset'
			ELSE 'Unknown'
		END AS SSRS_object_type
	FROM reportserver.dbo.Catalog
	LEFT JOIN ReportServer.dbo.Subscriptions
	ON Subscriptions.Report_OID = Catalog.ItemID
	WHERE Catalog.Path LIKE '%Test%'
	OR Catalog.Name LIKE '%Test%'
	OR CONVERT(NVARCHAR(MAX), CONVERT(XML, CONVERT(VARBINARY(MAX), Catalog.content))) LIKE '%Test%'
	OR Subscriptions.DataSettings LIKE '%Test%'
	OR Subscriptions.ExtensionSettings LIKE '%Test%'
	OR Subscriptions.Description LIKE '%Test%';
 

Here, we search for our search string within the report path, name, definition, as well as the settings and description of any subscriptions associated with that report. The case statement checks each possible type and returns a friendly name for it. The results will look like this:

在这里,我们在报告路径,名称,定义以及与该报告关联的所有订阅的设置和描述中搜索我们的搜索字符串。 case语句检查每种可能的类型并为其返回友好名称。 结果将如下所示:

This shows a single folder matching our search criteria, as well as a single report.

这显示了一个符合我们搜索条件的文件夹,以及一个报告。

Our second SSRS search query will only check subscriptions, and will return a row for any where the definition or settings match our search string. Since only subscriptions are searched, there is no need for additional joins or CASE statements to make sense of catalog types:

我们的第二个SSRS搜索查询将仅检查订阅,并且将在定义或设置与我们的搜索字符串匹配的任何地方返回一行。 由于仅搜索订阅,因此不需要其他联接或CASE语句即可理解目录类型:

 
SELECT
		Subscriptions.Description AS subscription_description,
		Subscriptions.LastStatus AS subscription_status,
		Subscriptions.DeliveryExtension AS subscription_delivery_method,
		Subscriptions.ExtensionSettings AS subscription_details,
		'Subscription' AS object_type
	FROM ReportServer.dbo.Subscriptions
	WHERE Subscriptions.ExtensionSettings LIKE '%Test%'
	OR Subscriptions.Description LIKE '%Test%'
	OR Subscriptions.DataSettings LIKE '%Test%';
 

The results of this query show a single subscription matching the criteria provided:

该查询的结果显示单个订阅符合提供的条件:

Some additional details are included so that we know the subscription target, status, and details of its definition. Further querying can be done, if needed, to get even more info on the subscription, such as if any reports are using it, and details on each of them.

其中包括一些其他详细信息,以便我们了解订阅目标,状态及其定义的详细信息。 如果需要,可以进行进一步的查询,以获取有关订阅的更多信息,例如是否有任何报告正在使用该订阅,以及每个订阅的详细信息。

SQL Server集成服务 (SQL Server Integration Services)

Searching SSIS packages involves checking those defined within MSDB, which is similar to many of our previous searches, as well as searching those stored on disk, which requires xp_cmdshell or Powershell in order to search.

搜索SSIS软件包涉及检查MSDB中定义的软件包,这与我们以前的许多搜索类似,还需要搜索存储在磁盘上的软件包,这需要xp_cmdshell或Powershell才能进行搜索。

Our first search will be the simpler of the two, pulling data from MSDB and returning a list of SSIS packages, along with the details in both XML and as text:

我们的第一个搜索将是两者中的简单操作,即从MSDB中提取数据并返回SSIS包列表以及XML和文本形式的详细信息:

 
WITH CTE_SSIS AS (
SELECT
	pf.foldername + '\'+ p.name AS full_path,
	CONVERT(XML,CONVERT(VARBINARY(MAX),packagedata)) AS package_details_XML,
	CONVERT(NVARCHAR(max), CONVERT(XML,CONVERT(VARBINARY(MAX),packagedata))) AS package_details_text,
	'SSIS Package (MSDB)' AS object_type
FROM msdb.dbo.sysssispackages p
INNER JOIN msdb.dbo.sysssispackagefolders pf
ON p.folderid = pf.folderid)
SELECT
	*
FROM CTE_SSIS
WHERE CTE_SSIS.package_details_text LIKE '%test%'
OR CTE_SSIS.full_path LIKE '%test%';
 

The results show any SSIS packages defined within MSDB:

结果显示了MSDB中定义的所有SSIS软件包:

Here, we found a lone maintenance plan defined on my local server. For the object type, we explicitly label it as an SSIS package that was read from MSDB, which will help when locating it later on (if anything needs to be done with it).

在这里,我们找到了在本地服务器上定义的一个单独的维护计划。 对于对象类型,我们将其明确标记为从MSDB读取的SSIS包,这将在以后查找它(如果需要对其进行任何处理)时有所帮助。

Last, but not least, we have the chore of searching any SSIS packages that are located on disk. For this demo, we’ll use xp_cmdshell to search them, but Powershell or a file search utility could be used instead, if needed. We need to define up front the folder in which SSIS packages reside. As with SSRS, if you are not using SSIS, there is no need to search for SSIS packages. Here is TSQL that will check a given folder on disk for SSIS packages in XML format and search those definitions for our search terms:

最后但并非最不重要的是,我们需要搜索磁盘上的所有SSIS软件包。 对于此演示,我们将使用xp_cmdshell进行搜索,但是如果需要,可以使用Powershell或文件搜索实用程序代替。 我们需要预先定义SSIS包所在的文件夹。 与SSRS一样,如果您不使用SSIS,则无需搜索SSIS软件包。 这是TSQL,它将在磁盘上的给定文件夹中检查XML格式的SSIS包,并在这些定义中搜索我们的搜索词:

 
CREATE TABLE ##ssis_data
  (  full_path	          NVARCHAR(MAX),
     package_details_XML  XML,
     package_details_text NVARCHAR(MAX) );
 
DECLARE @pkg_directory NVARCHAR(MAX) = 'C:\SSIS_Packages';
DECLARE @ssis_package_name NVARCHAR(MAX);
DECLARE @sql_command VARCHAR(4000);
 
SELECT @sql_command = 'dir "' + @pkg_directory + '" /A-D /B /S ';
 
INSERT INTO ##ssis_data
	(full_path)
EXEC Xp_cmdshell @sql_command;
 
DECLARE SSIS_CURSOR CURSOR FOR
SELECT full_path FROM ##ssis_data;
 
OPEN SSIS_CURSOR;
 
FETCH NEXT FROM SSIS_CURSOR INTO @ssis_package_name;
 
WHILE @@FETCH_STATUS = 0
  BEGIN
      SELECT @sql_command = 'WITH CTE_SSIS_PACKAGES AS (
						SELECT
							''' + @ssis_package_name + ''' AS full_path,
							CONVERT(XML, SSIS_PACKAGE.bulkcolumn) AS package_details_XML
						FROM OPENROWSET(BULK ''' + @ssis_package_name + ''', single_blob) AS SSIS_PACKAGE)
					  UPDATE SSIS_DATA 
							SET package_details_XML = CTE_SSIS_PACKAGES.package_details_XML
                      FROM CTE_SSIS_PACKAGES
                      INNER JOIN ##ssis_data SSIS_DATA
					  ON CTE_SSIS_PACKAGES.full_path = SSIS_DATA.full_path;'
      FROM ##ssis_data;
 
      EXEC (@sql_command);
      FETCH NEXT FROM SSIS_CURSOR INTO @ssis_package_name;
  END
 
CLOSE SSIS_CURSOR;
DEALLOCATE SSIS_CURSOR;
 
UPDATE SSIS_DATA
SET    package_details_text = CONVERT(NVARCHAR(MAX), SSIS_DATA.package_details_XML)
FROM   ##ssis_data SSIS_DATA;
 
SELECT
	*,
	'SSIS Package (File System)' AS object_type
FROM   ##ssis_data SSIS_DATA
WHERE  SSIS_DATA.package_details_text LIKE '%test%'
OR SSIS_DATA.full_path LIKE '%test%';
 
DROP TABLE ##ssis_data;
 

There’s no candy-coating this one. We need to go out to disk and parse each XML file within the predefined SSIS package folder. Once we have returned the directory listing of each file in the folder, we’ll iterate through each one, bulk loading the data and storing the XML definition for the SSIS package.

这个没有糖衣。 我们需要进入磁盘并解析预定义的SSIS包文件夹中的每个XML文件。 返回文件夹中每个文件的目录列表后,我们将遍历每个文件,批量加载数据并存储SSIS包的XML定义。

Once we have this data in our temp table, the remainder of our work is the same as it was earlier. We update the text version of the package details so that we have a non-XML-typed string to run a text search against, and then return our data set, filtering out for our search criteria. Running this on my local server returns the following:

将这些数据保存到临时表后,其余工作将与之前的工作相同。 我们更新了软件包详细信息的文本版本,以便我们有一个非XML类型的字符串来对其进行文本搜索,然后返回我们的数据集,以过滤出我们的搜索条件。 在本地服务器上运行此命令将返回以下内容:

The results returned are exactly the same as in our previous SSIS search demo, except that we have explicitly defined the object type as “SSIS Package (file system), in order to differentiate it with any defined within MSDB.

返回的结果与我们先前的SSIS搜索演示完全相同,除了我们已将对象类型显式定义为“ SSIS包(文件系统)”,以便与MSDB中定义的任何对象类型区分开。

In the event that any SSIS packages are encrypted within MSDB, then there is no simple way to search them using the framework that we have defined. In the spirit of keeping things as simple as possible, we’ll ignore these for this article at least, and perhaps revisit them in the future (with a vengeance).

如果在MSDB中对任何SSIS包进行了加密,则没有简单的方法使用我们定义的框架来搜索它们。 本着使事情尽可能简单的精神,我们至少会在本文中忽略这些内容,并可能在将来再次进行复审(以复仇的方式)。

结论 (Conclusion)

In this article, we introduced a wide variety of system views and demonstrated how each can be used in order to return information about our SQL Server, both for the server as a whole as well as metadata specific to each individual database.

在本文中,我们介绍了各种系统视图,并演示了如何使用每种视图来返回有关SQL Server的信息,包括整个服务器以及特定于每个数据库的元数据。

In my next article, we will bring everything from here together into a single script that can search all of the components discussed thus far within a single execution. The resulting script will be a tool that you can copy, tweak, customize, and deploy to any server where searching is needed!

在我的下一篇文章中 ,我们将把这里的所有内容放到一个脚本中,该脚本可以在一次执行中搜索到目前为止讨论的所有组件。 生成的脚本将成为您可以复制,调整,自定义并部署到需要搜索的任何服务器的工具!

翻译自: https://www.sqlshack.com/searching-sql-server-made-easy-searching-catalog-views/

sql server 视图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值