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 comment block to the right of the statement that keeps the SQL code vertically compact.
- A template you can copy as a starting point for your own statements.
- Why the same pattern works unchanged in Postgres.
- 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.
Here is the pattern in short form. The statement sits on the left, the comment block on the right, at the height of the line it explains:
1: SELECT -- --------------------------------------------------
2: [EmployeeKey] -- # Employees with their hierarchy level
3: ,[LastName] --
4: ,[Level] -- >> Level comes from the recursive CTE
5: FROM --
6: CTE_Employee --
7: ORDER BY --
8: [Level] ASC; -- >> CEO first, then downwards
The three examples further down show the same pattern on a real, complex statement.
Why inline documentation?
That technical documentation matters is hardly in dispute, 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 safety net below that. If it is missing too, a statement stops being maintainable the moment the knowledge leaves the developers’ heads. And that knowledge is fleeting: a few weeks later the logic is hard to follow even for the person who wrote the statement. In a multi-developer project it only gets worse.
One distinction belongs here: inline comments document the source code, not the data model. What a table means in business terms or how a column is defined belongs in the database metadata — COMMENT ON in PostgreSQL, extended properties in SQL Server. The two complement each other: metadata describes the structure, inline documentation the logic of one concrete statement.
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 sequence may appear at any position within a line, as long as it is not inside a string literal or a delimited identifier. From there the comment runs to the end of the line:
1: -- Inline comment
If the text spans several lines, the opening sequence must precede every one of them. With many lines that takes a little effort.
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: */
Technically the two forms are equivalent, and which one is better is, as so often, a matter of debate. For the parallel inline documentation the choice here falls on the single-line form despite the extra effort: especially with multi-line text, the leading -- clearly separates each comment from the rest of the statement. On top of that, 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, and a single keystroke applies to all of them at once. That way 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 against 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: -- >> UNION ALL is required between the last anchor member and the
23: -- 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. MAXRECURSION accepts a value
45: between 0 and 32767, where 0 means no limit.
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 goes in the outer statement's OPTION clause, not the CTE */
50: SELECT
51: [EmployeeKey]
52: ,[FirstName]
53: ,[LastName]
54: ,[Title]
55: ,[ParentEmployeeKey]
56: ,[Level]
57: ,[VacationHours]
58: ,[SickLeaveHours]
59: -- >> Splits the vacation hours into up to three NTILE groups per Level
60: ,NTILE(3)
61: OVER (PARTITION BY [Level]
62: ORDER BY [VacationHours]
63: ) AS [VacationHours_NTILE]
64: -- >> Assigns a dense rank by vacation hours within each Level
65: ,DENSE_RANK()
66: OVER (PARTITION BY [Level]
67: ORDER BY [VacationHours]
68: ) AS [VacationHours_DENSE_RANK]
69: -- >> Splits the sick leave hours into up to three NTILE groups per Level
70: ,NTILE(3)
71: OVER (PARTITION BY [Level]
72: ORDER BY [SickLeaveHours]
73: ) AS [SickLeaveHours_NTILE]
74: -- >> Assigns a dense rank by sick leave hours within each 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 -- >> UNION ALL is required between the last anchor member and the
18: SELECT -- 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. MAXRECURSION accepts a value
37: ,[Title] -- between 0 and 32767, where 0 means no limit.
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 goes in the outer statement's OPTION clause, not the CTE
42: -- # More information on recursive CTEs you can find in the Online Documentation
43: --
44: ,NTILE(3) -- >> Splits the vacation hours into up to three NTILE groups per Level
45: OVER (PARTITION BY [Level] -- - PARTITION clause
46: ORDER BY [VacationHours] -- - ORDER BY clause
47: ) AS [VacationHours_NTILE] --
48: ,DENSE_RANK() -- >> Assigns a dense rank by vacation hours within each Level
49: OVER (PARTITION BY [Level] -- - PARTITION clause
50: ORDER BY [VacationHours] -- - ORDER BY clause
51: ) AS [VacationHours_DENSE_RANK] --
52: ,NTILE(3) -- >> Splits the sick leave hours into up to three NTILE groups per Level
53: OVER (PARTITION BY [Level] -- - PARTITION clause
54: ORDER BY [SickLeaveHours] -- - ORDER BY clause
55: ) AS [SickLeaveHours_NTILE] --
56: ,DENSE_RANK() -- >> Assigns a dense rank by sick leave hours within each 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 vertically 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 UpandPage Downkeys) is far easier than horizontal.
One limitation belongs to the honest picture: the pattern needs width. On narrow displays, in the diff view of a code review or in a terminal editor, the second column can itself force the horizontal scrolling it is meant to avoid. There, a comment block above the section it explains is the better choice. The structure with # and the line reference via >> carry over unchanged.
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://example.com/docs/employee-hierarchy
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 grasp its main purpose at a glance.
19: ,[SickLeaveHours] --
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, the pattern works 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 comment block to the right of the statement from Example 2 transfers 1:1 to a Postgres statement.
That applies to the documentation convention, not to the statement itself. The examples use T-SQL: they rely on the bracket notation [dbo].[DimEmployee], and it limits recursion via OPTION (MAXRECURSION 5). PostgreSQL knows neither. There, WITH RECURSIVE introduces the recursive CTE, and you build the depth limit yourself, for instance via the Level column the query already carries and a condition in the recursive part. What is portable is the comment pattern, not the example 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 */. That is 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:
Ctrl+/(toggle) - Azure Data Studio:
Ctrl+/(toggle). Microsoft retired the editor on 28 February 2026. Its successor is the MSSQL extension for VS Code. - DataGrip:
Ctrl+/(single-line) /Ctrl+Shift+/(block) - DBeaver:
Ctrl+/(toggle)
The shortcuts above refer to the default Windows key bindings on a US layout. On a German keyboard the slash sits on Shift+7, so Ctrl+/ is not directly reachable there and the editors ship their own bindings. On macOS and with customised keymaps the combinations differ as well.
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.
Conclusion
Inline comments should keep a complex statement traceable without taking it apart. That is exactly what the parallel inline documentation achieves: the explanatory text sits in its own block to the right of the statement, the SQL code stays vertically compact, and every explanation sits at the height of the line it refers to. Three conventions carry the pattern: the single-line sequence -- left-aligned across all lines, the marker >> for a direct reference to a line of code, and bullet points with # for longer explanations. The template from Example 3 is the starting point for your own statements, in T-SQL as in Postgres. The real gain shows up while writing: whoever has to explain a line has understood it. That counts for more the more SQL comes out of an assistant.
FAQ
-- and /* */? -- comments out the rest of a line, /* … */ encloses an arbitrary region across several lines. For the parallel inline documentation the single-line form is the one to use: 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.
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, DataGrip and DBeaver.
Yes, unchanged. The comment syntax is identical — both engines even allow nested block comments — and the pattern is purely textual.
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.
When the width is missing. In narrow editor windows, in the diff views of code reviews and in terminal editors, the second column forces exactly the horizontal scrolling it is meant to spare you. The comment block then belongs above the section it explains. For short statements the effort rarely pays off either — the pattern earns its keep on long, deeply nested queries.
No, the two levels complement each other. Inline comments explain the logic of one concrete statement. What a table or column means in business terms belongs in the database metadata — COMMENT ON in PostgreSQL, extended properties in SQL Server. Whoever only comments inline documents their queries, but not their data model.
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, because scrolling vertically is far easier than horizontally.