sqlsql-server

How to use STRING_AGG function inside another STRING_AGG function in SQL Server


I have 3 tables with 1-n relations between them

CREATE TABLE Province
(
    Id BIGINT PRIMARY KEY,
    Name NVARCHAR(255),
    -- ...
)

CREATE TABLE District
(
    Id BIGINT PRIMARY KEY,
    Name NVARCHAR(255),
    ProvinceId BIGINT REFERENCES [Province](Id)
)

CREATE TABLE Ward
(
    Id BIGINT PRIMARY KEY,
    Name NVARCHAR(255),
    DistrictId BIGINT REFERENCES [District](Id)
)

I want to get detail data of a province which looks like this

{
    "id": 1,
    "province_name": "province name",
    "districts": [
        {
            "id": 1,
            "district_name": "district-1",
            "wards": [1,2,3]
        },
        {
            "id": 2,
            "district_name": "district-2",
            "wards": [4,5]
        }
    ]
}

(1 province has many districts, 1 district has many wards)

Here's what I implemented using the STRING_AGG built-in function

select CONCAT(
    '{"id":"', p.[id], '"',
    ',"province_name":', p.ProvinceName,
    ',"districts":',
    CONCAT(
    '[{',
    STRING_AGG(
        CONCAT('"id":', CAST(po.Id AS VARCHAR(10)),
        ',"name":"', po.[Name],
        '","district_name":[', STRING_AGG(CAST(w.id AS VARCHAR(10)), ','), ']'), '},{'),
    '}]'),
    '}')
from 
    province p
left join 
    district d on d.provinceId = p.id
left join 
    ward w on w.districtId = d.id
where 
    p.id = @id

But SQL Server throws this exception:

Cannot perform an aggregate function on an expression containing an aggregate or a subquery

I tried to search similar issue from this site, but couldn't find expect results.

How can I implement to get data from 3 1-n layers?


Solution

  • You can pre-group ward.Id, as shown in Cte.
    Or use cross apply with concatenated ward.id's

    See example

    declare @id int =1;
    
    with wardList as(
      select d.Id as DistrictId,string_agg(w.id,',') wList
      from district d
      left join ward w on w.DistrictId=d.Id
      where d.ProvinceId=@id
      group by d.id
    )
    select CONCAT(
        '{"id":"', p.[id], '"',
        ',"province_name":', max(p.Name),
        ',"districts":',
        CONCAT(
        '[{',
        STRING_AGG(
            CONCAT('"id":', CAST(d.Id AS VARCHAR(10)),
            ',"district_name":"', d.Name,
            '","wards":[', wList, ']'), '},{'),
        '}]'),
        '}')
    from province p
    left join district d on d.provinceId = p.id
    left join wardList w on w.DistrictId = d.id
    where p.id = @id
    group by P.ID
    

    Result is

    {"id":"1","province_name":Province1,
       "districts":[
               {"id":1,"district_name":"P1District1","wards":[1,2,3]}, 
               {"id":2,"district_name":"P1District2","wards":[4,5,6]}, 
               {"id":3,"district_name":"P1District3","wards":[]}
          ]
    }
    

    Similar query

    select CONCAT(
        '{"id":"', p.[id], '"',
        ',"province_name":', max(p.Name),
        ',"districts":',
        CONCAT(
        '[{',
        STRING_AGG(
            CONCAT('"id":', CAST(d.Id AS VARCHAR(10)),
            ',"district_name":"', d.Name,
            '","wards":[', wList, ']'), '},{'),
        '}]'),
        '}')
    from province p
    left join district d on d.provinceId = p.id
    cross apply (
     select string_agg(w.id,',') wList
      from district d2
      left join ward w on w.DistrictId=d.Id 
      where d2.ProvinceId=p.id and w.DistrictId=d2.Id
      group by d2.id
     ) wardList 
    where p.id = @id
    group by P.ID
    

    Output is

    {"id":"1","province_name":Province1
        ,"districts":[
          {"id":1,"district_name":"P1District1","wards":[1,2,3]},
          {"id":2,"district_name":"P1District2","wards":[4,5,6]}
        ]
    }
    

    With test data

    insert into Province values 
     (1,'Province1')
    ,(2,'Province1')
    insert into District values 
     (1, 'P1District1', 1)
    ,(2, 'P1District2', 1)
    ,(3, 'P1District3', 1)
    ,(4, 'P2District1', 2)
    ;
    insert into Ward values 
     (1, 'P1D1Ward1', 1)
    ,(2, 'P1D1Ward2', 1)
    ,(3, 'P1D1Ward3', 1)
    ,(4, 'P1D2Ward1', 2)
    ,(5, 'P1D2Ward2', 2)
    ,(6, 'P1D2Ward3', 2)
    ;
    

    fiddle