Database
 sql >> Base de Dados >  >> RDS >> Database

Gems T-SQL negligenciadas


Meu bom amigo Aaron Bertrand me inspirou a escrever este artigo. Ele me lembrou de como às vezes damos as coisas como garantidas quando elas parecem óbvias para nós e nem sempre nos preocupamos em verificar a história completa por trás delas. A relevância para o T-SQL é que às vezes assumimos que sabemos tudo o que há para saber sobre determinados recursos do T-SQL e nem sempre nos preocupamos em verificar a documentação para ver se há mais sobre eles. Neste artigo, abordo vários recursos do T-SQL que geralmente são totalmente ignorados ou que suportam parâmetros ou recursos que geralmente são ignorados. Se você tiver exemplos próprios de gems T-SQL que geralmente são ignorados, compartilhe-os na seção de comentários deste artigo.

Antes de começar a ler este artigo, pergunte a si mesmo o que você sabe sobre os seguintes recursos do T-SQL:EOMONTH, TRANSLATE, TRIM, CONCAT e CONCAT_WS, LOG, variáveis ​​de cursor e MERGE com OUTPUT.

Em meus exemplos, usarei um banco de dados de exemplo chamado TSQLV5. Você pode encontrar o script que cria e preenche esse banco de dados aqui e seu diagrama ER aqui.

EOMONTH tem um segundo parâmetro


A função EOMONTH foi introduzida no SQL Server 2012. Muitas pessoas pensam que ela suporta apenas um parâmetro contendo uma data de entrada e que simplesmente retorna a data de fim de mês que corresponde à data de entrada.

Considere uma necessidade um pouco mais sofisticada de calcular o final do mês anterior. Por exemplo, suponha que você precise consultar a tabela Sales.Orders e devolver pedidos que foram feitos no final do mês anterior.

Uma maneira de conseguir isso é aplicar a função EOMONTH a SYSDATETIME para obter a data de fim do mês atual e, em seguida, aplicar a função DATEADD para subtrair um mês do resultado, assim:
USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Observe que, se você realmente executar essa consulta no banco de dados de exemplo TSQLV5, obterá um resultado vazio, pois a última data do pedido registrada na tabela é 6 de maio de 2019. dia do mês anterior, a consulta os retornaria.

O que muitas pessoas não percebem é que EOMONTH suporta um segundo parâmetro onde você indica quantos meses adicionar ou subtrair. Aqui está a sintaxe [totalmente documentada] da função:
EOMONTH ( start_date [, month_to_add ] )

Nossa tarefa pode ser alcançada de forma mais fácil e natural simplesmente especificando -1 como o segundo parâmetro para a função, assim:
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRANSLATE às vezes é mais simples que REPLACE


Muitas pessoas estão familiarizadas com a função REPLACE e como ela funciona. Você o usa quando deseja substituir todas as ocorrências de uma substring por outra em uma string de entrada. Às vezes, porém, quando você tem várias substituições que precisa aplicar, usar REPLACE é um pouco complicado e resulta em expressões complicadas.

Como exemplo, suponha que você receba uma string de entrada @s que contém um número com formatação em espanhol. Na Espanha, eles usam um ponto como separador para grupos de milhares e uma vírgula como separador decimal. Você precisa converter a entrada para a formatação dos EUA, onde uma vírgula é usada como separador para grupos de milhares e um ponto como separador decimal.

Usando uma chamada para a função REPLACE, você pode substituir apenas todas as ocorrências de um caractere ou substring por outra. Para aplicar duas substituições (pontos a vírgulas e vírgulas a pontos), você precisa aninhar chamadas de função. A parte complicada é que, se você usar REPLACE uma vez para alterar pontos para vírgulas e, em seguida, uma segunda vez contra o resultado para alterar vírgulas para pontos, você acabará com apenas pontos. Tente:
DECLARE @s AS VARCHAR(20) = '123.456.789,00';
 
SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

Você obtém a seguinte saída:
123.456.789.00

Se você quiser continuar usando a função REPLACE, precisará de três chamadas de função. Um para substituir pontos por um caractere neutro que você sabe que normalmente não pode aparecer nos dados (digamos, ~). Outro contra o resultado de substituir todas as vírgulas por pontos. Outro contra o resultado de substituir todas as ocorrências do caractere temporário (~ em nosso exemplo) por vírgulas. Aqui está a expressão completa:
DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

