Partilhar via


Problemas de design do T-SQL

Aplica-se a: SQL Server Banco de Dados SQL do Azure Instância Gerenciada de SQL do Azure Banco de Dados SQL no Microsoft Fabric

Quando você analisa o código T-SQL em seu projeto de banco de dados, um ou mais avisos podem ser categorizados como problemas de design. Você deve resolver problemas de design para evitar as seguintes situações:

  • Alterações subsequentes em seu banco de dados podem causar problemas nos aplicativos que dependem dele.
  • O código pode não produzir o resultado esperado.
  • O código pode ser desfeito se você executá-lo com versões futuras do SQL Server.

Em geral, você não deve suprimir um problema de design porque ele pode causar problemas em seu aplicativo, agora ou no futuro.

As regras fornecidas identificam os seguintes problemas de design:

SR0001: evite SELECT * em procedimentos armazenados, exibições e funções com valor de tabela

Se você usar um caractere curinga em um procedimento armazenado, exibição ou função com valor de tabela para selecionar todas as colunas em uma tabela ou exibição, o número ou a forma das colunas retornadas poderá ser alterado se a tabela ou exibição subjacente for alterada. A forma de uma coluna é uma combinação de seu tipo e tamanho. Essa variação pode causar problemas em aplicativos que consomem o procedimento armazenado, a exibição ou a função com valor de tabela, pois esses consumidores esperam um número diferente de colunas.

Como corrigir violações

Você pode proteger os consumidores do procedimento armazenado, exibição ou função com valor de tabela contra alterações de esquema substituindo o caractere curinga por uma lista totalmente qualificada de nomes de coluna.

Exemplo

O exemplo a seguir primeiro define uma tabela chamada [Table2] e, em seguida, define dois procedimentos armazenados. O primeiro procedimento contém um SELECT *, que viola a regra SR0001. O segundo procedimento evita SELECT * e lista explicitamente as colunas na instrução SELECT.

CREATE TABLE [dbo].[Table2]
(
[ID] INT NOT NULL IDENTITY(0, 1),
[c1] INT NOT NULL,
[Comment] NVARCHAR (50)
)
ON [PRIMARY]

CREATE PROCEDURE [dbo].[procWithWarning]
AS
BEGIN
-- Contains code that breaks rule SR0001
SELECT *
FROM [dbo].[Table2]
END

CREATE PROCEDURE [dbo].[procFixed]
AS
BEGIN
-- Explicitly lists the column names in a SELECT statement
SELECT [dbo].[Table2].[ID], [dbo].[Table2].[c1], [dbo].[Table2].[Comment]
FROM [dbo].[Table2]
END

SR0008: considere usar SCOPE_IDENTITY em vez de @@IDENTITY

Como @@IDENTITY é um valor de identidade global, ele pode ter sido atualizado fora do escopo atual e obtido um valor inesperado. Os gatilhos, incluindo gatilhos aninhados usados pela replicação, podem atualizar @@IDENTITY fora do escopo atual.

Como corrigir violações

Para resolver esse problema, você deve substituir as referências a @@IDENTITY por SCOPE_IDENTITY, que retorna o valor de identidade mais recente no escopo da instrução do usuário.

Exemplo

No primeiro exemplo, @@IDENTITY é usado em um procedimento armazenado que insere dados em uma tabela. Em seguida, a tabela é publicada para replicação de mesclagem, que adiciona gatilhos às tabelas publicadas. Portanto, @@IDENTITY pode retornar o valor da operação de inserção em uma tabela do sistema de replicação em vez da operação de inserção em uma tabela de usuário.

A tabela Sales.Customer tem um valor de identidade máximo de 29483. Se você inserir uma linha na tabela, @@IDENTITY e SCOPE_IDENTITY() retornarão valores diferentes. SCOPE_IDENTITY() retorna o valor da operação de inserção em uma tabela de usuário, mas @@IDENTITY retorna o valor da operação de inserção na tabela do sistema de replicação.

O segundo exemplo mostra como você pode usar SCOPE_IDENTITY() para acessar o valor de identidade inserido e resolver o aviso.

CREATE PROCEDURE [dbo].[ProcedureWithWarning]
@param1 INT,
@param2 NCHAR(1),
@Param3 INT OUTPUT
AS
BEGIN
INSERT INTO Sales.Customer ([TerritoryID],[CustomerType]) VALUES (@param1,@param2);

