我最近正在查询我们的内部数据库库存工具以获取服务器、实例和数据库的列表,并向每个服务器、实例和数据库添加相应的状态。
关系图
Server ˂-- 1 : n --˃ Instance ˂-- 1 : n --˃ Database
˄ ˄ ˄
| | |
| 1 : 1 |
| | |
| ˅ |
+-- 1 : 1 --˃ Status ˂-- 1 : 1 --+
读作:
...一个服务器可以有多个实例
...一个实例可以有多个数据库
...一个服务器、一个实例和一个数据库可以有一个状态
设置
状态表
CREATE TABLE [Status]
(
StatusID int,
StatusName char(20),
);
状态数据
INSERT INTO [Status] (StatusID, StatusName)
VALUES
(1,'Productive'),
(2,'Prod ACC'),
(3,'Prod APP'),
(4,'Test'),
(5,'Test ACC'),
(6,'Test APP'),
(7,'OFFLINE'),
(8,'Reserved'),
(9,'Decommisioned');
服务器表
CREATE TABLE [Server]
(
ServerID int,
ServerName char(20),
ServerStatusID int
);
服务器数据
INSERT INTO [Server] (ServerID, ServerName, ServerStatusID)
VALUES
(1,'FirstServer',1),
(2,'SecondServer',2),
(3,'ThirdServer',5),
(4,'FourthServer',8),
(5,'FifthServer',8);
实例表
CREATE TABLE [Instance]
(
InstanceID int,
ServerID int,
InstanceName char(30),
InstanceStatusID int
);
实例数据
INSERT INTO [Instance]
(InstanceID, ServerID, InstanceName, InstanceStatusID)
VALUES
(1,1,'GENERAL',1),
(2,1,'TAXES',1),
(3,2,'GENERAL',9),
(4,2,'SOCIAL',2),
(5,3,'GENERAL',5),
(6,3,'FBI',8),
(7,5,'COMINGSOON',8);
数据库表
CREATE TABLE [Database]
(
DatabaseID int,
InstanceID int,
DatabaseName char(30),
DatabaseStatusID int
);
数据库数据
INSERT INTO [Database]
(DatabaseID, InstanceID, DatabaseName, DatabaseStatusID)
VALUES
(1,1,'master',1),
(2,1,'model',1),
(3,1,'msdb',1),
(4,1,'UserDB1',1),
(5,2,'master',1),
(6,2,'model',1),
(7,2,'msdb',1),
(8,2,'TaxesDB',1),
(9,4,'master',2),
(10,4,'model',2),
(11,4,'msdb',2),
(12,4,'HealthCareDB',2),
(13,5,'master',5),
(14,5,'model',5),
(15,5,'msdb',5),
(16,5,'GeneralUserDB',5),
(17,6,'master',8),
(18,6,'model',8),
(19,6,'msdb',8),
(20,6,'CriminalDB',8);
不涉及状态表的 SELECT 语句
初始 SELECT 语句仅涉及连接三个表:服务器、实例、数据库,如下所示:
-- Simple SELECT to get all information on Servers, Instances and Databases
-- The status of the server, instance or database is not returned
SELECT
ServerName,
InstanceName,
DatabaseName
FROM [Server] as srv
LEFT JOIN [Instance] as ins
ON srv.ServerID = ins.ServerID
LEFT JOIN [Database] as dbs
ON ins.InstanceID = dbs.InstanceID;
1. 声明的结果
请注意...
- 有一台没有实例和数据库的服务器
- 有一个实例没有数据库
服务器名称 | 实例名称 | 数据库名称 |
---|---|---|
第一服务器 | 一般的 | 掌握 |
第一服务器 | 一般的 | 模型 |
第一服务器 | 一般的 | 数据库数据库 |
第一服务器 | 一般的 | 用户数据库1 |
第一服务器 | 税收 | 掌握 |
第一服务器 | 税收 | 模型 |
第一服务器 | 税收 | 数据库数据库 |
第一服务器 | 税收 | 税收数据库 |
第二服务器 | 一般的 | 无效的 |
第二服务器 | 社会的 | 掌握 |
第二服务器 | 社会的 | 模型 |
第二服务器 | 社会的 | 数据库数据库 |
第二服务器 | 社会的 | 医疗保健数据库 |
第三服务器 | 一般的 | 掌握 |
第三服务器 | 一般的 | 模型 |
第三服务器 | 一般的 | 数据库数据库 |
第三服务器 | 一般的 | 通用用户数据库 |
第三服务器 | 联邦调查局 | 掌握 |
第三服务器 | 联邦调查局 | 模型 |
第三服务器 | 联邦调查局 | 数据库数据库 |
第三服务器 | 联邦调查局 | 刑事数据库 |
第四服务器 | 无效的 | 无效的 |
第五服务器 | 即将推出 | 无效的 |
涉及 Status 表的 SELECT 语句
在下一个语句中,我决定将状态添加到每个元素(服务器、实例、数据库),并JOIN
使用表编辑每个表,Status
如下所示:
-- Advanced SELECT to get all information on Servers, Instances and Databases
-- including their status
SELECT
ServerName,
srvst.StatusName,
InstanceName,
insst.StatusName,
DatabaseName,
dbsst.StatusName
FROM [Server] as srv
JOIN [Status] as srvst
ON srv.ServerStatusID = srvst.StatusID
LEFT JOIN [Instance] as ins
ON srv.ServerID = ins.ServerID
JOIN [Status] as insst
ON ins.InstanceStatusID = insst.StatusID
LEFT JOIN [Database] as dbs
ON ins.InstanceID = dbs.InstanceID
JOIN [Status] as dbsst
ON dbs.DatabaseStatusID = dbsst.StatusID
;
2. 声明结果
令我惊讶的是,没有实例和数据库的服务器以及有实例但没有数据库的服务器不再列出:
服务器名称 | 状态名称 | 实例名称 | 状态名称 | 数据库名称 | 状态名称 |
---|---|---|---|---|---|
第一服务器 | 富有成效 | 一般的 | 富有成效 | 掌握 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 模型 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 数据库数据库 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 用户数据库1 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 掌握 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 模型 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 数据库数据库 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 税收数据库 | 富有成效 |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 掌握 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 模型 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 数据库数据库 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 医疗保健数据库 | 产品ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 掌握 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 模型 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 数据库数据库 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 通用用户数据库 | 测试ACC |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 掌握 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 模型 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 数据库数据库 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 刑事数据库 | 预订的 |
调查结果/解决方案
通过反复试验的方法检查了各种选项后,我发现必须将表JOIN
上的Status
更改为 a ,LEFT JOIN
以允许该语句显示没有实例或数据库的服务器,并显示没有数据库的实例:
-- Advanced SELECT to get all information on Servers, Instances and Databases
-- including their status
SELECT
ServerName,
srvst.StatusName,
InstanceName,
insst.StatusName,
DatabaseName,
dbsst.StatusName
FROM [Server] as srv
LEFT JOIN [Status] as srvst
ON srv.ServerStatusID = srvst.StatusID
LEFT JOIN [Instance] as ins
ON srv.ServerID = ins.ServerID
LEFT JOIN [Status] as insst
ON ins.InstanceStatusID = insst.StatusID
LEFT JOIN [Database] as dbs
ON ins.InstanceID = dbs.InstanceID
LEFT JOIN [Status] as dbsst
ON dbs.DatabaseStatusID = dbsst.StatusID;
3. 声明的结果
服务器名称 | 状态名称 | 实例名称 | 状态名称 | 数据库名称 | 状态名称 |
---|---|---|---|---|---|
第一服务器 | 富有成效 | 一般的 | 富有成效 | 掌握 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 模型 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 数据库数据库 | 富有成效 |
第一服务器 | 富有成效 | 一般的 | 富有成效 | 用户数据库1 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 掌握 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 模型 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 数据库数据库 | 富有成效 |
第一服务器 | 富有成效 | 税收 | 富有成效 | 税收数据库 | 富有成效 |
第二服务器 | 产品ACC | 一般的 | 退役 | 无效的 | 无效的 |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 掌握 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 模型 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 数据库数据库 | 产品ACC |
第二服务器 | 产品ACC | 社会的 | 产品ACC | 医疗保健数据库 | 产品ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 掌握 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 模型 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 数据库数据库 | 测试ACC |
第三服务器 | 测试ACC | 一般的 | 测试ACC | 通用用户数据库 | 测试ACC |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 掌握 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 模型 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 数据库数据库 | 预订的 |
第三服务器 | 测试ACC | 联邦调查局 | 预订的 | 刑事数据库 | 预订的 |
第四服务器 | 预订的 | 无效的 | 无效的 | 无效的 | 无效的 |
第五服务器 | 预订的 | 即将推出 | 预订的 | 无效的 | 无效的 |
参考资料
这里有一个db<>fiddle的链接来重现我的发现。
问题
为什么 SQL Server 需要在表LEFT JOIN
中Status
显示不存在的子项以及查询来显示这些项?
您需要嵌套连接。否则,会发生的情况是,它期望每个单独的连接子句返回一个结果,但如果前一个连接子句没有返回任何结果,则它不能返回结果。
Essentially, you want the server to take the result of the inner-join of
Instance
andStatus
, and left-join all of that back toServer
.In SQL Server, the parenthesis are not essential, they are purely for readability. The key is putting
JOIN table2 ON ...
inside another join, so it comes out toLEFT JOIN table1 JOIN table2 ON ... ON ...
ie theON
clauses are nested.This is the exact equivalent of using derived subqueries:
因为子项与状态表的联接是基于 ins.InstanceStatusID、dbs.DatabaseStatusID 进行的,当其之前的联接发生在某些行中时,结果为 NULL。
当您稍后执行内部联接时,它会显示两个表中匹配的行,并且要获取具有空值的行,您将必须使用左联接。
可视化相同的:
使用左连接后,会从左结果集中提取所有记录,并从连接到结果集的两个表中提取匹配值
It doesn't, in general. It is just one of the ways to express your requirement that produces the results you are after.
Your query tries to (inner) join from a null-extended row (created by an outer join) to the Status table using a join predicate that looks for an equality match on StatusID values.
This predicate does not return true because StatusID is null on one side, there is no matching null in the Status table, and the equality predicate would reject null matches anyway. Since the join predicate fails and you have specified an inner join, no result row is produced.
For clarity, the inner join to Status could produce a result with a different join predicate.
For example, there could be a null StatusID in the Status table (something your schema allows) and the join predicate could use
IS NOT DISTINCT FROM
or an equivalent test where nulls match. You could also useCOALESCE
orISNULL
in the join predicate to select a particular status (as a default) without adding any new status rows.There are many possibilities; the point is you could write a join predicate that would produce a match. The one you have does not.
It is possible you thought SQL Server would skip the join to Status for any rows previously null-extended by an outer join, but things don't work that way. An outer join is not an 'optional' join with subsequent short-circuiting. Perhaps some people have a mental image of them functioning that way.
Making any following joins 'outer' as a way to fix the output is close to using 'magic'—a recipe that works but isn't understood.
Why does it work here? Because, with a left outer join, the null-extended row is preserved despite there being no match in Status according to the join predicate. The missing status columns are populated with null, as usual.
An alternative solution
People do seem to find left outer joins more intuitive to work with, but there's no particular reason to prefer them otherwise. You don't have to start at the top of the hierarchy and 'join down' to rows that may or may not be present.
Let's see what happens with your schema if we instead start at the bottom of the hierarchy (the [Database] table) and work up. The first join to Status is straightforward:
Moving up the hierarchy, we now need to add rows from the Instance table. We can't use a left outer join or inner join because there may be an instance without a database. The natural solution is to use a right join, with the associated inner join to Status:
There is no need for 'nested' joins here. We quite naturally add in any instances without a database, adding null-extended database rows as necessary. All instances are accounted for, so the inner join to Status is perfectly correct.
We follow the same approach to add in rows from the Server table, along with its status:
This produces the results you want, without invoking any magic.
There are any number of ways to write a correct query specification for your requirement. I show a
RIGHT JOIN
alternative mostly because people seem somewhat scared by right outer joins or consider them redundant.