Desta vez, você obtém a saída correta:
123,456,789.00

É meio factível, mas resulta em uma expressão longa e complicada. E se você tivesse mais substituições para aplicar?

Muitas pessoas não sabem que o SQL Server 2017 introduziu uma nova função chamada TRANSLATE que simplifica bastante essas substituições. Aqui está a sintaxe da função:
TRANSLATE ( inputString, characters, translations )

A segunda entrada (caracteres) é uma string com a lista dos caracteres individuais que você deseja substituir, e a terceira entrada (traduções) é uma string com a lista dos caracteres correspondentes pelos quais você deseja substituir os caracteres de origem. Isso naturalmente significa que o segundo e o terceiro parâmetros devem ter o mesmo número de caracteres. O importante sobre a função é que ela não faz passagens separadas para cada uma das substituições. Se tivesse, teria potencialmente resultado no mesmo bug do primeiro exemplo que mostrei usando as duas chamadas para a função REPLACE. Consequentemente, lidar com nossa tarefa se torna um acéfalo:
DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT TRANSLATE(@s, '.,', ',.');

Este código gera a saída desejada:
123,456,789.00

Isso é bem legal!

TRIM é maior que LTRIM(RTRIM())


O SQL Server 2017 introduziu suporte para a função TRIM. Muitas pessoas, inclusive eu, inicialmente assumem que não é mais do que um simples atalho para LTRIM(RTRIM(input)). No entanto, se você verificar a documentação, perceberá que na verdade é mais poderoso do que isso.

Antes de entrar nos detalhes, considere a seguinte tarefa:dada uma string de entrada @s, remova as barras iniciais e finais (para trás e para frente). Como exemplo, suponha que @s contenha a seguinte string:
//\\ remove leading and trailing backward (\) and forward (/) slashes \\//

A saída desejada é:
 remove leading and trailing backward (\) and forward (/) slashes 

Observe que a saída deve reter os espaços à esquerda e à direita.

Se você não conhecia os recursos completos do TRIM, aqui está uma maneira de resolver a tarefa:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT
  TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
    AS outputstring;

A solução começa usando TRANSLATE para substituir todos os espaços por um caractere neutro (~) e barras por espaços, depois usando TRIM para cortar espaços à esquerda e à direita do resultado. Essa etapa basicamente corta as barras à frente e à direita, usando temporariamente ~ em vez de espaços originais. Aqui está o resultado desta etapa:
\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

A segunda etapa usa TRANSLATE para substituir todos os espaços por outro caractere neutro (^) e barras invertidas por espaços, depois usando TRIM para cortar espaços à esquerda e à direita do resultado. Essa etapa basicamente corta as barras invertidas à esquerda e à direita, usando temporariamente ^ em vez de espaços intermediários. Aqui está o resultado desta etapa:
~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

A última etapa usa TRANSLATE para substituir espaços por barras invertidas, ^ por barras e ~ por espaços, gerando a saída desejada:
 remove leading and trailing backward (\) and forward (/) slashes 

Como exercício, tente resolver esta tarefa com uma solução compatível com pré-SQL Server 2017 em que você não pode usar TRIM e TRANSLATE.

De volta ao SQL Server 2017 e acima, se você se incomodasse em verificar a documentação, descobriria que o TRIM é mais sofisticado do que você pensava inicialmente. Aqui está a sintaxe da função:
TRIM ( [ characters FROM ] string )

Os caracteres DE opcionais part permite especificar um ou mais caracteres que você deseja aparar do início e do fim da string de entrada. No nosso caso, tudo que você precisa fazer é especificar '/\' como esta parte, assim:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT TRIM( '/\' FROM @s) AS outputstring;

Essa é uma melhoria bastante significativa em comparação com a solução anterior!

CONCAT e CONCAT_WS


Se você trabalha com T-SQL há algum tempo, sabe como é difícil lidar com NULLs quando você precisa concatenar strings. Como exemplo, considere os dados de localização registrados para funcionários na tabela HR.Employees:
SELECT empid, country, region, city
FROM HR.Employees;

