Commenting Complex SQL Statements — Parallel Inline Documentation That Keeps Code Readable

Anyone who writes a 200-line SELECT with a recursive CTE understands it completely while writing it — and three weeks later, not a word of it. Inline comments are the safety net against that. The problem: placed badly, they destroy the very readability they are meant to preserve.

What this article covers:

  • The two kinds of inline comments in T-SQL — single-line (--) and block (/* */) — and when to use which.
  • The anti-pattern: comments that tear the statement apart.
  • The parallel inline documentation — a right-aligned comment block that keeps the statement compact.
  • A template you can copy as a starting point for your own statements.
  • Why the same pattern works in Postgres — and where the one difference lies.
  • Why commenting matters more for AI-generated SQL, not less.

Prerequisite: The examples run against AdventureWorksDW2017 (table [dbo].[DimEmployee], a recursive CTE over ParentEmployeeKey). SSMS serves as the example editor — the principles apply to any SQL editor.

Why inline documentation?

The value of documentation is largely undisputed — and yet it is the first thing to fall victim to project pressure. „Just read the code” is not documentation: not everyone can read SQL, and documentation also addresses the business side, project management and management. Inline documentation is the net with a double floor — if it too is missing, a statement is no longer maintainable once the knowledge has left the developers’ heads. And that knowledge is fleeting: after just a few weeks even the author struggles to follow a complex statement; in a multi-developer project this only gets worse.

Two kinds of inline comments

T-SQL knows two kinds of inline comments: single-line comments and block comments.

Single-line comments start with two consecutive hyphens; everything to their right is treated as a comment. The hyphens may appear at any position within a line:

  1: -- Inline comment
  

If the text spans several lines, the opening sequence must precede every line — a bit of effort with many lines.

Block comments start with a slash followed by an asterisk and end with the reverse sequence. They enclose arbitrary regions:

  1: /*
  2: Block comment
  3: */

Which variant is better is — as so often — a matter of debate. Despite the extra effort for the opening sequence, I prefer the single-line form: especially with multi-line text, the leading -- clearly separates each comment from the rest of the statement; in addition, the sequence can serve as a structuring element within a complex statement.

The block selection (called „column selection” in SSMS, „box selection” in VS Code and Azure Data Studio) keeps the effort for the hyphens low: the cursor is spanned across multiple lines, a single keystroke applies to all of them at once — so you can comment or uncomment several lines in one go (see The Functional Aesthetics of SQL). Also helpful are the shortcuts Ctrl+K, Ctrl+C (comment, C for Comment) and Ctrl+K, Ctrl+U (uncomment, U for Uncomment) — both act on the entire current selection.

Example 1: inconsistent inline documentation

