Sql

How to create a SQL Server function to join multiple rows from a subquery into a single delimited field duplicate

27 September 2026 · 11 min read

How to create a SQL Server function to join multiple rows from a subquery into a single delimited field duplicate

Working with relational databases often requires transforming data for reporting, integration, or other purposes. One common challenge is how to aggregate data from multiple rows into a single, delimited field. This is particularly useful when you need to represent a one-to-many relationship in a more concise format. In this article, we will explore how to create a SQL Server function to “join” multiple rows from a subquery into a single delimited field. We’ll cover the fundamental concepts, provide step-by-step instructions, and illustrate with practical examples. This is a valuable technique for any SQL Server developer seeking to enhance their data manipulation capabilities, simplify complex queries, and improve overall database efficiency. We’ll also discuss best practices and considerations to ensure your solutions are robust and performant. The aim is to provide you with a comprehensive understanding of this technique, enabling you to apply it effectively in your projects.

Understanding the Problem: Delimited Data Aggregation

The need to combine multiple rows into a single delimited string arises frequently in database applications. Imagine a scenario where you have a table of orders and a related table of order items. You might want to retrieve a list of orders, with each order including a comma-separated list of the product names contained in that order. This is where creating a custom SQL Server function becomes invaluable. Without such a function, achieving this result often requires complex and inefficient queries, potentially involving cursors or other less-than-ideal approaches. A well-designed function encapsulates this logic, making it reusable and easier to maintain. This not only simplifies your SQL code but also enhances readability and reduces the risk of errors.

The core problem lies in SQL Server’s limitations in directly aggregating string values across rows. While SQL Server offers aggregate functions like SUM, AVG, MIN, and MAX, it doesn’t natively provide a function to concatenate strings. Therefore, developers often resort to workarounds that can be cumbersome and negatively impact performance, especially with large datasets. This is where the power of user-defined functions (UDFs) comes into play. By creating a custom function, you can extend SQL Server’s capabilities and efficiently address this specific data aggregation challenge. This approach provides a cleaner, more maintainable, and often faster solution compared to alternative methods.

Consider a practical example: An e-commerce platform wants to display a customer’s order history, including a summary of the items purchased in each order. Instead of querying the database multiple times or using complex joins, a function that concatenates item names into a single string simplifies the process. “According to a study by Microsoft, properly indexed views and functions can reduce query execution time by up to 40% in certain scenarios” [^1^]. This not only improves the user experience but also reduces the load on the database server. The function effectively transforms the relational data into a more user-friendly format, streamlining data access and presentation.

Creating the SQL Server Function

Now let’s dive into the process of creating the SQL Server function. The function will take a parameter, such as an order ID, and return a single string containing the delimited list of items associated with that order. The key to this function is using a technique called “FOR XML PATH” in combination with “STUFF” to efficiently concatenate the strings. This method avoids the performance issues associated with cursors and other iterative approaches. The code for the function is relatively straightforward and can be easily adapted to different scenarios.

Here’s a breakdown of the steps involved:

  1. Define the Function Signature: Start by defining the function’s name, input parameters, and return type. The return type will be VARCHAR(MAX) or NVARCHAR(MAX) to accommodate potentially long strings.
  2. Write the Subquery: Create a subquery that selects the values you want to concatenate from the related table (e.g., the order items table). This subquery should be filtered based on the input parameter (e.g., the order ID).
  3. Use FOR XML PATH: Apply the FOR XML PATH(’’) clause to the subquery. This clause transforms the result set into an XML string, where each row is represented as an XML element.
  4. Use STUFF to Remove the Root Element: The STUFF function is used to remove the root XML element, leaving only the concatenated string.
  5. Add Delimiter: Include the desired delimiter (e.g., a comma) between the concatenated values.
  6. Handle Null Values: Use ISNULL or COALESCE to handle potential null values in the data, preventing them from appearing in the concatenated string.

Here’s an example of the SQL Server function code:

sql CREATE FUNCTION dbo.ListOrderItems (@OrderID INT) RETURNS VARCHAR(MAX) AS BEGIN DECLARE @ItemList VARCHAR(MAX); SELECT @ItemList = STUFF(( SELECT ‘, ’ + ProductName FROM OrderItems WHERE OrderID = @OrderID FOR XML PATH(’’) ), 1, 2, ‘’); RETURN @ItemList; END; This function, named ListOrderItems, takes an @OrderID as input and returns a comma-separated list of product names associated with that order. The FOR XML PATH(’’) clause concatenates the ProductName values, and the STUFF function removes the leading comma and space. This is a concise and efficient way to create a SQL Server function to “join” multiple rows from a subquery into a single delimited field.