Essa consulta gera a seguinte saída:
empid       country         region          city
----------- --------------- --------------- ---------------
1           USA             WA              Seattle
2           USA             WA              Tacoma
3           USA             WA              Kirkland
4           USA             WA              Redmond
5           UK              NULL            London
6           UK              NULL            London
7           UK              NULL            London
8           USA             WA              Seattle
9           UK              NULL            London

Observe que para alguns funcionários a parte da região é irrelevante e uma região irrelevante é representada por um NULL. Suponha que você precise concatenar as partes do local (país, região e cidade), usando uma vírgula como separador, mas ignorando as regiões NULL. Quando a região for relevante, você deseja que o resultado tenha o formato <coutry>,<region>,<city> e quando a região é irrelevante você quer que o resultado tenha o formato <country>,<city> . Normalmente, concatenar algo com um NULL produz um resultado NULL. Você pode alterar esse comportamento desativando a opção de sessão CONCAT_NULL_YIELDS_NULL, mas eu não recomendaria ativar o comportamento fora do padrão.

Se você não soubesse da existência das funções CONCAT e CONCAT_WS, provavelmente teria usado ISNULL ou COALESCE para substituir um NULL por uma string vazia, assim:
SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
FROM HR.Employees;

Aqui está a saída desta consulta:
empid       location
----------- -----------------------------------------------
1           USA,WA,Seattle
2           USA,WA,Tacoma
3           USA,WA,Kirkland
4           USA,WA,Redmond
5           UK,London
6           UK,London
7           UK,London
8           USA,WA,Seattle
9           UK,London

O SQL Server 2012 introduziu a função CONCAT. Esta função aceita uma lista de entradas de cadeia de caracteres e as concatena e, ao fazê-lo, ignora NULLs. Então, usando CONCAT você pode simplificar a solução assim:
SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM HR.Employees;

Ainda assim, você precisa especificar explicitamente os separadores como parte das entradas da função. Para facilitar ainda mais nossa vida, o SQL Server 2017 introduziu uma função semelhante chamada CONCAT_WS onde você começa indicando o separador, seguido dos itens que deseja concatenar. Com esta função, a solução é ainda mais simplificada assim:
SELECT empid, CONCAT_WS(',', country, region, city) AS location
FROM HR.Employees;

O próximo passo é, claro, a leitura da mente. Em 1º de abril de 2020, a Microsoft planeja lançar o CONCAT_MR. A função aceitará uma entrada vazia e descobrirá automaticamente quais elementos você deseja concatenar lendo sua mente. A consulta então ficará assim:
SELECT empid, CONCAT_MR() AS location
FROM HR.Employees;

LOG tem um segundo parâmetro


Semelhante à função EOMONTH, muitas pessoas não percebem que a partir do SQL Server 2012, a função LOG suporta um segundo parâmetro que permite indicar a base do logaritmo. Antes disso, o T-SQL suportava a função LOG(input) que retorna o logaritmo natural da entrada (usando a constante e como base), e LOG10(input) que usa 10 como base.

Não estar ciente da existência do segundo parâmetro para a função LOG, quando as pessoas queriam calcular Logb (x), onde b é uma base diferente de e e 10, eles geralmente faziam isso pelo caminho mais longo. Você pode confiar na seguinte equação:
Registrob (x) =Loga (x)/Loga (b)
Como exemplo, para calcular Log2 (8), você confia na seguinte equação:
Registro2 (8) =Loge (8)/Loge (2)
Traduzido para T-SQL, você aplica o seguinte cálculo:
DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x) / LOG(@b);

Uma vez que você percebe que o LOG suporta um segundo parâmetro onde você indica a base, o cálculo simplesmente se torna:
DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x, @b);

Variável de cursor


Se você trabalha com T-SQL há algum tempo, provavelmente teve muitas chances de trabalhar com cursores. Como você sabe, ao trabalhar com um cursor, você normalmente usa as seguintes etapas:
  • Declare o cursor
  • Abra o cursor
  • Iterar pelos registros do cursor
  • Fechar o cursor
  • Desalocar o cursor

Como exemplo, suponha que você precise executar alguma tarefa por banco de dados em sua instância. Usando um cursor, você normalmente usaria um código semelhante ao seguinte:
DECLARE @dbname AS sysname;
 
DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN C;
 
FETCH NEXT FROM C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

