SQL Server Concatenate only the non empty columns

bq9c1y66  于 2023-03-07  发布在  其他
关注(0)|答案(3)|浏览(115)

Is there a good way on SQL Server to concatenate only the columns which are not empty? I tried with CASE WHEN but it was a complete mess, I don't want to concatenate the whole line with the name+field+', ' whenever the field is either empty or NULL.

Example:

'Sub-título: ', p.[config_metadata subtitle],', ',

Is not null, but

'Descrição: ', p.[last_timeline descricao],', ',

Is null, so I don't want to concat this whole line (name+field+', '), and so on.

Desired output, if first and third line are not empty and second line is:

'Sub-título: field content, Fwd_descrição: field content, "

SELECT 
    p._id AS id_documento,
    '16' AS id_fluxo,
    p.nP AS np,
    t.evento,
    YEAR(created_at) AS ano_doc,
    p.[config_metadata title] AS assunto, 
    CONCAT ('Sub-título: ', p.[config_metadata subtitle],', ',
        'Descrição: ', p.[last_timeline descricao],', ',
        'Fwd_descrição: ', p.[last_timeline forwardingDescription],', ',
        'Justificativa: ', p.[last_version Justificativa],', ',
        'Motivo: ', p.[last_version motivo],', ',
        'Área propriedade: ', p.[last_version areapropriedade],', ',
        'Endereço: ', p.[last_version endereco],', ',
        'Atividade: ', p.[last_version atividade],', ',
        'Atividade empreendida: ', p.[last_version atividadeempreend],', ',
        'Descreva finalidade: ', p.[last_version descrevafinalidade],', ',
        'Nome: ', p.[last_version nome],', ',
        'Nome empreendimento: ', [last_version nomeempreendimento],', ',
        'Quantidade: ', [last_version quantidade],', ',
        'Outros: ', [last_version outros],', ',
        'Data finalizado: ', p.[finalizado date] ) AS conteudo,
    '' as codigo,
    '0' as prioridade,
    t.from_id,
    t.from_nome,
    t.to_id,        
    t.to_nome,
    CONVERT(DATE,[created_at]) AS dia,
    CONVERT(TIME(0), [created_at]) AS hora,
    created_at
FROM 
    my_table AS p
JOIN 
    my_table2 AS t ON p._id = t.pid
WHERE 
    t.evento = 'Processo criado'

SQL Server version:

Microsoft SQL Server 2022 (RTM-GDR) (KB5021522) - 16.0.1050.5 (X64)   
Jan 23 2023 17:02:42   
Copyright (C) 2022 Microsoft Corporation  
Developer Edition (64-bit) on Windows 10 Home Single Language 10.0 <X64> (Build 22621: ) (Hypervisor)
eulz3vhy

eulz3vhy1#

Use + to concatenate the values in each parameter to get NULL when the column has the value NULL instead. NULLIF can be used to change "empty" (I assume zero length strings ( '' ) to NULL as well. Also, I recommend using CONCAT_WS too:

CONCAT_WS(', ',
          'Sub-título: ' + NULLIF(p.[config_metadata subtitle],''),
          'Descrição: ' + NULLIF(p.[last_timeline descricao],''),
          'Fwd_descrição: ' + NULLIF(p.[last_timeline forwardingDescription],''),
          'Justificativa: ' + NULLIF(p.[last_version Justificativa],''),
          'Motivo: ' + NULLIF(p.[last_version motivo],''),
          'Área propriedade: ' + NULLIF(p.[last_version areapropriedade],''),
          'Endereço: ' + NULLIF(p.[last_version endereco],''),
          'Atividade: ' + NULLIF(p.[last_version atividade],''),
          'Atividade empreendida: ' + NULLIF(p.[last_version atividadeempreend],''),
          'Descreva finalidade: ' + NULLIF(p.[last_version descrevafinalidade],''),
          'Nome: ' + NULLIF(p.[last_version nome],''),
          'Nome empreendimento: ' + [last_version nomeempreendimento],''),
          'Quantidade: ' + [last_version quantidade],''),
          'Outros: ' + [last_version outros],''),
          'Data finalizado: ' + NULLIF(p.[finalizado date],''))

This assumes that all your columns are a string based value. If a column isn't, you'll need to explicitly CAST / CONVERT the value to a (n)varchar of an appropriate length (and style if wanted/needed).

ncecgwcz

ncecgwcz2#

A minimal reproducible example is not provided.

Please try the following conceptual solution. It will work starting from SQL Server 2017 onwards.

SQL

-- DDL and sample data population, start
DECLARE @tbl TABLE (ID INT IDENTITY PRIMARY KEY, col1 VARCHAR(10), col2 VARCHAR(10));
INSERT @tbl (col1, col2) VALUES
('Miami', NULL),
('Orlando', 'Florida'),
(NULL, 'Florida'),
(NULL, NULL);
-- DDL and sample data population, end

SELECT * 
    , result = CONCAT_WS(', ', 'City: ' + NULLIF(col1,''), 'State: ' + NULLIF(col2,''))
FROM @tbl;

Output

IDcol1col2result
1MiamiNULLCity: Miami
2OrlandoFloridaCity: Orlando, State: Florida
3NULLFloridaState: Florida
4NULLNULL
k7fdbhmy

k7fdbhmy3#

If you had something like an EAV table with lots of columns and data normalization was not an option, you can take advantage of XML (and potentially JSON) methods to dynamically retrieve column names. This way, if the column names are updated or more are added "horizontally" to the table (for example you have no control over the data design, but you are still required to query it), the query will continue to "work". While PIVOT/UNPIVOT operations are possible as well, these have to have a 'hardcoded' list of values that would need to be maintained or utilize dynamic multi-statement SQL.