SELECT @Param3 = @@IDENTITY
END

CREATE PROCEDURE [dbo].[ProcedureFixed]
@param1 INT,
@param2 NCHAR(1),
@param3 INT OUTPUT
AS
BEGIN
INSERT INTO Sales.Customer ([TerritoryID],[CustomerType]) VALUES (@param1,@param2);

SELECT @Param3 = SCOPE_IDENTITY()
END

SR0009: evite usar tipos de comprimento variável que sejam de tamanho 1 ou 2

Ao usar tipos de dados de comprimento variável, como VARCHAR, NVARCHAR e VARBINARY, você incorre em um custo de armazenamento adicional para rastrear o comprimento do valor armazenado no tipo de dados. Além disso, as colunas de comprimento variável são armazenadas após todas as colunas de comprimento fixo, o que pode ter implicações de desempenho. Você também receberá um aviso se declarar um tipo de comprimento variável, como VARCHAR, mas não especificar nenhum comprimento. Esse aviso ocorre porque, se não for especificado, o comprimento padrão é 1.

Como corrigir violações

Se o comprimento do tipo for muito pequeno (tamanho 1 ou 2) e consistente, declare-os como um tipo de comprimento fixo, como CHAR, NCHAR e BINARY.

Exemplo

Este exemplo mostra definições para duas tabelas. A primeira tabela declara uma cadeia de caracteres de comprimento variável como tendo comprimento 2. Em vez disso, a segunda tabela declara uma cadeia de caracteres de comprimento fixo, o que evita o aviso.

CREATE TABLE [dbo].[TableWithWarning]
(
[ID] INT NOT NULL IDENTITY(0, 1),
[c1] INT NOT NULL PRIMARY KEY,
[c2] INT,
[c3] INT,
[SmallString] VARCHAR(2)
)
ON [PRIMARY]

CREATE TABLE [dbo].[FixedTable]
(
[ID] INT NOT NULL IDENTITY(0, 1),
[c1] INT NOT NULL PRIMARY KEY,
[c2] INT,
[c3] INT,
[SmallString] CHAR(2)
)
ON [PRIMARY]

Os dados para tipos de comprimento variável são armazenados fisicamente após os dados para tipos de comprimento fixo. Portanto, você causará movimentação de dados se alterar uma coluna de variável para comprimento fixo em uma tabela que não esteja vazia.

SR0010: evite usar sintaxe preterida ao unir tabelas ou exibições

As junções que usam a sintaxe preterida se enquadram em duas categorias:

  • Junção interna: para uma junção interna, os valores nas colunas que estão sendo unidas são comparados usando um operador de comparação como =, <, >= e assim por diante. As junções internas retornam linhas somente se pelo menos uma linha de cada tabela corresponder à condição de junção.
  • Junção externa: as junções externas retornam todas as linhas de pelo menos uma das tabelas ou exibições especificadas na cláusula FROM, contanto que essas linhas atendam algum critério de pesquisa WHERE ou HAVING. Se você usar = ou = para especificar uma junção externa, estará usando a sintaxe preterida.

Como corrigir violações

Para corrigir uma violação em uma junção interna, use a sintaxe INNER JOIN.

Para corrigir uma violação em uma junção externa, use a sintaxe apropriada OUTER JOIN. Você tem as seguintes opções:

  • LEFT OUTER JOIN ou LEFT JOIN
  • RIGHT OUTER JOIN ou RIGHT JOIN

Exemplos da sintaxe preterida e da sintaxe atualizada são fornecidos nos exemplos a seguir. Encontre mais informações sobre junções em Junções.

Exemplos

Os seis exemplos demonstram como usar as seguintes opções:

  1. O exemplo 1 demonstra a sintaxe preterida para uma junção interna.
  2. O exemplo 2 demonstra como você pode atualizar o exemplo 1 para usar a sintaxe atual.
  3. O exemplo 3 demonstra a sintaxe preterida para uma junção externa esquerda.
  4. O exemplo 4 demonstra como você pode atualizar o exemplo 2 para usar a sintaxe atual.
  5. O exemplo 5 demonstra a sintaxe preterida para uma junção externa direita.
  6. O exemplo 6 demonstra como você pode atualizar o exemplo 5 para usar a sintaxe atual.