The following example shows inline documentation as it is often found: no consistent use of the comment functions, no consistent indentation, every comment occupying a full line. Despite the cleanly formatted statement, it is hard to read — because of the unstructured documentation. The complexity lies in a recursive Common Table Expression (CTE) and in documenting that feature. The statement runs in the AdventureWorksDW2017 database.

  1: WITH
  2: CTE_Employee AS
  3: (
  4: -- This SELECT statement returns the anchor element of the recursive query. The 
  5: -- anchor element is the top level Employee of Adventure Works, the CEO.
  6:    SELECT
  7:        [EmployeeKey]
  8:       ,[FirstName]
  9:       ,[LastName]
 10:       ,[Title]
 11:       ,[ParentEmployeeKey]
 12:       ,[VacationHours]
 13:       ,[SickLeaveHours]
 14: -- >> The field Level is used to calculate the hierarchy level of an employee. 
 15: --    The CEO is on Level 1. All top level managers are on Level 2. 
 16: --    Regional Managers, Technical supervisors etc. are on Level 3
 17:       ,1 AS [Level]
 18:    FROM
 19:       [dbo].[DimEmployee]
 20:    WHERE
 21:       [ParentEmployeeKey] IS NULL
 22:    -- >> The operator UNION ALL is the only operator allowed between the 
 23:    --    anchor and the first recursive member.
 24:    UNION ALL
 25:    SELECT
 26:        T01.[EmployeeKey]
 27:       ,T01.[FirstName]
 28:       ,T01.[LastName]
 29:       ,T01.[Title]
 30:       ,T01.[ParentEmployeeKey]
 31:       ,T01.[VacationHours]
 32:       ,T01.[SickLeaveHours]
 33: --  >> Increases the level for each recursion
 34:       ,T02.[Level] + 1 AS [Level]
 35:     FROM
 36:        [dbo].[DimEmployee] T01
 37:        INNER JOIN CTE_Employee T02
 38:        ON
 39:          T01.[ParentEmployeeKey] = T02.[EmployeeKey]
 40: )
 41: /* # If the recursive member query definition returns the same values for both
 42:      the parent and child columns, an infinite loop is created. To avoid an 
 43:      infinite loop you can limit the number of recursions. By default SQL 
 44:      Server limits the recursions to 100. The maximum number is limited to 
 45:      32767 recursions. 
 46:    # The number of recursions can be limited with the option MAXRECURSION
 47:    # If the number of recursion exceeds the specified value for MAXRECURSION
 48:      SQL Server will throw an exception
 49:    # The option cannot be used within a CTE */
 50: SELECT
 51:     [EmployeeKey]
 52:    ,[FirstName]
 53:    ,[LastName]
 54:    ,[Title]
 55:    ,[ParentEmployeeKey]
 56:    ,[Level]
 57:    ,[VacationHours]
 58:    ,[SickLeaveHours]
 59: -- >> Classifies the vacation hours by Level (3 levels)
 60:    ,NTILE(3)
 61:        OVER (PARTITION BY [Level]
 62:                  ORDER BY [VacationHours]
 63:             ) AS [VacationHours_NTILE]
 64: -- >> Orders the vacation hours by Level
 65:    ,DENSE_RANK()
 66:        OVER (PARTITION BY [Level]
 67:                  ORDER BY [VacationHours]
 68:             ) AS [VacationHours_DENSE_RANK]
 69: -- >> Classifies the sick leave hours by Level (3 levels)
 70:    ,NTILE(3)
 71:        OVER (PARTITION BY [Level]
 72:                  ORDER BY [SickLeaveHours]
 73:             ) AS [SickLeaveHours_NTILE]
 74: -- >> Orders the sick leave hours by Level
 75:    ,DENSE_RANK()
 76:        OVER (PARTITION BY [Level]
 77:                  ORDER BY [SickLeaveHours]
 78:             ) AS [SickLeaveHours_DENSE_RANK]
 79: FROM
 80:    CTE_Employee
 81: -- >> Limits the maximum number of recursions to 5 recursions
 82: -- OPTION (MAXRECURSION 5)
 83: --WHERE
 84: --   [Level] = 2
 85: ORDER BY
 86:    [Level] ASC;
 

Example 2: parallel inline documentation

The same statement, reworked with a parallel inline documentation: to the right of the statement sits a dedicated block for the documentation, set up using the single-line comment sequence (--). The sequences are left-aligned across all lines.

  1: WITH                                                   
  2: CTE_Employee AS                                        -- --------------------------------------------------------------------------------
  3: (                                                      --
  4:    SELECT                                              -- This SELECT statement returns the anchor element of the recursive query. The
  5:        [EmployeeKey]                                   -- anchor element is the top level Employee of Adventure Works, the CEO.
  6:       ,[FirstName]                                     --
  7:       ,[LastName]                                      --
  8:       ,[Title]                                         --
  9:       ,[ParentEmployeeKey]                             --
 10:       ,[VacationHours]                                 --
 11:       ,[SickLeaveHours]                                --
 12:       ,1 AS [Level]                                    -- >> The field Level is used to calculate the hierarchy level of an employee.
 13:    FROM                                                --    The CEO is on Level 1. All top level managers are on Level 2.
 14:       [dbo].[DimEmployee]                              --    Regional Managers, Technical supervisors etc. are on Level 3
 15:    WHERE                                               --
 16:       [ParentEmployeeKey] IS NULL                      --
 17:    UNION ALL                                           -- >> The operator UNION ALL is the only operator allowed between the
 18:    SELECT                                              --    anchor and the first recursive member.
 19:        T01.[EmployeeKey]                               --
 20:       ,T01.[FirstName]                                 --
 21:       ,T01.[LastName]                                  --
 22:       ,T01.[Title]                                     --
 23:       ,T01.[ParentEmployeeKey]                         --
 24:       ,T01.[VacationHours]                             --
 25:       ,T01.[SickLeaveHours]                            --
 26:       ,T02.[Level] + 1 AS [Level]                      --  >> Increases the level for each recursion
 27:     FROM                                               --
 28:        [dbo].[DimEmployee] T01                         --
 29:        INNER JOIN CTE_Employee T02                     --
 30:        ON                                              --
 31:          T01.[ParentEmployeeKey] = T02.[EmployeeKey]   --
 32: )                                                      --
 33: SELECT                                                 -- # If the recursive member query definition returns the same values for both
 34:     [EmployeeKey]                                      --   the parent and child columns, an infinite loop is created. To avoid an
 35:    ,[FirstName]                                        --   infinite loop you can limit the number of recursions. By default SQL
 36:    ,[LastName]                                         --   Server limits the recursions to 100. The maximum number is limited to
 37:    ,[Title]                                            --   32767 recursions.
 38:    ,[ParentEmployeeKey]                                -- # The number of recursions can be limited with the option MAXRECURSION
 39:    ,[Level]                                            -- # If the number of recursion exceeds the specified value for MAXRECURSION
 40:    ,[VacationHours]                                    --   SQL Server will throw an exception
 41:    ,[SickLeaveHours]                                   -- # The option cannot be used within a CTE
 42:                                                        -- # More information on recursive CTEs you can find in the Online Documentation
 43:                                                        --
 44:    ,NTILE(3)                                           -- >> Classifies the vacation hours by Level (3 levels)
 45:        OVER (PARTITION BY [Level]                      --    - PARTITION clause
 46:                  ORDER BY [VacationHours]              --    - ORDER BY clause
 47:             ) AS [VacationHours_NTILE]                 --
 48:    ,DENSE_RANK()                                       -- >> Orders the vacation hours by Level
 49:        OVER (PARTITION BY [Level]                      --    - PARTITION clause
 50:                  ORDER BY [VacationHours]              --    - ORDER BY clause
 51:             ) AS [VacationHours_DENSE_RANK]            --
 52:    ,NTILE(3)                                           -- >> Classifies the sick leave hours by Level (3 levels)
 53:        OVER (PARTITION BY [Level]                      --    - PARTITION clause
 54:                  ORDER BY [SickLeaveHours]             --    - ORDER BY clause
 55:             ) AS [SickLeaveHours_NTILE]                --
 56:    ,DENSE_RANK()                                       -- >> Orders the sick leave hours by Level
 57:        OVER (PARTITION BY [Level]                      --    - PARTITION clause
 58:                  ORDER BY [SickLeaveHours]             --    - ORDER BY clause
 59:             ) AS [SickLeaveHours_DENSE_RANK]           --
 60: FROM                                                   --
 61:    CTE_Employee                                        --
 62: -- OPTION (MAXRECURSION 5)                             -- >> Limits the maximum number of recursions to 5 recursions
 63: ORDER BY                                               --
 64:    [Level] ASC;
 