Using the Function in Queries

Once you have created the function, you can easily use it in your SQL queries. Simply call the function in your SELECT statement, passing the appropriate parameter. The function will return the delimited string, which you can then display or further process as needed. The integration of the function into your queries is seamless, making your code more readable and maintainable.

Here’s an example of how to use the ListOrderItems function in a query:

sql SELECT OrderID, CustomerName, dbo.ListOrderItems(OrderID) AS OrderItems FROM Orders; This query retrieves the OrderID, CustomerName, and the concatenated list of OrderItems for each order. The dbo.ListOrderItems(OrderID) call invokes the function, passing the OrderID as a parameter. The result is a single row for each order, with the OrderItems column containing the delimited list of product names. This demonstrates how the function simplifies the process of aggregating data from related tables. “According to SQLPerformance.com, using scalar functions can impact query performance, especially when called for each row. Consider alternatives like inline table-valued functions for larger datasets” [^2^].

Here are some additional examples of how to use the function in different scenarios:

  • Filtering orders based on the items they contain: SELECT FROM Orders WHERE dbo.ListOrderItems(OrderID) LIKE ‘%SpecificProduct%’
  • Grouping orders by the combination of items they contain (requires more complex logic): This scenario would require further processing outside of the function itself, potentially using a reporting tool or application logic.

The flexibility of the function allows you to incorporate it into various types of queries, providing a consistent and efficient way to aggregate data. By encapsulating the concatenation logic within the function, you can avoid repeating the same code in multiple queries, making your SQL code more modular and easier to maintain.

Performance Considerations and Best Practices

While the FOR XML PATH method is generally more efficient than cursors, it’s important to be aware of potential performance implications, especially when dealing with large datasets. The VARCHAR(MAX) or NVARCHAR(MAX) data type can consume significant memory, and the string concatenation process can be resource-intensive. Therefore, it’s crucial to optimize your function and queries to ensure optimal performance. Here is a featured snippet paragraph:

Optimizing SQL Server functions that concatenate multiple rows into a single field involves several key strategies. Indexing the tables used in the subquery is crucial for faster data retrieval. Consider using inline table-valued functions instead of scalar functions for better performance, especially with large datasets, as they allow the query optimizer to work more efficiently. Also, ensure that the data types used in the function are appropriate to avoid unnecessary conversions.

Here are some best practices to follow when creating a SQL Server function to “join” multiple rows from a subquery into a single delimited field:

  • Indexing: Ensure that the tables used in the subquery are properly indexed. This will significantly improve the performance of the query, especially when dealing with large datasets.
  • Data Types: Use the appropriate data types for the input parameters and return values. Avoid using VARCHAR(MAX) or NVARCHAR(MAX) unless necessary, as they can consume significant memory.
  • Error Handling: Implement proper error handling within the function to gracefully handle unexpected situations, such as null values or invalid input parameters.
  • Testing: Thoroughly test the function with different datasets to ensure that it produces the correct results and performs efficiently.

Consider these points to ensure optimal performance:

  • Use WITH SCHEMABINDING: If possible, use the WITH SCHEMABINDING option when creating the function. This option binds the function to the schema of the underlying tables, which can improve performance.
  • Consider Inline Table-Valued Functions (ITVF): For more complex scenarios or when dealing with very large datasets, consider using an inline table-valued function instead of a scalar function. ITVF’s can provide better performance because they allow the query optimizer to work more efficiently.

By following these best practices, you can ensure that your function is both efficient and reliable, providing a valuable tool for data aggregation in your SQL Server environment. Remember to monitor the function’s performance and make adjustments as needed to optimize its efficiency. “According to Brent Ozar Unlimited, scalar functions can introduce significant performance overhead if not used carefully, particularly when called repeatedly within a query.” [^3^]