-- Example 1: Deprecated syntax for an inner join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] T2, [dbo].[Table1] T1
WHERE [T1].[ID] = [T2].[ID]

-- Example 2: Current syntax for an inner join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] AS T2
INNER JOIN [dbo].[Table1] as T1
ON [T2].[ID] = [T2].[ID]

-- Example 3: Deprecated syntax for a left outer join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] T2, [dbo].[Table1] T1
WHERE [T1].[ID] *= [T2].[ID]

-- Example 4: Fixed syntax for a left outer join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] AS T2
LEFT OUTER JOIN [dbo].[Table1] as T1
ON [T2].[ID] = [T2].[ID]

-- Example 5: Deprecated syntax for a right outer join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] T2, [dbo].[Table1] T1
WHERE [T1].[ID] =* [T2].[ID]

-- Example 6: Fixed syntax for a right outer join
SELECT [T2].[c3], [T1].[c3]
FROM [dbo].[Table2] AS T2
RIGHT OUTER JOIN [dbo].[Table1] as T1
ON [T2].[ID] = [T2].[ID]

SR0013: o parâmetro de saída (parameter) não é preenchido em todos os caminhos de código

Essa regra identifica o código no qual o parâmetro de saída não está definido como um valor em um ou mais caminhos de código por meio do procedimento armazenado ou da função. Essa regra não identifica em quais caminhos o parâmetro de saída deve ser definido. Se vários parâmetros de saída tiverem esse problema, um aviso será exibido para cada parâmetro.

Como corrigir violações

Você pode corrigir esse problema de uma das duas maneiras. Você pode corrigir esse problema mais facilmente se inicializar os parâmetros de saída para um valor padrão no início do corpo do procedimento. Como alternativa, você também pode definir o parâmetro de saída como um valor nos caminhos de código específicos nos quais o parâmetro não está definido. No entanto, você pode ignorar um caminho de código incomum em um procedimento complexo.

Importante

Especificar um valor na declaração de procedimento, como CREATE PROC MyProcedure (@param1 INT = 10 OUTPUT) não resolverá o problema. Você deve atribuir um valor ao parâmetro de saída dentro do corpo do procedimento.

Exemplo

O exemplo a seguir mostra dois procedimentos simples. O primeiro procedimento não define o valor do parâmetro de saída, @Sum. O segundo procedimento inicializa o parâmetro @Sum no início do procedimento, o que garante que o valor será definido em todos os caminhos de código.

CREATE PROCEDURE [dbo].[procedureHasWarning]
(
@Value1 BIGINT,
@Value2 INT,
@Value3 INT,
@Sum INT OUTPUT
)
AS
BEGIN
-- No initialization of the output parameter
--
-- Additional statements here.
--
RETURN 0;
END
--
CREATE PROCEDURE [dbo].[procedureFixed]
(
@Value1 BIGINT,
@Value2 INT,
@Value3 INT,
@Sum INT OUTPUT
)
AS
BEGIN
-- Initialize the out parameter
SET @Sum = 0;
--
-- Additional statements here
--
RETURN 0;
END

SR0014: pode ocorrer perda de dados ao converter de {Type1} para {Type2}

Se os tipos de dados forem atribuídos de forma inconsistente a colunas, variáveis ou parâmetros, eles serão convertidos implicitamente quando o código Transact-SQL que contém esses objetos for executado. Esse tipo de conversão não apenas reduz o desempenho, mas também, em alguns casos, causa perda sutil de dados. Por exemplo, uma verificação de tabela pode ser executada se todas as colunas em uma cláusula WHERE precisarem ser convertidas. Pior, os dados podem ser perdidos se uma cadeia de caracteres Unicode for convertida em uma cadeia de caracteres ASCII que usa uma página de código diferente.

Esta regra NÃO:

  • Verifica o tipo de uma coluna computada porque o tipo não é conhecido até o tempo de execução.
  • Analisa qualquer coisa dentro de uma instrução CASE. Ele também não analisa o valor retornado de uma instrução CASE.
  • Analisa os parâmetros de entrada ou o valor retornado de uma chamada para ISNULL

Esta tabela resume as verificações cobertas pela regra SR0014:

Constructo de linguagemO que é verificadoExemplo
O valor padrão de parâmetrosTipo de dados de parâmetro
CREATE PROCEDURE p1(@p1 INT = 1)
AS
BEGIN
END
O predicado CREATE INDEXO predicado é booliano
CREATE INDEX index1 ON table1 (column1)
WHERE column1 > 10
Os argumentos das funções LEFT ou RIGHTO tipo e o comprimento do argumento de cadeia de caracteres
SET @v = LEFT('abc', 2)
Os argumentos das funções CAST e CONVERTA expressão e os tipos são válidos
SET @v = CAST('abc' AS CHAR(10))
Instrução SETO lado esquerdo e o lado direito têm tipos compatíveis
SET @v1 = 'xyz'
SELECT @v1 = c1 FROM t1
O predicado de instrução IFO predicado é booliano
IF (@v > 10)
O predicado de instrução WHILEO predicado é booliano
WHILE (@v > 10)
Instrução INSERTOs valores e as colunas estão corretos
INSERT INTO t1(c1, c2) VALUES (99, 'xyz')
INSERT INTO t1 SELECT c1 FROM t2.
O predicado SELECT WHEREO predicado é booliano
SELECT * FROM t1 WHERE c1 > 10
A expressão SELECT TOPA expressão é um tipo Integer ou Float
SELECT TOP 4 * FROM t1
SELECT TOP 1.5 PERCENT * FROM t1
Instrução UPDATEA expressão e a coluna têm tipos compatíveis
UPDATE t1 SET c1 = 100
O predicado UPDATEO predicado é booliano
UPDATE t1 SET c1 = 100
WHERE c1 > 100
A expressão UPDATE TOPA expressão é um tipo Integer ou Float
UPDATE TOP 4 table1
DELETE PREDICATEO predicado é booliano
DELETE t1 WHERE c1 > 10
A expressão DELETE TOPA expressão é um tipo Integer ou Float
DELETE TOP 2 FROM t1
A declaração da variável DECLAREO valor inicial e o tipo de dados são compatíveis
DECLARE @v INT = 10
Os argumentos da instrução EXECUTE e o tipo de retornoParâmetros e argumentos
CREATE PROCEDURE p1 (@p1 INT) AS
GO
EXECUTE p1 100
EXECUTE @v1 = p1 100
Instrução RETURNA expressão RETURN tem um tipo de dados compatível
CREATE FUNCTION f1() RETURNS INT
AS
BEGIN
  RETURN 100
END
As condições da instrução MERGEA condição é booliana
MERGE t1 USING t2
ON t1.c1 = t2.c2
WHEN t1.c1 > 10 THEN DELETE

Como corrigir violações

Você pode evitar e resolver esses problemas atribuindo tipos de dados de forma consistente e convertendo explicitamente os tipos onde eles são necessários. Para obter mais informações sobre como converter tipos de dados explicitamente, consulte esta página no site da Microsoft: CAST e CONVERT (Transact-SQL).

Exemplo

Este exemplo mostra dois procedimentos armazenados que inserem dados em uma tabela. O primeiro procedimento, procWithWarning, causará uma conversão implícita de um tipo de dados. O segundo procedimento, procFixed, mostra como você pode adicionar uma conversão explícita para maximizar o desempenho e reter todos os dados.

CREATE TABLE [dbo].[Table2]
(
[ID] INT NOT NULL IDENTITY(0, 1),
[c1] INT NOT NULL,
[c2] INT NOT NULL,
[c3] BIGINT NOT NULL,
[Comment] VARCHAR (25)
)
ON [PRIMARY]

CREATE PROCEDURE [dbo].[procWithWarning]
(
@Value1 INT,
@Value2 INT,
@Value3 BIGINT,
@Comment CHAR(30)
)
AS
BEGIN
INSERT INTO [Table2] ([c1], [c2], [c3], Comment)
VALUES (@Value1, @Value2, @Value3, @Comment)

END

CREATE PROCEDURE [dbo].[procFixed]
(
@Value1 INT,
@Value2 INT,
@Value3 BIGINT,
@Comment CHAR(10)
)
AS
BEGIN
INSERT INTO [Table2] ([c1], [c2], [c3], Comment)
VALUES (@Value1, @Value2, @Value3, CAST(@Comment AS VARCHAR(25)))

END