O comando CLOSE libera o conjunto de resultados atual e libera bloqueios. O comando DEALLOCATE remove uma referência de cursor e, quando a última referência é desalocada, libera as estruturas de dados que compõem o cursor. Se você tentar executar o código acima duas vezes sem os comandos CLOSE e DEALLOCATE, você receberá o seguinte erro:
Msg 16915, Level 16, State 1, Line 4
A cursor with the name 'C' already exists.
Msg 16905, Level 16, State 1, Line 6
The cursor is already open.

Certifique-se de executar os comandos CLOSE e DEALLOCATE antes de continuar.

Muitas pessoas não percebem que quando precisam trabalhar com um cursor em apenas um lote, que é o caso mais comum, ao invés de usar um cursor normal você pode trabalhar com uma variável de cursor. Como qualquer variável, o escopo de uma variável de cursor é apenas o lote onde ela foi declarada. Isso significa que assim que um lote termina, todas as variáveis ​​expiram. Usando uma variável de cursor, uma vez que um lote termina, o SQL Server fecha e desaloca automaticamente, poupando a necessidade de executar o comando CLOSE e DEALLOCATE explicitamente.

Aqui está o código revisado usando uma variável de cursor desta vez:
DECLARE @dbname AS sysname, @C AS CURSOR;
 
SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN @C;
 
FETCH NEXT FROM @C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM @C INTO @dbname;
END;

Sinta-se à vontade para executá-lo várias vezes e observe que desta vez você não recebe nenhum erro. É apenas mais limpo e você não precisa se preocupar em manter os recursos do cursor se esqueceu de fechar e desalocar o cursor.

MERGE com SAÍDA


Desde o início da cláusula OUTPUT para instruções de modificação no SQL Server 2005, ela se tornou uma ferramenta muito prática sempre que você desejava retornar dados de linhas modificadas. As pessoas usam esse recurso regularmente para fins como arquivamento, auditoria e muitos outros casos de uso. Uma das coisas irritantes sobre esse recurso, no entanto, é que, se você usá-lo com instruções INSERT, só poderá retornar dados das linhas inseridas, prefixando as colunas de saída com inserido . Você não tem acesso às colunas da tabela de origem, embora às vezes precise retornar colunas da origem junto com colunas do destino.

Como exemplo, considere as tabelas T1 e T2, que você cria e preenche executando o seguinte código:
DROP TABLE IF EXISTS dbo.T1, dbo.T2;
GO
 
CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

Observe que uma propriedade de identidade é usada para gerar as chaves em ambas as tabelas.

Suponha que você precise copiar algumas linhas de T1 para T2; digamos, aquelas em que keycol % 2 =1. Você deseja usar a cláusula OUTPUT para retornar as chaves recém-geradas em T2, mas também deseja retornar juntamente com essas chaves as respectivas chaves de origem de T1. A expectativa intuitiva é usar a seguinte instrução INSERT:
INSERT INTO dbo.T2(datacol)
    OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
  SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

Infelizmente, como mencionado, a cláusula OUTPUT não permite que você faça referência a colunas da tabela de origem, então você recebe o seguinte erro:
Msg 4104, Level 16, State 1, Line 2
O identificador de várias partes "T1.keycol" não pôde ser vinculado.
Muitas pessoas não percebem que estranhamente essa limitação não se aplica à instrução MERGE. Portanto, mesmo que seja um pouco estranho, você pode converter sua instrução INSERT em uma instrução MERGE, mas para fazer isso, você precisa que o predicado MERGE seja sempre falso. Isso ativará a cláusula WHEN NOT MATCHED e aplicará a única ação INSERT suportada lá. Você pode usar uma condição falsa fictícia como 1 =2. Aqui está o código convertido completo:
MERGE INTO dbo.T2 AS TGT
USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
  ON 1 = 2
WHEN NOT MATCHED THEN
  INSERT(datacol) VALUES(SRC.datacol)
OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

Desta vez, o código é executado com sucesso, produzindo a seguinte saída:
T1_keycol   T2_keycol
----------- -----------
1           1
3           2
5           3

Esperamos que a Microsoft aprimore o suporte para a cláusula OUTPUT nas outras instruções de modificação para permitir o retorno de colunas da tabela de origem também.

Conclusão


Não assuma, e RTFM! :-)