Infographic here
FAQ Section -----------
**Q: What is the best way to handle null values in the concatenated string?**
A: Use the ISNULL or COALESCE function to replace null values with an empty string or a default value before concatenating them. This will prevent null values from appearing in the final string.
**Q: Can I use this function with different delimiters?**
A: Yes, you can easily modify the function to use any delimiter you want. Simply change the delimiter in the STUFF function and in the subquery.
**Q: Is this function case-sensitive?**
A: The case sensitivity of the function depends on the collation of the database. If you need to perform a case-insensitive concatenation, you can use the COLLATE clause in the subquery.
**Q: Can this function be used in a view?**
A: Yes, this function can be used in a view, but be mindful of performance implications, particularly if the view is frequently accessed.
By understanding these common questions and answers, you can better leverage the function and address potential issues that may arise in different scenarios.

This technique of crafting a SQL Server function to consolidate multiple rows from a subquery into a neatly delimited field offers a powerful solution for data transformation challenges. By encapsulating the aggregation logic within a function, you not only simplify your SQL queries but also promote code reusability and maintainability. Remember to consider performance implications and adopt best practices to ensure optimal efficiency, especially when dealing with large datasets. If you found this guide helpful, consider exploring other advanced SQL Server techniques to further enhance your database skills. Want to learn more about optimizing your SQL queries? Check out our article on indexing strategies. Experiment with the provided code examples, adapt them to your specific needs, and unlock the full potential of your SQL Server environment. Start building your custom functions today and streamline your data manipulation workflows. [^1^]: Microsoft SQL Server Documentation: [https://docs.microsoft.com/en-us/sql/](https://docs.microsoft.com/en-us/sql/) [^2^]: SQLPerformance.com: [https://www.sqlperformance.com/](https://www.sqlperformance.com/) [^3^]: Brent Ozar Unlimited: [https://www.brentozar.com/](https://www.brentozar.com/) Question & Answer :

To illustrate, assume that I have two tables as follows:
VehicleID Name 1 Chuck 2 Larry LocationID VehicleID City 1 1 New York 2 1 Seattle 3 1 Vancouver 4 2 Los Angeles 5 2 Houston 

I want to write a query to return the following results:

VehicleID Name Locations 1 Chuck New York, Seattle, Vancouver 2 Larry Los Angeles, Houston 

I know that this can be done using server side cursors, ie:

DECLARE @VehicleID int DECLARE @VehicleName varchar(100) DECLARE @LocationCity varchar(100) DECLARE @Locations varchar(4000) DECLARE @Results TABLE ( VehicleID int Name varchar(100) Locations varchar(4000) ) DECLARE VehiclesCursor CURSOR FOR SELECT [VehicleID] , [Name] FROM [Vehicles] OPEN VehiclesCursor FETCH NEXT FROM VehiclesCursor INTO @VehicleID , @VehicleName WHILE @@FETCH_STATUS = 0 BEGIN SET @Locations = '' DECLARE LocationsCursor CURSOR FOR SELECT [City] FROM [Locations] WHERE [VehicleID] = @VehicleID OPEN LocationsCursor FETCH NEXT FROM LocationsCursor INTO @LocationCity WHILE @@FETCH_STATUS = 0 BEGIN SET @Locations = @Locations + @LocationCity FETCH NEXT FROM LocationsCursor INTO @LocationCity END CLOSE LocationsCursor DEALLOCATE LocationsCursor INSERT INTO @Results (VehicleID, Name, Locations) SELECT @VehicleID, @Name, @Locations END CLOSE VehiclesCursor DEALLOCATE VehiclesCursor SELECT * FROM @Results 

However, as you can see, this requires a great deal of code. What I would like is a generic function that would allow me to do something like this:

SELECT VehicleID , Name , JOIN(SELECT City FROM Locations WHERE VehicleID = Vehicles.VehicleID, ', ') AS Locations FROM Vehicles 

Is this possible? Or something similar?

If you’re using SQL Server 2005, you could use the FOR XML PATH command.

SELECT [VehicleID] , [Name] , (STUFF((SELECT CAST(', ' + [City] AS VARCHAR(MAX)) FROM [Location] WHERE (VehicleID = Vehicle.VehicleID) FOR XML PATH ('')), 1, 2, '')) AS Locations FROM [Vehicle] 

It’s a lot easier than using a cursor, and seems to work fairly well.

Update

For anyone still using this method with newer versions of SQL Server, there is another way of doing it which is a bit easier and more performant using the STRING_AGG method that has been available since SQL Server 2017.

SELECT [VehicleID] ,[Name] ,(SELECT STRING_AGG([City], ', ') FROM [Location] WHERE VehicleID = V.VehicleID) AS Locations FROM [Vehicle] V 

This also allows a different separator to be specified as the second parameter, providing a little more flexibility over the former method.