STRING_AGG y STRING_SPLIT: reunir y separar texto delimitado

Algunas salidas necesitan presentar varios valores en una sola cadena; otras reciben una cadena con elementos delimitados que debemos convertir en filas. STRING_AGG reúne y STRING_SPLIT separa.

Son herramientas útiles en los límites del sistema, pero una lista dentro de una columna no sustituye una relación correctamente modelada.

Reunir valores por grupo

STRING_AGG reúne los valores de varias filas y coloca el separador entre ellos. Como es una agregación, produce un resultado por cada grupo definido por la consulta.

STRING_AGG(Expresion, Separador)

CONVERT(NVARCHAR(MAX), p.Nombre) asegura que la agregación trabaje con texto Unicode, una codificación capaz de representar caracteres de numerosos sistemas de escritura, sin el límite corto de una columna intermedia. La conversión no cambia cada nombre; controla el tipo del resultado que construirá STRING_AGG.

SELECT
    p.IdCategoria,
    STRING_AGG(
        CONVERT(NVARCHAR(MAX), p.Nombre),
        N', '
    ) WITHIN GROUP (ORDER BY p.Nombre) AS Productos
FROM dbo.Producto AS p
WHERE p.Activo = 1
GROUP BY p.IdCategoria;

STRING_AGG concatena los nombres de cada categoría y coloca el separador entre valores. WITHIN GROUP establece el orden dentro de la cadena; el ORDER BY exterior, si existe, ordena las filas finales.

Los valores NULL —ausentes o desconocidos— se omiten junto con su separador. Si queremos representar su ausencia, aplicamos antes COALESCE o ISNULL.

Separar una lista

STRING_SPLIT recibe una cadena y un separador, y devuelve una fila por cada fragmento reconocido.

STRING_SPLIT(Cadena, Separador)

Cada fragmento separado puede conservar espacios o contener texto que no representa un número. TRIM retira los espacios de los extremos y TRY_CONVERT(INT, valor) intenta convertir el resultado a entero; si no puede hacerlo, devuelve NULL en lugar de detener la consulta con un error.

DECLARE @Ids NVARCHAR(100) = N'1, 3, 5';

SELECT
    TRY_CONVERT(INT, TRIM(s.value)) AS IdProducto
FROM STRING_SPLIT(@Ids, N',') AS s
WHERE TRY_CONVERT(INT, TRIM(s.value)) IS NOT NULL;

Cada fragmento se devuelve como una fila llamada value. El filtro descarta los fragmentos cuya conversión produjo NULL.

El orden de salida de STRING_SPLIT no está garantizado. Si el orden original importa y utilizamos SQL Server 2022 o posterior, podemos solicitar una columna ordinal, un número que indica la posición de cada fragmento:

SELECT
    s.ordinal,
    TRIM(s.value) AS Valor
FROM STRING_SPLIT(N'rojo,verde,azul', N',', 1) AS s
ORDER BY s.ordinal;

El tercer argumento debe ser una constante y no está disponible en versiones anteriores.

Compatibilidad

STRING_SPLIT requiere un nivel de compatibilidad de base de datos 130 o superior. Este nivel determina qué comportamientos y funciones del motor puede utilizar una base. STRING_AGG está disponible desde SQL Server 2017; el orden mediante WITHIN GROUP depende del nivel admitido.

Cuando un script debe funcionar en instalaciones antiguas, comprobamos versión y nivel de compatibilidad en lugar de asumir que la función existe.

Longitud del resultado

El tipo de la expresión agregada influye en el tipo y la longitud del resultado. Convertir de forma explícita a NVARCHAR(MAX) evita límites inesperados cuando concatenamos muchos valores, aunque también debemos controlar el tamaño que realmente puede consumir el cliente.

Cuándo no utilizar una lista delimitada

Para filtrar cientos o miles de identificadores enviados por una aplicación, un parámetro con valor de tabla —un parámetro que transporta varias filas con una estructura definida— suele ser más claro y eficiente. Para almacenar relaciones, usamos una tabla relacionada y no una cadena separada por comas.

Reservamos estas funciones para presentación, intercambio acotado o datos que llegan legítimamente como texto.

Qué conviene comprobar

Revisamos delimitador, espacios, elementos vacíos, conversión, duplicados, intercalación y tamaño. La intercalación define cómo se compara y ordena el texto. Si los valores pueden contener el mismo delimitador, necesitamos un formato estructurado como JSON, no una división ingenua.

Relacionado con