Eu tenho várias centenas de tabelas em um banco de dados que têm a mesma estrutura:
Alguns Id, Pos, Número Variado de Outros Campos
Assim, por exemplo, uma tabela pode ficar assim:
PersonId, Pos, Hobby, Degree
12345, 1, Basketball, Bachelor of Science
12345, 2, Baseball, Master of Science
12345, 3, Boxing, NULL
12345, 4, Tennis, NULL
22222, 1, Golf, Bachelor of Science
22222, 2, NULL, Master of Science
22222, 3, NULL, Doctorate
Eu quero acumular os valores para cada coluna 3-N. Então isso se tornaria:
12345, "Basketball, Baseball, Boxing, Tennis", "Bachelor of Science, Master of Science"
22222, "Golf", "Bachelor of Science, Master of Science, Doctorate"
Outra tabela pode ficar assim:
ClientId, Pos, Location, Language, CommunicationType
33333, 1, North Carolina, English, Phone
33333, 2, New York, Spanish, Email
33333, 3, NULL, Portuguese, NULL
44444, 1, California, English, Phone
44444, 2, NULL, NULL, Email
e torne-se isso:
33333, "North Carolina, New York", "English, Spanish, Portugeue", "Phone, Email"
44444, "California", "English", "Phone, Email"
O que eu gostaria de fazer é criar um TVF onde eu possa especificar o nome da tabela e fazer com que a função retorne seus campos. Idealmente, enrolado como acabei de demonstrar acima.
Solomon Rutzky forneceu uma solução ( SQL Server: Pass table name into table value function as a parameter ) onde mostrou como usar instruções XML e CASE para aceitar nomes de tabelas em um TVF.
Segue uma adaptação:
DECLARE @TableName sysname = 'objects'
/*
DECLARE @TableName sysname = 'columns'
DECLARE @TableName sysname = 'indexes'
*/
SELECT tab.BaseData.value(N'/row[1]/@name', N'VARCHAR(128)') AS [name],
tab.BaseData.value(N'/row[1]/@object_id', N'BIGINT') AS [object_id],
*
FROM (
SELECT CASE @TableName
WHEN N'objects' THEN (SELECT * FROM master.sys.tables FOR XML RAW, TYPE)
WHEN N'indexes' THEN (SELECT * FROM master.sys.indexes FOR XML RAW, TYPE)
WHEN N'columns' THEN (SELECT * FROM master.sys.columns FOR XML RAW, TYPE)
END AS [BaseData]
) tab;
Se eu criasse uma instrução CASE monstro e contasse todos os nomes de tabela de entrada possíveis, existe uma maneira de referenciar as colunas por posição ordinal (em vez de nome como estou fazendo acima)? Melhor ainda, arregaçar e delimitar seus valores também (que é meu objetivo final)?
Agradeço antecipadamente!
Além disso, aqui está o DDL para criar minhas duas tabelas de amostra:
CREATE TABLE [dbo].[Person](
[PersonId] [int] NULL,
[Pos] [int] NULL,
[Hobby] [varchar](100) NULL,
[Degree] [varchar](50) NULL
)
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (12345, 1, N'Basketball', N'Bachelor of Science')
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (12345, 2, N'Baseball', N'Master of Science')
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (12345, 3, N'Boxing', NULL)
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (12345, 4, N'Tennis', NULL)
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (22222, 1, N'Golf', N'Bachelor of Science')
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (22222, 2, NULL, N'Master of Science')
GO
INSERT [dbo].[Person] ([PersonId], [Pos], [Hobby], [Degree]) VALUES (22222, 3, NULL, N'Doctorate')
GO
CREATE TABLE [dbo].[Client](
[ClientId] [int] NULL,
[Pos] [int] NULL,
[Location] [varchar](100) NULL,
[Language] [varchar](50) NULL,
[CommunicationType] [varchar](50) NULL
)
GO
INSERT [dbo].[Client] ([ClientId], [Pos], [Location], [Language], [CommunicationType]) VALUES (33333, 1, N'North Carolina', N'English', N'Phone')
GO
INSERT [dbo].[Client] ([ClientId], [Pos], [Location], [Language], [CommunicationType]) VALUES (33333, 2, N'New York', N'Spanish', N'Email')
GO
INSERT [dbo].[Client] ([ClientId], [Pos], [Location], [Language], [CommunicationType]) VALUES (33333, 3, NULL, N'Portuguese', NULL)
GO
INSERT [dbo].[Client] ([ClientId], [Pos], [Location], [Language], [CommunicationType]) VALUES (44444, 1, N'California', N'English', N'Phone')
GO
INSERT [dbo].[Client] ([ClientId], [Pos], [Location], [Language], [CommunicationType]) VALUES (44444, 2, NULL, NULL, N'Email')
GO
SELECT * FROM Person;
SELECT * FROM Client;
Sim, existe, mas não tenho certeza de como isso ajudaria você a fazer o que deseja. Você coloca a posição ordinal no predicado, como já faz para
row[1]
.Alterar
'/row[1]/@name'
para obter a terceira coluna seria semelhante a'/row[1]/@*[3]'
. Você deve estar ciente de que valores nulos não criam nenhum atributo, portanto, seus dados no terceiro atributo nem sempre virão da terceira coluna.Para corrigir isso, você poderia gerar elementos em vez de atributos para valores de coluna e usar
XSINIL
para obter elementos vazios para valores nulos em colunas, ex:SELECT * FROM master.sys.indexes FOR XML RAW, ELEMENTS XSINIL, TYPE
. Então você precisa selecionar o terceiro elemento do XML em vez do terceiro atributo'/row[1]/*[3]'
.Você já está no caminho para "criar uma instrução CASE monstruosa e contabilizar todos os nomes de tabelas de entrada possíveis" , então por que não criar uma consulta monstruosa que faça o que você deseja, sem o material XML.
Você pode usar SQL dinâmico em meta tabelas para gerar a consulta acima se estiver em uma situação em que precise atualizar a função com frequência ou até mesmo automaticamente.
Como você está no SQL Server 2016, não
string_agg()
precisa usarfor xml path
para fazer a concatenação. A consulta ficou maior, mas é o mesmo princípio e ainda pode ser criada usando SQL dinâmico.Você não pode usar SQL dinâmico aqui, pois isso não funcionará dentro de um TVF. Você pode usar dynamic para gerar o código real abaixo.
Dado que você está no SQL Server 2016, você não tem
STRING_AGG
disponível, então você terá que usar oFOR XML/STUFF
método, que é bastante complexo com várias colunas.Não é necessário ou eficiente continuar consultando os dados novamente para cada coluna, você pode usar uma combinação de
APPLY
e.value
Para reiterar, você precisa substituir
col1,col2
pelos nomes reais das colunas, o mesmo que os nomes das tabelas. Você não pode fazer isso com SQL dinâmico em uma função.Para completar, vou mostrar o
STRING_AGG
método que é muito mais simples:Eu não acredito que seja possível fazer exatamente o que você quer com uma função, porque ela deve ter uma forma de saída fixa (número, tipos e nomes de colunas).
Uma aproximação possível é retornar um número fixo de colunas (com nomes genéricos), cada uma contendo uma agregação de strings, com null retornado para as colunas extras não aplicáveis a uma tabela de origem com menos colunas que o máximo.
Conforme observado em outras respostas,
STRING_AGG
é ideal para isso, mas não está disponível no SQL Server 2016. Um substituto eficiente para isso pode ser fornecido por uma função com valor de tabela de streaming SQL CLR, conforme observado nas perguntas e respostas vinculadas. Agora, eu conheço você 'll dizer que você não pode usar SQL CLR por qualquer motivo, mas para o benefício de futuros leitores com um requisito semelhante, aqui está um exemplo de implementação.O código usa uma conexão de loopback por motivos técnicos, portanto, os primeiros parâmetros especificam o nome do servidor/instância e o nome do banco de dados. O terceiro parâmetro é a tabela, que deve conter uma coluna de id de inteiros e uma coluna de ordenação de inteiros na segunda posição. As colunas restantes são consideradas strings.
Esta implementação de amostra é limitada a cinco dessas colunas. Ele faz uma única passagem ordenada pela tabela e minimiza o uso de memória mantendo apenas o grupo atual na memória de uma só vez. Deve ser muito mais rápido do que as
XML PATH
soluções.Exemplo de chamadas e resultados
T-SQL
Fonte C#