Recentemente, consultei nossa ferramenta interna de inventário de banco de dados para obter uma lista de servidores, instâncias e bancos de dados e adicionei o status correspondente a cada servidor, instância e banco de dados.
Diagrama de Relacionamento
Server ˂-- 1 : n --˃ Instance ˂-- 1 : n --˃ Database
˄ ˄ ˄
| | |
| 1 : 1 |
| | |
| ˅ |
+-- 1 : 1 --˃ Status ˂-- 1 : 1 --+
Leia como:
...um servidor pode ter múltiplas instâncias
...uma instância pode ter vários bancos de dados
...um servidor, uma instância e um banco de dados podem ter um status
Configurar
Tabela de status
CREATE TABLE [Status]
(
StatusID int,
StatusName char(20),
);
Dados de status
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');
Tabela de servidores
CREATE TABLE [Server]
(
ServerID int,
ServerName char(20),
ServerStatusID int
);
Dados do servidor
INSERT INTO [Server] (ServerID, ServerName, ServerStatusID)
VALUES
(1,'FirstServer',1),
(2,'SecondServer',2),
(3,'ThirdServer',5),
(4,'FourthServer',8),
(5,'FifthServer',8);
Tabela de Instâncias
CREATE TABLE [Instance]
(
InstanceID int,
ServerID int,
InstanceName char(30),
InstanceStatusID int
);
Dados da instância
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);
Tabela de banco de dados
CREATE TABLE [Database]
(
DatabaseID int,
InstanceID int,
DatabaseName char(30),
DatabaseStatusID int
);
Dados do banco de dados
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);
Instrução SELECT sem tabela de status envolvida
A instrução SELECT inicial envolvia simplesmente juntar as três tabelas: servidor, instância, banco de dados e era a seguinte:
-- 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;
Resultados de 1. Declaração
POR FAVOR, OBSERVE ISSO...
- existe um servidor sem instância e banco de dados
- há uma instância sem banco de dados
Nome do servidor | Nome da instância | Nome do banco de dados |
---|---|---|
Primeiro Servidor | EM GERAL | mestre |
Primeiro Servidor | EM GERAL | modelo |
Primeiro Servidor | EM GERAL | msdb |
Primeiro Servidor | EM GERAL | UsuárioDB1 |
Primeiro Servidor | IMPOSTOS | mestre |
Primeiro Servidor | IMPOSTOS | modelo |
Primeiro Servidor | IMPOSTOS | msdb |
Primeiro Servidor | IMPOSTOS | ImpostosDB |
Segundo Servidor | EM GERAL | nulo |
Segundo Servidor | SOCIAL | mestre |
Segundo Servidor | SOCIAL | modelo |
Segundo Servidor | SOCIAL | msdb |
Segundo Servidor | SOCIAL | HealthCareDB |
Terceiro Servidor | EM GERAL | mestre |
Terceiro Servidor | EM GERAL | modelo |
Terceiro Servidor | EM GERAL | msdb |
Terceiro Servidor | EM GERAL | GeralUserDB |
Terceiro Servidor | FBI | mestre |
Terceiro Servidor | FBI | modelo |
Terceiro Servidor | FBI | msdb |
Terceiro Servidor | FBI | CriminalDB |
Quarto Servidor | nulo | nulo |
Quinto Servidor | EM BREVE | nulo |
Instrução SELECT envolvendo tabela de status
Na próxima instrução decido adicionar o status a cada elemento (servidor, instância, banco de dados) e JOIN
editar cada tabela com a Status
tabela da seguinte forma:
-- 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
;
Resultados de 2. Declaração
Para minha surpresa o servidor sem instância e banco de dados e o servidor com instância mas sem banco de dados não estavam mais listados:
Nome do servidor | StatusNome | Nome da instância | StatusNome | Nome do banco de dados | StatusNome |
---|---|---|---|---|---|
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | mestre | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | modelo | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | msdb | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | UsuárioDB1 | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | mestre | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | modelo | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | msdb | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | ImpostosDB | Produtivo |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | mestre | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | modelo | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | msdb | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | HealthCareDB | ACC de produção |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | mestre | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | modelo | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | msdb | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | GeralUserDB | Teste ACC |
Terceiro Servidor | Teste ACC | FBI | Reservado | mestre | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | modelo | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | msdb | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | CriminalDB | Reservado |
Descobertas / Solução
Depois de verificar várias opções com uma abordagem de tentativa e erro, descobri que JOIN
on the Status
table teve que ser alterado para a LEFT JOIN
para permitir que a instrução exibisse o servidor sem uma instância ou um banco de dados e exibisse a instância sem um banco de dados :
-- 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;
Resultados de 3. Declaração
Nome do servidor | StatusNome | Nome da instância | StatusNome | Nome do banco de dados | StatusNome |
---|---|---|---|---|---|
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | mestre | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | modelo | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | msdb | Produtivo |
Primeiro Servidor | Produtivo | EM GERAL | Produtivo | UsuárioDB1 | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | mestre | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | modelo | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | msdb | Produtivo |
Primeiro Servidor | Produtivo | IMPOSTOS | Produtivo | ImpostosDB | Produtivo |
Segundo Servidor | ACC de produção | EM GERAL | Desativado | nulo | nulo |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | mestre | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | modelo | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | msdb | ACC de produção |
Segundo Servidor | ACC de produção | SOCIAL | ACC de produção | HealthCareDB | ACC de produção |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | mestre | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | modelo | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | msdb | Teste ACC |
Terceiro Servidor | Teste ACC | EM GERAL | Teste ACC | GeralUserDB | Teste ACC |
Terceiro Servidor | Teste ACC | FBI | Reservado | mestre | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | modelo | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | msdb | Reservado |
Terceiro Servidor | Teste ACC | FBI | Reservado | CriminalDB | Reservado |
Quarto Servidor | Reservado | nulo | nulo | nulo | nulo |
Quinto Servidor | Reservado | EM BREVE | Reservado | nulo | nulo |
Material de referência
Aqui está um link para o db<>fiddle para reproduzir minhas descobertas.
Pergunta
Por que o SQL Server exige um LEFT JOIN
na Status
tabela para itens filhos que não existem e para que a consulta exiba esses itens?
Você precisa aninhar suas junções. Caso contrário, o que está acontecendo é que ele espera que cada cláusula de junção individual retorne um resultado, mas não poderá se a anterior não retornar nada.
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:
Porque a junção com a tabela de status para os itens filhos está acontecendo com base em ins.InstanceStatusID, dbs.DatabaseStatusID que acaba sendo NULL quando as junções anteriores ocorrem em algumas linhas.
Quando você faz uma junção interna posteriormente, ele mostra as linhas correspondentes em ambas as tabelas e para obter também as linhas com valores nulos, você terá que usar a junção à esquerda.
Para visualizar o mesmo:
Que depois de usar a junção à esquerda extrai todos os registros do conjunto de resultados esquerdo e os valores correspondentes de ambas as tabelas unidas ao conjunto de resultados
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.