This variant has several properties:

  • The SQL statement stays compact and readable.
  • The inline comments do not interrupt the statement and hinder neither readability nor comprehension.
  • If a comment refers directly to the code on the same line, that link can be marked with a leading character sequence (e.g. >>).
  • Longer comments should be structured as bullet points rather than running text; the hash character (#) works well as a bullet marker.
  • A comment should not get too long, so the reader does not have to navigate horizontally too much. Wrap longer comments and indent them left-aligned to the previous line — vertical navigation (mouse wheel, Page Up and Page Down keys) is far easier than horizontal.

Example 3: a template for your own inline documentation

In the final example, the key properties of the parallel inline documentation from Example 2 are themselves added as comments — a template you can copy as a starting point for your own statements.

  1: -- --------------------------------------------------------------------------------
  2: -- Section Header
  3: -- --------------------------------------------------------------------------------
  4: -- # Use a section header to describe the overall intention of the following
  5: --   SQL statement.
  6: -- # Use bullet points to structure the inline documentation.
  7: --   > If necessary, you can use bullet points for sub-items, too
  8: --   > ...
  9: -- # In case of complex transformations add links to online document
 10: --   https://onedrive.live.com/?id=root&cid=ABCDEF0123456789
 11: -- --------------------------------------------------------------------------------
 12: SELECT                                       -- --------------------------------------------------------------------------------
 13:     [EmployeeKey]                            -- # Inline documentation that occupies complete lines and that interrupts more or
 14:    ,[FirstName]                              --   less the readability of a SQL statement can affect the comprehensibility of
 15:    ,[LastName]                               --   the statement.
 16:    ,[Title]                                  -- # An essential criterion for the understanding of an SQL statement is not only a
 17:    ,[ParentEmployeeKey]                      --   clear structure and formatting of the statement, but also whether the
 18:    ,[VacationHours]                          --   statement is compact enough to get the major task on a short glance at the
 19:    ,[SickLeaveHours]                         --   statement.
 20:                                              -- # Use bullet points, too, for the parallel inline documentation
 21:                                              -- # It is easier to navigate vertically through a document than horizontally.
 22:                                              --   Keeping that in mind, limit the maximum length of inline documentation
 23:                                              --   to e.g. 80 characters as in this example.
 24:                                              -- # It may help to insert a separating line with 80 characters as an
 25:                                              --   orientation for the maximum length
 26:                                              -- 1-----------------------------------------------------------------------------80
 27:    ,NTILE(3)                                 -- >> If the inline documentation refers exactly to the line of code on the left
 28:        OVER (PARTITION BY [Level]            --    side, you should mark the documentation with for example the characters '>>'
 29:                  ORDER BY [VacationHours]    --   
 30:             ) AS [VacationHours_NTILE]       --
 31:                                              -- # If the inline documentation needs more lines than the SQL statement, just add
 32:                                              --   these lines and leave the left part of the documentation blank. Blank lines
 33:                                              --   do not affect the readability that much as inline documentation, that occupies
 34:                                              --   complete lines.
 35:    ,DENSE_RANK()                             -- >> This documentation would refer to the command DENSE_RANK()
 36:        OVER (PARTITION BY [Level]            -- >> This documentation would explain the PARTITION clause
 37:                  ORDER BY [VacationHours]    -- >> ...and this documentation the ORDER BY statement
 38:             ) AS [VacationHours_DENSE_RANK]  --
 39:    ,NTILE(3)                                 --
 40:        OVER (PARTITION BY [Level]            --
 41:                  ORDER BY [SickLeaveHours]   --
 42:             ) AS [SickLeaveHours_NTILE]      --
 43:    ,DENSE_RANK()                             --
 44:        OVER (PARTITION BY [Level]            --
 45:                  ORDER BY [SickLeaveHours]   --
 46:             ) AS [SickLeaveHours_DENSE_RANK] --
 47: FROM                                         --
 48:    [dbo].[DimEmployee];                      --
 

Does this work in Postgres too?

Yes — and almost unchanged. The two comment syntaxes are identical to T-SQL in PostgreSQL: -- starts a single-line comment, /* … */ encloses a block. The parallel inline documentation is purely textual and therefore completely engine-independent — the right-aligned comment block from Example 2 transfers 1:1 to a Postgres statement.

One detail that many expect to differ actually behaves the same in both engines: block comments may be nested (as the SQL standard prescribes). Both SQL Server and PostgreSQL treat each inner /* as its own comment that needs its own */ — handy for commenting out larger blocks of code that already contain block comments. So the parallel inline documentation can be used in both worlds without hesitation. The comment shortcuts, by the way, differ by editor, not by engine — more on that shortly.

Commenting is understanding — especially for AI-generated SQL

Inline comments are not just documentation for later, they are a tool for understanding in the moment. Whoever writes a comment like „>> limits the recursion to level 2″ must have read the statement completely and built a mental model of the relationships between the tables. The act of commenting forces understanding — much like manual formatting.

In the age of Copilot and Cursor this is doubly relevant. An AI assistant delivers a syntactically correct statement in seconds — but without the business rationale for why it looks exactly the way it does. The risk is not wrong code, but technically correct SQL that still fails to answer the business question. A parallel inline documentation forces you to follow generated SQL line by line and to record the business intent — before it goes to production. The comment block thus becomes the review log of the generated code.

Modern editors: multi-cursor and comment shortcuts

The block selection from Example 2 has a close relative in modern editors: the multi-cursor. Instead of a rectangle spanned across several lines, you place several independent cursors (in VS Code and Azure Data Studio via Alt+Click or Ctrl+Alt+Down) and type the opening -- sequence at all positions at once. For rectangular regions the classic block selection remains the first choice — in SSMS and Visual Studio via Shift+Alt+Arrow, in VS Code via Shift+Alt-drag with the mouse.

The comment shortcuts depend on the editor, not on the database:

  • SSMS: Ctrl+K, Ctrl+C (comment) / Ctrl+K, Ctrl+U (uncomment)
  • VS Code / Azure Data Studio: Ctrl+/ (toggle)
  • DataGrip: Ctrl+/ (single-line) / Ctrl+Shift+/ (block)
  • DBeaver: Ctrl+/ (toggle)

The parallel inline documentation itself is independent of all this — it is a convention, not a feature. It works in any editor that supports some form of multi-line editing.

FAQ

Block or single-line comments — which should I use?

For the parallel inline documentation, the single-line form (--): every line is recognizable as a comment, even without syntax highlighting, and the sequence doubles as a structuring element. Block comments (/* … */) suit longer, connected explanations at the start of a statement — and they nest in SQL Server just as in Postgres.

How do I comment many lines at once?

Use block selection or the multi-cursor to place the -- sequence on all lines at once — or use the editor shortcut: Ctrl+K, Ctrl+C in SSMS, Ctrl+/ in VS Code, Azure Data Studio, DataGrip and DBeaver.

Does the parallel inline documentation work in Postgres too?

Yes, unchanged. The comment syntax is identical — both engines even allow nested block comments — and the pattern is purely textual.

Should I even comment AI-generated SQL?

Especially then. Generated code is fast but not understood. A parallel inline documentation forces you to follow every line — the best protection against technically correct SQL that does not answer the actual question.

How long may an inline comment be?

Short enough that no horizontal navigation is required — about 80 characters per line is a good guideline. Wrap longer comments and indent them left-aligned to the previous line; scrolling vertically is far easier than horizontally.