While the above example from the OP shows generating a concatenation where the "label" portion is different than the column name specifically, the following example of the query technique could be adapted to join to a "Lookup" table to match a column name to a description needed for the runtime query. The basic premise is to :

  1. Generate an automatic XML representation of the field data (potentially utilizing COLUMN_SET for SPARSE columns)
  2. Filter the representation's nodes() as needed for the query (for example, non empty, equals value, etc.)
  3. Use APPLY statements in combination with local-name(.) to get the field name associated with the filtered value. (applying any 'formatting' to the name)
  4. Aggregate the filtered information as needed.

In this example, both COLUMN_SET / SPARSE as well as 'filter out the non-field columns' techniques are shown to dynamically summarize BIT fields that are set to 'true' for a record (tested in SQL2022 LocalDB):

/* Option 1: Use "Sparse" columns and COLUMN_SET with xml 'local-name(.)' query
, to generate an XML already filtered for field columns to be queried.
See SQL Documentation for considerations of SPARSE and COLUMN_SET */
CREATE TABLE SparsePropertyList
(
  CLID     INT IDENTITY(1,1)    NOT NULL CONSTRAINT CX_SPL UNIQUE      CLUSTERED
, SerialNo       VARCHAR(42)    NOT NULL CONSTRAINT PK_SPL PRIMARY KEY NONCLUSTERED
, FieldInfo              XML             COLUMN_SET FOR ALL_SPARSE_COLUMNS
, Has_FieldA             BIT SPARSE NULL 
, Has_FieldB             BIT SPARSE NULL 
, Has_FieldC             BIT SPARSE NULL 
)

INSERT INTO dbo.SparsePropertyList ( SerialNo
                                   , Has_FieldA
                                   , Has_FieldB
                                   , Has_FieldC )
VALUES ( 'ABC',1,1,1),('AB',1,1,0),('BC',0,1,1),('C',NULL,NULL,1)

SELECT 
SPL.CLID,
SPL.SerialNo,
f1.HasFields
FROM
SparsePropertyList SPL
CROSS APPLY ( SELECT STRING_AGG(x3.p1,',') WITHIN GROUP (ORDER BY x3.p1 ASC) HasFields FROM SPL.FieldInfo.nodes('/*[text()[1] eq 1]') x1(p1)
                     CROSS APPLY ( SELECT x1.p1.query('local-name(.)').value('.','varchar(100)')) x2(p1)
                     CROSS APPLY ( SELECT CASE WHEN x2.p1 LIKE 'Has_%' THEN SUBSTRING(x2.p1,LEN('Has_!'),LEN(x2.p1)) ELSE x2.p1 END ) x3(p1)
                     WHERE x2.p1 LIKE 'Has_%' ) f1

/* Option 2: Self reference APPLY to FOR XML ELEMENTS 'local-name(.)' query
, to generate an XML node that will need to be filtered to field columns */
CREATE TABLE NonsparsePropertyList
(
  CLID     INT IDENTITY(1,1)    NOT NULL CONSTRAINT CX_NPL UNIQUE      CLUSTERED
, SerialNo       VARCHAR(42)    NOT NULL CONSTRAINT PK_NPL PRIMARY KEY NONCLUSTERED
, Has_FieldA             BIT        NULL 
, Has_FieldB             BIT        NULL 
, Has_FieldC             BIT        NULL 
)

INSERT INTO dbo.NonsparsePropertyList ( SerialNo
                                   , Has_FieldA
                                   , Has_FieldB
                                   , Has_FieldC )
VALUES ( 'ABC',1,1,1),('AB',1,1,0),('BC',0,1,1),('C',NULL,NULL,1)

SELECT NPLx.* FROM dbo.NonsparsePropertyList NPLx WHERE NPLx.CLID = 2 FOR XML AUTO,ELEMENTS

SELECT 
NPL.CLID,
NPL.SerialNo,
f1.HasFields
FROM
NonsparsePropertyList NPL
CROSS APPLY ( SELECT CAST((SELECT NPLx.* FROM dbo.NonsparsePropertyList NPLx WHERE NPLx.CLID = NPL.CLID FOR XML AUTO,ELEMENTS) AS XML) ) XPL(FieldInfo)
CROSS APPLY ( SELECT STRING_AGG(x3.p1,',') WITHIN GROUP (ORDER BY x3.p1 ASC) HasFields FROM XPL.FieldInfo.nodes('/NPLx/*[text()[1] eq 1]') x1(p1)
                     CROSS APPLY ( SELECT x1.p1.query('local-name(.)').value('.','varchar(100)')) x2(p1)
                     CROSS APPLY ( SELECT CASE WHEN x2.p1 LIKE 'Has_%' THEN SUBSTRING(x2.p1,LEN('Has_!'),LEN(x2.p1)) ELSE x2.p1 END ) x3(p1)
                     WHERE x2.p1 NOT IN ('CLID','SerialNo') AND x2.p1 LIKE 'Has_%' ) f1

/* Option 3: Use a similar method, but with FOR JSON and OPENJSON 
, to get key names to be filtered and queried. Out of scope for this example */

The results in either method include : | SerialNo | HasFields | |----------|------------| | AB | FieldA,FieldB | | ABC | FieldA,FieldB,FieldC | | BC | FieldB,FieldC | | C | FieldC |

The key point here (😉), is that the source tables could be expanded to any number of columns, and the query would not need to change to select them specifically. (As is the case in CONCAT / CONCAT_WS or PIVOT based methods)

相关问题