parte de uma definição de tabela derivada é uma tabela . Esse é o caso, mesmo que seja expresso como uma consulta. Lembre-se da propriedade de fechamento da álgebra relacional? O mesmo se aplica ao restante das expressões de tabela nomeadas acima mencionadas (CTEs, visualizações e TVFs inline). Como você já aprendeu, a tabela do SQL é a contrapartida da relação da teoria relacional , embora não seja uma contrapartida perfeita. Assim, uma expressão de tabela precisa atender a certos requisitos para garantir que o resultado seja uma tabela – aqueles que uma consulta que não é usada como expressão de tabela não necessariamente precisa. Aqui estão três requisitos específicos:
- Todas as colunas da expressão de tabela devem ter nomes
- Todos os nomes de coluna da expressão de tabela devem ser exclusivos
- As linhas da expressão de tabela não têm ordem
Vamos detalhar esses requisitos um por um, discutindo a relevância tanto para a teoria relacional quanto para o SQL.
Todas as colunas devem ter nomes
Lembre-se que uma relação tem um título e um corpo. O cabeçalho de uma relação é um conjunto de atributos (colunas em SQL). Um atributo tem um nome e um nome de tipo e é identificado por seu nome. Uma consulta que não é usada como uma expressão de tabela não precisa necessariamente atribuir nomes a todas as colunas de destino. Considere a seguinte consulta como um exemplo:
SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city)FROM HR.Employees;
Essa consulta gera a seguinte saída:
nome empid sobrenome (sem nome de coluna)------ ---------- ---------- ------------- ----1 Sara Davis EUA/WA/Seattle2 Don Funk EUA/WA/Tacoma3 Judy Lew EUA/WA/Kirkland4 Yael Peled EUA/WA/Redmond5 Sven Mortensen Reino Unido/Londres6 Paul Suurs Reino Unido/Londres7 Russell King Reino Unido/Londres8 Maria Cameron EUA/WA/Seattle9 Patricia Doyle Reino Unido/Londres
A saída da consulta possui uma coluna anônima resultante da concatenação dos atributos de localização utilizando a função CONCAT_WS. (A propósito, essa função foi adicionada no SQL Server 2017, portanto, se você estiver executando o código em uma versão anterior, sinta-se à vontade para substituir esse cálculo por um cálculo alternativo de sua escolha.) Essa consulta, portanto, não retornar uma tabela, para não falar de uma relação. Portanto, não é válido usar uma consulta como a expressão de tabela/consulta interna de uma definição de tabela derivada.
Tente:
SELECT *FROM ( SELECT empid, nome, sobrenome, CONCAT_WS(N'/', país, região, cidade) FROM HR.Employees ) AS D;
Você recebe o seguinte erro:
Msg 8155, Level 16, State 2, Line 50
Nenhum nome de coluna foi especificado para a coluna 4 de 'D'.
Como um aparte, notou algo interessante sobre a mensagem de erro? Ele reclama da coluna 4, destacando a diferença entre colunas no SQL e atributos na teoria relacional.
A solução é, obviamente, certificar-se de que você atribui explicitamente nomes às colunas que resultam de cálculos. O T-SQL oferece suporte a algumas técnicas de nomenclatura de colunas. Vou citar dois deles.
Você pode usar uma técnica de nomenclatura inline na qual você atribui o nome da coluna de destino após o cálculo e uma cláusula AS opcional, como em < expression > [ AS ] < column name >
, igual a:
SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees ) AS D;
Essa consulta gera a seguinte saída:
nome vazio sobrenome custlocation------ ---------- ---------- ----------------1 Sara Davis EUA/WA/Seattle2 Don Funk EUA/WA/Tacoma3 Judy Lew EUA/WA/Kirkland4 Yael Peled EUA/WA/Redmond5 Sven Mortensen Reino Unido/Londres6 Paul Suurs Reino Unido/Londres7 Russell King Reino Unido/Londres8 Maria Cameron EUA/WA/Seattle9 Patricia Doyle Reino Unido/Londres
Usando essa técnica, é muito fácil, ao revisar o código, saber qual nome de coluna de destino está atribuído a qual expressão. Além disso, você só precisa nomear colunas que ainda não tenham nomes.
Você também pode usar uma técnica de nomenclatura de coluna mais externa, na qual especifica os nomes das colunas de destino entre parênteses logo após o nome da tabela derivada, assim:
SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees ) AS D(empid, firstname, lastname, custlocation);
Com essa técnica, porém, você precisa listar nomes para todas as colunas, incluindo aquelas que já possuem nomes. A atribuição dos nomes das colunas de destino é feita por posição, da esquerda para a direita, ou seja, o nome da primeira coluna de destino representa a primeira expressão na lista SELECT da consulta interna; o nome da segunda coluna de destino representa a segunda expressão; e assim por diante.
Observe que em caso de inconsistência entre os nomes das colunas interna e externa, digamos, devido a um bug no código, o escopo dos nomes internos é a consulta interna — ou, mais precisamente, a variável de intervalo interno (aqui implicitamente HR.Employees AS Employees)—e o escopo dos nomes externos é a variável de intervalo externa (D em nosso caso). Há um pouco mais de envolvimento no escopo dos nomes das colunas que tem a ver com o processamento lógico de consultas, mas isso é um item para discussões posteriores.
O potencial de bugs com a sintaxe de nomenclatura externa é melhor explicado com um exemplo.
Examine a saída da consulta anterior, com o conjunto completo de funcionários da tabela HR.Employees. Em seguida, considere a seguinte consulta e, antes de executá-la, tente descobrir quais funcionários você espera ver no resultado:
SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees WHERE lastname LIKE N'D%' ) AS D(empid, sobrenome, nome, localização do cliente)WHERE nome LIKE N'D%';
Se você espera que a consulta retorne um conjunto vazio para os dados de amostra fornecidos, já que não há funcionários no momento com um sobrenome e um nome que comecem com a letra D, você está perdendo o bug no código.
Agora execute a consulta e examine a saída real:
nome vazio sobrenome custlocation------ ---------- ------------- ---------------1 Davis Sara EUA/WA/Seattle9 Doyle Patricia Reino Unido/Londres
O que aconteceu?
A consulta interna especifica o nome como a segunda coluna e o sobrenome como a terceira coluna na lista SELECT. O código que atribui os nomes das colunas de destino da tabela derivada na consulta externa especifica o segundo nome e o terceiro nome. O código nomeia firstname como lastname e lastname como firstname na variável de intervalo D. Efetivamente, você está apenas filtrando funcionários cujo sobrenome começa com a letra D. Você não está filtrando funcionários com um sobrenome e um nome que começam com a letra D
A sintaxe de alias inline não é propensa a esses bugs. Por um lado, você normalmente não alias uma coluna que já tem um nome com o qual você está feliz. Segundo, mesmo que você queira atribuir um alias diferente para uma coluna que já tem um nome, não é muito provável que com a sintaxe AS você atribua o alias errado. Pense nisso; qual a probabilidade de você escrever assim:
SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees WHERE lastname LIKE N'D %' ) AS DWHERE nome LIKE N'D%';
Obviamente, não muito provável.
Todos os nomes de coluna devem ser exclusivos
Voltando ao fato de que o cabeçalho de uma relação é um conjunto de atributos, e dado que um atributo é identificado pelo nome, os nomes dos atributos devem ser únicos para a mesma relação. Em uma determinada consulta, você sempre pode fazer referência a um atributo usando um nome de duas partes com o nome da variável de intervalo como qualificador, como em .. Quando o nome da coluna sem o qualificador não for ambíguo, você poderá omitir o prefixo do nome da variável de intervalo. O que é importante lembrar, porém, é o que eu disse anteriormente sobre o escopo dos nomes das colunas. No código que envolve uma expressão de tabela nomeada, com uma consulta interna (a expressão de tabela) e uma consulta externa, o escopo dos nomes de coluna na consulta interna são as variáveis de intervalo interno e o escopo dos nomes de coluna na query são as variáveis de intervalo externo. Se a consulta interna envolver várias tabelas de origem com o mesmo nome de coluna, você ainda poderá fazer referência a essas colunas de maneira inequívoca adicionando o nome da variável de intervalo como um prefixo. Se você não atribuir um nome de variável de intervalo explicitamente, obterá um atribuído implicitamente, como se você usasse AS .
Considere a seguinte consulta independente como exemplo:
SELECT C.custid, O.custid, O.orderidFROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid;
Esta consulta não falha com um erro de nome de coluna duplicado, pois uma coluna custid é realmente denominada C.custid e a outra O.custid dentro do escopo da consulta atual. Essa consulta gera a seguinte saída:
custid custid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...
No entanto, tente usar essa consulta como uma expressão de tabela na definição de uma tabela derivada chamada CO, assim:
SELECT *FROM ( SELECT C.custid, O.custid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;
No que diz respeito à consulta externa, você tem uma variável de intervalo chamada CO e o escopo de todos os nomes de coluna na consulta externa é essa variável de intervalo. Os nomes de todas as colunas em uma determinada variável de intervalo (lembre-se, uma variável de intervalo é uma variável de relação) devem ser exclusivos. Assim, você obtém o seguinte erro:
Msg 8156, Level 16, State 1, Line 80
A coluna 'custid' foi especificada várias vezes para 'CO'.
A correção é, obviamente, atribuir nomes de coluna diferentes às duas colunas custid no que diz respeito à variável de intervalo CO, assim:
SELECT *FROM ( SELECT C.custid AScustid, O.custid AS ordercustid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;
Essa consulta gera a seguinte saída:
custid ordercustid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...
Se você seguir as boas práticas, listará explicitamente os nomes das colunas na lista SELECT da consulta externa. Como há apenas uma variável de intervalo envolvida, você não precisa usar o nome de duas partes para as referências de coluna externas. Se você deseja usar o nome de duas partes, você prefixa os nomes das colunas com o nome da variável de intervalo externo CO, assim:
SELECT CO.custid, CO.ordercustid, CO.orderidFROM ( SELECT C.custid AScustid, O.custid AS ordercustid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;
Sem pedido
Há muito a dizer sobre expressões e ordenação de tabelas nomeadas - o suficiente para um artigo por si só -, então dedicarei um artigo futuro a este tópico. Ainda assim, eu queria tocar o tópico brevemente aqui, já que é tão importante. Lembre-se de que o corpo de uma relação é um conjunto de tuplas e, da mesma forma, o corpo de uma tabela é um conjunto de linhas. Um conjunto não tem ordem. Ainda assim, o SQL permite que a consulta mais externa tenha uma cláusula ORDER BY servindo a um significado de ordenação de apresentação, como demonstra a consulta a seguir:
SELECT orderid, valFROM Sales.OrderValuesORDER BY val DESC;
O que você precisa entender, porém, é que essa consulta não retorna uma relação como resultado. Mesmo da perspectiva do SQL, a consulta não retorna uma tabela como resultado e, portanto, não é considerada uma expressão de tabela. Consequentemente, é inválido usar essa consulta como a parte da expressão de tabela de uma definição de tabela derivada.
Tente executar o seguinte código:
SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ORDER BY val DESC ) AS D;
Você recebe o seguinte erro:
Msg 1033, Level 15, State 1, Line 124
A cláusula ORDER BY é inválida em visualizações, funções inline, tabelas derivadas, subconsultas e expressões de tabela comuns, a menos que TOP, OFFSET ou FOR XML também seja especificado.
Vou abordar o a menos que parte da mensagem de erro em breve.
Se você deseja que a consulta mais externa retorne um resultado ordenado, você precisa especificar a cláusula ORDER BY na consulta mais externa, assim:
SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ) AS DORDER BY val DESC;
Quanto ao a menos que parte da mensagem de erro; O T-SQL suporta o filtro TOP proprietário, bem como o filtro OFFSET-FETCH padrão. Ambos os filtros contam com uma cláusula ORDER BY no mesmo escopo de consulta para definir quais linhas superiores filtrar. Infelizmente, isso é resultado de uma armadilha no design desses recursos, que não separa a ordenação da apresentação da ordenação do filtro. Seja como for, tanto a Microsoft com seu filtro TOP, quanto o padrão com seu filtro OFFSET-FETCH, permitem especificar uma cláusula ORDER BY na consulta interna desde que especifique também o filtro TOP ou OFFSET-FETCH, respectivamente. Portanto, esta consulta é válida, por exemplo:
SELECT orderid, valFROM ( SELECT TOP (3) orderid, val FROM Sales.OrderValues ORDER BY val DESC ) AS D;
Quando executei essa consulta no meu sistema, ela gerou a seguinte saída:
valor do pedido-------- ---------10865 16387.5010981 15810.0011030 12615.05
O que é importante ressaltar, porém, é que a única razão pela qual a cláusula ORDER BY é permitida na consulta interna é para suportar o filtro TOP. Essa é a única garantia que você obtém no que diz respeito ao pedido. Como a consulta externa também não possui uma cláusula ORDER BY, você não obtém uma garantia para nenhuma ordenação de apresentação específica dessa consulta, independentemente do comportamento observado. Esse é o caso no T-SQL, bem como no padrão. Aqui está uma citação da norma abordando esta parte:
“A ordenação das linhas da tabela especificada pela é garantida apenas para a que contém imediatamente a .”
Como mencionado, há muito mais a dizer sobre expressões de tabela e ordenação, o que farei em um artigo futuro. Também fornecerei exemplos demonstrando como a falta da cláusula ORDER BY na consulta externa significa que você não obtém nenhuma garantia de ordenação de apresentação.
Portanto, uma expressão de tabela, por exemplo, uma consulta interna em uma definição de tabela derivada, é uma tabela. Da mesma forma, uma tabela derivada (no sentido específico) também é uma tabela. Não é uma mesa base, mas não deixa de ser uma mesa. O mesmo se aplica a CTEs, visualizações e TVFs inline. Não são tabelas base, mas derivadas (no sentido mais geral), mas são tabelas.
Falhas de projeto
As tabelas derivadas têm duas deficiências principais em seu design. Ambos têm a ver com o fato de que a tabela derivada é definida na cláusula FROM da consulta externa.
Uma falha de design tem a ver com o fato de que, se você precisar consultar uma tabela derivada de uma consulta externa e, por sua vez, usar essa consulta como uma expressão de tabela em outra definição de tabela derivada, você acabará aninhando essas consultas de tabela derivada. Na computação, o aninhamento explícito de código envolvendo vários níveis de aninhamento tende a resultar em um código complexo que é difícil de manter.
Aqui está um exemplo muito básico demonstrando isso:
SELECT orderyear, numcustsFROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM (SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ) AS D1 GROUP BY orderyear ) AS D2WHERE numcusts> 70;
Este código retorna os anos do pedido e o número de clientes que fizeram pedidos durante cada ano, apenas para os anos em que o número de clientes que fizeram pedidos foi maior que 70.
A principal motivação para usar expressões de tabela aqui é poder fazer referência a um alias de coluna várias vezes. A consulta mais interna usada como uma expressão de tabela para a tabela derivada D1 consulta a tabela Sales.Orders e atribui o nome da coluna orderyear à expressão YEAR(orderdate) e também retorna a coluna custid. A consulta em D1 agrupa as linhas de D1 por ano do pedido e retorna o ano do pedido, bem como o número distinto de clientes que fizeram pedidos durante o ano em questão com o alias de numcusts. O código define uma tabela derivada chamada D2 com base nessa consulta. A consulta mais externa que consulta D2 e filtra apenas os anos em que o número de clientes que fizeram pedidos foi superior a 70.
Uma tentativa de revisar esse código ou solucioná-lo em caso de problemas é complicada devido aos vários níveis de aninhamento. Em vez de revisar o código da maneira mais natural de cima para baixo, você precisa analisá-lo começando pela unidade mais interna e gradualmente indo para fora, pois isso é mais prático.
O ponto principal sobre o uso de tabelas derivadas neste exemplo era simplificar o código evitando a necessidade de repetir expressões. Mas não tenho certeza de que essa solução atinja esse objetivo. Nesse caso, provavelmente é melhor repetir algumas expressões, evitando a necessidade de usar tabelas derivadas, assim:
SELECT YEAR(orderdate) AS orderyear, COUNT(DISTINCT custid) AS numcustsFROM Sales.OrdersGROUP BY YEAR(orderdate)HAVING COUNT(DISTINCT custid)> 70;
Tenha em mente que estou mostrando um exemplo muito simples aqui para fins de ilustração. Imagine um código de produção com mais níveis de aninhamento e com código mais longo e elaborado, e você verá como se torna substancialmente mais complicado de manter.
Outra falha no design de tabelas derivadas tem a ver com casos em que você precisa interagir com várias instâncias da mesma tabela derivada. Considere a seguinte consulta como um exemplo:
SELECT CUR.orderyear, CUR.numorders, CUR.numorders - PRV.numorders AS diffFROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS CUR LEFT OUTER JOIN ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS PRV ON CUR.orderyear =PRV.orderyear + 1;
Este código calcula o número de pedidos processados em cada ano, bem como a diferença em relação ao ano anterior. Ignore o fato de que existem maneiras mais simples de realizar a mesma tarefa com funções de janela - estou usando este código para ilustrar um determinado ponto, portanto, a tarefa em si e as diferentes maneiras de resolvê-la não são significativas.
Uma junção é um operador de tabela que trata suas duas entradas como um conjunto, o que significa que não há ordem entre elas. Eles são chamados de entradas esquerda e direita para que você possa marcar um deles (ou ambos) como uma tabela preservada em uma junção externa, mas ainda assim, não há primeiro e segundo entre eles. Você tem permissão para usar tabelas derivadas como entradas de junção, mas o nome da variável de intervalo que você atribui à entrada esquerda não está acessível na definição da entrada direita. Isso porque ambos são conceitualmente definidos no mesmo passo lógico, como se estivessem no mesmo ponto no tempo. Consequentemente, ao unir tabelas derivadas, você não pode definir duas variáveis de intervalo com base em uma expressão de tabela. Infelizmente, você precisa repetir o código, definindo duas variáveis de intervalo com base em duas cópias idênticas do código. Isso obviamente complica a manutenção do código e aumenta a probabilidade de erros. Cada alteração que você faz em uma expressão de tabela também precisa ser aplicada à outra.
Como explicarei em um artigo futuro, CTEs, em seu design, não incorrem nessas duas falhas que as tabelas derivadas incorrem.
Construtor de valor de tabela
Um construtor de valor de tabela permite que você construa um valor de tabela com base em expressões escalares independentes. Você pode usar essa tabela em uma consulta externa da mesma forma que usa uma tabela derivada baseada em uma consulta interna. Em um artigo futuro eu discuto tabelas derivadas laterais e correlações em detalhes, e mostrarei formas mais sofisticadas de construtores de valor de tabela. Neste artigo, porém, vou me concentrar em um formulário simples que é baseado puramente em expressões escalares autocontidas.
A sintaxe geral para uma consulta em um construtor de valor de tabela é a seguinte:
SELECT
FROM ( ) AS ();
O construtor do valor da tabela é definido na cláusula FROM da consulta externa.
The table’s body is made of a VALUES clause, followed by a comma separated list of pairs of parentheses, each defining a row with a comma separated list of expressions forming the row’s values.
The table’s heading is a comma separated list of the target column names. I’ll talk about a shortcoming of this syntax regarding the table’s heading shortly.
The following code uses a table value constructor to define a table called MyCusts with three columns called custid, companyname and contractdate, and three rows:
SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);
The above code is equivalent (both logically and in performance terms) in T-SQL to the following alternative:
SELECT custid, companyname, contractdateFROM ( SELECT 2, 'Cust 2', '20200212' UNION ALL SELECT 3, 'Cust 3', '20200118' UNION ALL SELECT 5, 'Cust 5', '20200401' ) AS MyCusts(custid, companyname, contractdate);
The two are internally algebrized the same way. The syntax with the VALUES clause is standard whereas the syntax with the unified FROMless queries isn’t, hence I prefer the former.
There is a shortcoming in the design of table value constructors in both standard SQL and in T-SQL. Remember that the heading of a relation is made of a set of attributes, and an attribute has a name and a type name. In the table value constructor’s syntax, you specify the column names, but not their data types. Suppose that you need the custid column to be of a SMALLINT type, the companyname column of a VARCHAR(50) type, and the contractdate column of a DATE type. It would have been good if we were able to define the column types as part of the definition of the table’s heading, like so (this syntax isn’t supported):
SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);
That’s of course just wishful thinking.
The way it works in T-SQL, is that each literal that is based on a constant has a predetermined type irrespective of context. For instance, can you guess what the types of the following literals are:
- 1
- 2147483647
- 2147483648
- 1E
- '1E'
- '20200212'
Is 1 considered BIT, INT, SMALLINT, other?
Is 1E considered VARBINARY(1), VARCHAR(2), other?
Is '20200212' considered DATE, DATETIME, VARCHAR(8), CHAR(8), other?
There’s a simple trick to figure out the default type of a literal, using the SQL_VARIANT_PROPERTY function with the 'BaseType' property, like so:
SELECT SQL_VARIANT_PROPERTY(2147483648, 'BaseType');
What happens is that SQL Server implicitly converts the literal to SQL_VARIANT—since that’s what the function expects—but preserves its base type. It then reports the base type as requested.
Similarly, you can query other properties of the input value, like the maximum length (MaxLength), Precision, Scale, and so on.
Try it with the aforementioned literal values, and you will get the following:
- 1:INT
- 2147483647:INT
- 2147483648:NUMERIC(10, 0)
- 1E:FLOAT
- '1E':VARCHAR(2)
- '20200212':VARCHAR(8)
As you can see, SQL Server has default assumptions about the data type, maximum length, precision, scale, and so on.
There are some cases where you need to specify a literal of a certain type, but you cannot do it directly in T-SQL. For example, you cannot specify a literal of the following types directly:BIT, TINYINT, BIGINT, all date and time types, and quite a few others. Unfortunately, T-SQL doesn’t provide a selector property for its types, which would have served exactly the needed purpose of selecting a value of the given type. Of course, you can always convert an expression’s type explicitly using the CAST or CONVERT function, as in CAST(5 AS SMALLINT). If you don’t, SQL Server will sometimes need to implicitly convert some of your expressions to a different type based on its implicit conversion rules. For example, when you try to compare values of different types, e.g., WHERE datecol ='20200212', assuming datecol is of a DATE type. Another example is when you specify a literal in an INSERT or an UPDATE statement, and the literal’s type is different than the target column’s type.
If all this is not confusing enough, set operators like UNION ALL rely on data type precedence to define the target column types—and remember, a table value constructor is algebrized like a series of UNION ALL operations. Consider the table value constructor shown earlier:
SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);
Each literal here has a predetermined type. 2, 3 and 5 are all of an INT type, so clearly the custid target column type is INT. If you had the values 1000000000, 3000000000 and 2000000000, the first and the third are considered INT and the second is considered NUMERIC(10, 0). According to data type precedence NUMERIC (same as DECIMAL) is stronger than INT, hence in such a case the target column type would be NUMERIC(10, 0).
If you want to figure out which data types SQL Server chooses for the target columns in your table value constructor, you have a few options. One is to use a SELECT INTO statement to write the table value constructor’s data into a temporary table, and then query the metadata for the temporary table, like so:
SELECT custid, companyname, contractdateINTO #MyCustsFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts');
Here’s the output of this code:
colname typename maxlength------------- ---------- ---------custid int 4companyname varchar 6contractdate varchar 8
You can then drop the temporary table for cleanup:
DROP TABLE IF EXISTS #MyCusts;
Another option is to use the SQL_VARIANT_PROPERTY, which I mentioned earlier, like so:
SELECT TOP (1) SQL_VARIANT_PROPERTY(custid, 'BaseType') AS custid_typename, SQL_VARIANT_PROPERTY(custid, 'MaxLength') AS custid_maxlength, SQL_VARIANT_PROPERTY(companyname, 'BaseType') AS companyname_typename, SQL_VARIANT_PROPERTY(companyname, 'MaxLength') AS companyname_maxlength, SQL_VARIANT_PROPERTY(contractdate, 'BaseType') AS contractdate_typename, SQL_VARIANT_PROPERTY(contractdate, 'MaxLength') AS contractdate_maxlengthFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);
This code generates the following output (formatted for readability):
custid_typename custid_maxlength-------------------- ---------------- int 4 companyname_typename companyname_maxlength -------------------- --------------------- varchar 6 contractdate_typename contractdate_maxlength--------------------- ----------------------varchar 8
So, what if you need to control the types of the target columns? As mentioned earlier, say you need custid to be SMALLINT, companyname VARCHAR(50), and contractdate DATE.
Don’t be misled to think that it’s enough to explicitly convert just one row’s values. If a corresponding value’s type in any other row is considered stronger, it would dictate the target column’s type. Here’s an example demonstrating this:
SELECT custid, companyname, contractdateINTO #MyCusts1FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts1');
Este código gera a seguinte saída:
colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3
Notice that the type for custid is INT.
The same applies never mind which row’s values you explicitly convert, if you don’t convert all of them. For example, here the code explicitly converts the types of the values in the second row:
SELECT custid, companyname, contractdateINTO #MyCusts2FROM ( VALUES( 2, 'Cust 2', '20200212'), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE) ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts2');
Este código gera a seguinte saída:
colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3
As you can see, custid is still of an INT type.
You basically have two main options. One is to explicitly convert all values, like so:
SELECT custid, companyname, contractdateINTO #MyCusts3FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE)), ( CAST(5 AS SMALLINT), CAST('Cust 5' AS VARCHAR(50)), CAST('20200401' AS DATE)) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts3');
This code generates the following output, showing all target columns have the desired types:
colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3
That’s a lot of coding, though. Another option is to apply the conversions in the SELECT list of the query against the table value constructor, and then define a derived table against the query that applies the conversions, like so:
SELECT custid, companyname, contractdateINTO #MyCusts4FROM ( SELECT CAST(custid AS SMALLINT) AS custid, CAST(companyname AS VARCHAR(50)) AS companyname, CAST(contractdate AS DATE) AS contractdate FROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS D(custid, companyname, contractdate) ) AS MyCusts; SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts4');
Este código gera a seguinte saída:
colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3
The reasoning for using the additional derived table is due to how logical query processing is designed. The SELECT clause is evaluated after FROM, WHERE, GROUP BY and HAVING. By applying the conversions in the SELECT list of the inner query, you allow expressions in all clauses of the outermost query to interact with the columns with the proper types.
Back to our wishful thinking, clearly, it would be good if we ever get a syntax that allows explicit control of the types in the definition of the table value constructor’s heading, like so:
SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);
When you’re done, run the following code for cleanup:
DROP TABLE IF EXISTS #MyCusts1, #MyCusts2, #MyCusts3, #MyCusts4;
Used in modification statements
T-SQL allows you to modify data through table expressions. That’s true for derived tables, CTEs, views and inline TVFs. What gets modified in practice is some underlying base table that is used by the table expression. I have much to say about modifying data through table expressions, and I will in a future article dedicated to this topic. Here, I just wanted to briefly mention the types of modification statements that specifically support derived tables, and provide the syntax.
Derived tables can be used as the target table in DELETE and UPDATE statements, and also as the source table in the MERGE statement (in the USING clause). They cannot be used in the TRUNCATE statement, and as the target in the INSERT and MERGE statements.
For the DELETE and UPDATE statements, the syntax for defining the derived table is a bit awkward. You don’t define the derived table in the DELETE and UPDATE clauses, like you would expect, but rather in a separate FROM clause. You then specify the derived table name in the DELETE or UPDATE clause.
Here’s the general syntax of a DELETE statement against a derived table:
DELETE [ FROM ]
FROM ( ) [ AS ] [ () ]
[ WHERE ];
As an example (don’t actually run it), the following code deletes all US customers with a customer ID that is greater than the minimum for the same region (the region column represents the state for US customers):
DELETE FROM UCFROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum FROM Sales.Customers WHERE country =N'USA' ) AS UCWHERE rownum> 1;
Here’s the general syntax of an UPDATE statement against a derived table:
UPDATE
SET
FROM ( ) [ AS ] [ () ]
[ WHERE ];
As you can see, from the perspective of the definition of the derived table, it’s quite similar to the syntax of the DELETE statement.
As an example, the following code changes the company names of US customers to one using the format N'USA Cust ' + rownum, where rownum represents a position based on customer ID ordering:
BEGIN TRAN; UPDATE UC SET companyname =newcompanyname OUTPUT inserted.custid, deleted.companyname AS oldcompanyname, inserted.companyname AS newcompanynameFROM ( SELECT custid, companyname, N'USA Cust ' + CAST(ROW_NUMBER() OVER(ORDER BY custid) AS NVARCHAR(10)) AS newcompanyname FROM Sales.Customers WHERE country =N'USA' ) AS UC; ROLLBACK TRAN;
The code applies the update in a transaction that it then rolls back so that the change won't stick.
This code generates the following output, showing both the old and the new company names:
custid oldcompanyname newcompanyname------- --------------- ----------------32 Customer YSIQX USA Cust 136 Customer LVJSO USA Cust 243 Customer UISOJ USA Cust 345 Customer QXPPT USA Cust 448 Customer DVFMB USA Cust 555 Customer KZQZT USA Cust 665 Customer NYUHS USA Cust 771 Customer LCOUJ USA Cust 875 Customer XOJYP USA Cust 977 Customer LCYBZ USA Cust 1078 Customer NLTYP USA Cust 1182 Customer EYHKM USA Cust 1289 Customer YBQTI USA Cust 13
That’s it for now on the topic.
Resumo
Derived tables are one of the four main types of named table expressions that T-SQL supports. In this article I focused on the logical aspects of derived tables. I described the syntax for defining them and their scope.
Remember that a table expression is a table and as such, all of its columns must have names, all column names must be unique, and the table has no order.
The design of derived tables incurs two main flaws. In order to query one derived table from another, you need to nest your code, causing it to be more complex to maintain and troubleshoot. If you need to interact with multiple occurrences of the same table expression, using derived tables you are forced to duplicate your code, which hurts the maintainability of your solution.
You can use a table value constructor to define a table based on self-contained expressions as opposed to querying some existing base tables.
You can use derived tables in modification statements like DELETE and UPDATE, though the syntax for doing so is a bit awkward.