Dynamic SQL PIVOT Generator — SQL Server

Generate a SQL Server dynamic PIVOT query that discovers its column values at runtime — no hardcoded lists. New values automatically appear as columns without any code changes. Uses STRING_AGG, QUOTENAME, and sp_executesql for safe, production-ready output.

SQL Server / Azure SQL
📚 Guide & Tutorial: Dynamic PIVOT in SQL Server: Auto-Generate Column Lists — step-by-step walkthrough with examples.

Examples

Example: Dynamic monthly sales pivot — auto-discovers months

Generates a pivot where the month columns are found by querying the table. Works even as new months are added.

DECLARE @cols   NVARCHAR(MAX) = N'';
DECLARE @sql    NVARCHAR(MAX) = N'';

SELECT @cols = STRING_AGG(QUOTENAME(Month), ', ')
               WITHIN GROUP (ORDER BY MIN(MonthOrder))
FROM (
    SELECT DISTINCT Month,
           CASE Month
               WHEN 'Jan' THEN 1 WHEN 'Feb' THEN 2
               WHEN 'Mar' THEN 3 WHEN 'Apr' THEN 4
               ELSE 99 END AS MonthOrder
    FROM dbo.SalesData
) x;

SET @sql = N'
SELECT [ProductName], ' + @cols + N'
FROM (
    SELECT [ProductName], [Month], [Revenue]
    FROM dbo.SalesData
) AS [src]
PIVOT (
    SUM([Revenue])
    FOR [Month] IN (' + @cols + N')
) AS [pvt]
ORDER BY [ProductName];';

EXEC sp_executesql @sql;

Tip: print @sql before EXEC to debug. Add PRINT @sql; after SET @sql to see the final query string.

Example: Count employees by role and department — dynamic roles

Employee table where new roles may be added over time. Dynamic PIVOT ensures the new roles appear automatically.

DECLARE @cols   NVARCHAR(MAX) = N'';
DECLARE @sql    NVARCHAR(MAX) = N'';

SELECT @cols = STRING_AGG(QUOTENAME([Role]), ', ')
FROM (SELECT DISTINCT [Role] FROM dbo.Employees) x;

SET @sql = N'
SELECT [Department], ' + @cols + N'
FROM (
    SELECT [Department], [Role], [EmployeeID]
    FROM dbo.Employees
) AS [src]
PIVOT (
    COUNT([EmployeeID])
    FOR [Role] IN (' + @cols + N')
) AS [pvt]
ORDER BY [Department];';

EXEC sp_executesql @sql;

What This Tool Does

The Dynamic PIVOT Generator builds a T-SQL stored procedure pattern that constructs a PIVOT query at runtime. Instead of listing the pivot values in the query code, the generated SQL first queries the table to find all distinct values of the column field, builds those into a column list string using STRING_AGG and QUOTENAME, then assembles and executes the final PIVOT query.

This means when new values appear in the column field — a new product, a new region, a new month — they automatically appear as columns in the pivot output without any code changes.

How to Use It

Static vs Dynamic PIVOT

CriterionStatic PIVOTDynamic PIVOT
Column valuesHardcoded in queryQueried at runtime
New values handled automaticallyNoYes
Easier to read and reviewYesNo
PerformanceSlightly fasterAdds one extra SELECT
SQL injection riskNoneLow if using QUOTENAME()
DebuggingStraightforwardPrint @sql first
Best forKnown, stable value listsDynamic or growing value sets

Why Dynamic PIVOT?

Static PIVOT breaks silently when new values appear: the new column simply does not exist in the output. If you are building a report that runs on a schedule — a monthly sales report, a weekly status by project — and new months or projects are added, a static PIVOT requires a code change every time.

Dynamic PIVOT solves this by treating the column value discovery as a runtime step. The QUOTENAME() function wraps each value in square brackets safely, preventing any column value from being interpreted as SQL syntax.

Privacy & Security

All SQL generation runs in your browser. Your table names and schema details are never transmitted to any server. The generated query uses QUOTENAME() on all discovered column values, which is the correct SQL Server defense against SQL injection in dynamic SQL contexts.

Common Problems & Fixes

ProblemCauseFix
@cols is NULL after SELECTNo rows match the column discovery queryCheck the column filter — test the discovery SELECT separately
String or binary data would be truncatedColumn list exceeds NVARCHAR(MAX) in practiceFilter the column discovery query to reduce the number of distinct values
Incorrect syntax near the keyword PIVOTSQL Server compatibility level too lowSET COMPATIBILITY_LEVEL = 130 or higher on the database
sp_executesql fails with parameter errorPassing parameters to a dynamic PIVOT is complexUse EXEC(@sql) if no parameters needed; restructure for parameterization if required
STRING_AGG not recognizedSQL Server version below 2017Replace STRING_AGG with STUFF/FOR XML PATH pattern — static PIVOT may be simpler in this case

💡 If your column values are known and stable, a static PIVOT query is simpler, more readable, and slightly faster to execute. For normalizing a pivoted table back into rows, use the UNPIVOT Generator. To clean up the generated SQL's indentation and keyword casing, paste it into the SQL Formatter.

Dynamic PIVOT Workflow

Dynamic PIVOT connects to the full SQL query builder toolkit:

Frequently Asked Questions

What is dynamic PIVOT in SQL Server?
Dynamic PIVOT builds the PIVOT query as a string at runtime, discovering the pivot column values by querying the table itself. This means new values automatically appear as columns without any code changes.
Is dynamic SQL safe?
Dynamic SQL built from table data (not user input) is safe. The risk is when user-supplied strings are concatenated into the SQL. The generated query uses QUOTENAME() to bracket all column values, which prevents SQL injection from column value content.
Why use sp_executesql instead of EXEC?
sp_executesql supports parameterized queries and has better performance due to plan caching. For dynamic PIVOT, both work — but sp_executesql is the best-practice choice.
Can I filter which values become columns?
Yes. Add a WHERE clause in the value-discovery subquery. The generator includes an optional filter field for this purpose.
How do I debug a dynamic PIVOT query?
Change the final EXEC to PRINT @sql or SELECT @sql to see the generated SQL string before running it. This is the most effective way to diagnose dynamic SQL errors.

Related SQL Tools