Anyone who has ever had to debug a badly or completely unformatted SELECT with 30 columns and half a dozen joins knows the feeling: it isn't the SQL that eats up your day — it's hunting down what the statement is actually trying to do. SQL formatting isn't a matter of taste; it's a maintenance tool — and it starts with a naming convention.
→ Part of a series. This is part 1 and covers identifiers, delimiters, commas, and aliases. For the layout of longer statements, continue with Part 2 — Structure and Formatting.
What this article covers:
- Identifiers and delimiters — when to use square brackets, when quotation marks, and what counts as a regular identifier
- Comma, semicolon, spacer — the small separators with a big impact on readability
-
Table aliases and qualified column names — why systematic
T01-style aliases scale better in wide statements than mnemonic abbreviations - Box selection as the killer argument for leading commas
SSMS serves as the example editor. The principles apply equally to DataGrip, VS Code, and DBeaver. The SQL examples reference AdventureWorksDW2017.
Why a Naming Convention at All?
Capital letters are an established device in most written languages to emphasise individual words. In programming, that translates into notations such as CamelCase (each compound word starts with a capital), camelCase (the same, except the first word is lower case), or snake_case (everything lower case with underscores between words). Conventions like these are bundled into a naming convention — a deliberate decision about how identifiers, function names, and data types should be written. In practice, however, you'll often find that developers don't even hold to their own preferred convention.
Sticking to a naming convention improves readability of any text, and of code in particular. tHE SAME Sentence Written Once Again With DEVIATIONS from the generally KNOWN convention that nouns are CAPITALISED and verbs and adjectives are NOT — and the text Turns Unreadable: "adherence to a NAMING convention IMPROVES The readability of text In General And of code In Particular."
The following statement comes from the view vTimeSeries in the database AdventureWorksDW2017, slightly reworked. It does not follow any naming convention:
1: SELECT
2: case [Model]
3: WHEN 'Mountain-100' THEN 'M200' when 'Road-150' THEN 'R250' when 'Road-650' THEN 'R750'
4: WHEN 'Touring-1000' THEN 'T1000' ELSE LEFT(Model, 1) + Right([Model], 3)
5: END + ' ' + [Region] AS [ModelRegion] ,(convert(Integer, CalendarYear) * 100) + CONVERT(int, Month) AS [TimeIndex]
6: ,SUM(Quantity) AS [Quantity]
7: ,sum(Amount) AS Amount, calendaryear,[Month]
8: ,[dbo].[udfbuildiso8601date] ([CALENDARYEAR], [Month], 25)
9: as reportingdate
10: FROM [dbo].[vDMPrep]
11: where [Model] IN ('Mountain-100', 'Mountain-200', 'Road-150', 'Road-250',
12: 'Road-650', 'Road-750', 'Touring-1000')
13: GROUP BY CASE [Model]
14: WHEN 'Mountain-100' THEN 'M200' WHEN 'Road-150' THEN 'R250' WHEN 'Road-650' THEN 'R750'
15: WHEN 'Touring-1000' THEN 'T1000' ELSE Left(Model,1) + Right(Model,3)
16: end+' '+[Region] ,(Convert(Integer, [CalendarYear]) * 100) + Convert(Integer, [Month])
17: ,CalendarYear,[Month],[dbo].[udfBuildISO8601Date] ([CalendarYear], [Month], 25);
In the version below, function names are upper case, every field name uses delimiters, and data types are lower case. Every element is laid out consistently:
1: SELECT
2: CASE [Model]
3: WHEN 'Mountain-100' THEN 'M200'
4: WHEN 'Road-150' THEN 'R250'
5: WHEN 'Road-650' THEN 'R750'
6: WHEN 'Touring-1000' THEN 'T1000'
7: ELSE LEFT([Model], 1) + RIGHT([Model], 3)
8: END + ' ' + [Region] AS [ModelRegion]
9: ,(CONVERT(int, [CalendarYear]) * 100) + CONVERT(int, [Month]) AS [TimeIndex]
10: ,SUM([Quantity]) AS [Quantity]
11: ,SUM([Amount]) AS [Amount]
12: ,[CalendarYear]
13: ,[Month]
14: ,[dbo].[udfBuildISO8601Date]([CalendarYear], [Month], 25) AS [ReportingDate]
15: FROM
16: [dbo].[vDMPrep]
17: WHERE
18: [Model] IN ('Mountain-100', 'Mountain-200', 'Road-150', 'Road-250', 'Road-650', 'Road-750', 'Touring-1000')
19: GROUP BY
20: CASE [Model]
21: WHEN 'Mountain-100' THEN 'M200'
22: WHEN 'Road-150' THEN 'R250'
23: WHEN 'Road-650' THEN 'R750'
24: WHEN 'Touring-1000' THEN 'T1000'
25: ELSE LEFT([Model], 1) + RIGHT([Model], 3)
26: END + ' ' + [Region]
27: ,(CONVERT(int, [CalendarYear]) * 100) + CONVERT(int, [Month])
28: ,[CalendarYear]
29: ,[Month]
30: ,[dbo].[udfBuildISO8601Date]([CalendarYear], [Month], 25);
Whether you like the formatting or not is a personal call. Either way, the second statement is, at a glance, tidier and easier to grasp.
The sections that follow walk through the most important parts of a naming convention. The list is not exhaustive; treat it as a starting point. One thing is worth saying up front: some of it is technical fact (how QUOTED_IDENTIFIER behaves, say, or how Postgres folds case), but most of it is reasoned convention — a choice a team can just as well make differently, as long as it makes that choice consistently. A second part picks up with best practices for the structure of SQL statements.
Regular Identifiers
Every database object has a name — its identifier. Each database vendor defines rules for what a valid identifier looks like. In SQL Server, identifiers are typically capped at 128 characters and must not contain spaces. The exact definition of a regular identifier is in the online documentation:
learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers
For practical purposes, though, that definition is framed too generously. A convention worth having draws tighter boundaries than the database system demands.
Before settling on a spelling style, take a look at how your database actually treats identifiers. SQL Server with a case-insensitive collation does not distinguish between upper and lower case: FactInternetSales and factinternetsales point to the same table. That is the usual situation, since the installation defaults are case-insensitive, but the collation is in fact selectable per instance, per database, and even per column. Case-insensitivity gives you the freedom to choose an emphatic notation such as CamelCase without breaking your statement. Postgres, on the other hand, folds unquoted identifiers to lower case: FactInternetSales becomes factinternetsales internally. You can still write CamelCase without quotes, but it loses its capitalisation along the way. The only way to keep it is "double quotes", which becomes a burden on every statement you write. That is why snake_case is the common convention in the Postgres world.
Behind this blog are ten years of SQL Server practice with CamelCase as a settled convention. Since the switch to Postgres three years ago, snake_case has taken over there. The full depth of case-sensitivity differences between the two engines deserves its own article, and one is on the way. The examples here stay with the CamelCase notation that is typical for SQL Server and comes with AdventureWorksDW2017 anyway.
The underscore _ is a widely used word separator and a legal part of a regular identifier. In the SQL Server world it can nonetheless be avoided for the most part: CamelCase serves the same purpose and keeps identifiers more compact.
Special characters such as @, #, and $ are permitted by the definition, but under this blog's convention they have no place in an identifier. Technically they are allowed even without delimiters inside a regular identifier; they only carry a special meaning in leading position, where @ marks a variable and # a temporary table. That double role is precisely the problem. Using the characters in column or table identifiers creates visual noise and invites confusion with those special meanings:
1: SELECT
2: [EnglishDayNameOfWeek] AS [English@DayNameOfWeek]
3: ,[SpanishDayNameOfWeek] AS [Spanish#DayNameOfWeek]
4: ,[FrenchDayNameOfWeek] AS [French$DayNameOfWeek]
5: FROM
6: [dbo].[DimDate];
By the definition, regular identifiers may even contain Unicode letters from any language. This blog's recommendation nonetheless limits the choice to the letters of the Latin alphabet [a-zA-Z] plus the digits [0-9] where digits are unavoidable. That reduces the rules for a good identifier to just two:
- Letters of the Latin alphabet only
- Digits as a fallback
A disciplined approach to regular identifiers always goes hand in hand with the development of a naming convention for the objects themselves.
Delimiters
Once an identifier contains a space, it stops being a regular identifier. Using non-regular identifiers is bad style. They are nonetheless permitted as long as they are wrapped either in quotation marks or in square brackets.
Double quotation marks are the form the SQL standard provides for delimiting identifiers. Microsoft additionally allows square brackets, diverging from the standard. This blog prefers the brackets as the proprietary form of delimiter.
Delimiters set identifiers apart clearly from the other language elements of a SQL statement and so contribute a great deal to readability. This blog's convention therefore applies delimiters throughout, regardless of whether an identifier is regular or not:
- Schemas
- Tables
- Views
- Column names
- Aliases
- All programmable objects (functions, stored procedures etc.)
This is a deliberate house convention, not a general best practice. For regular identifiers the delimiters are optional according to the Microsoft documentation, and plenty of teams quote only where they have to. Whoever opts for quoting throughout gains the visual separation, at the price of more characters per identifier.
The statement below is identical to the second one in the overview, but written without any delimiters:
1: SELECT
2: CASE Model
3: WHEN 'Mountain-100' THEN 'M200'
4: WHEN 'Road-150' THEN 'R250'
5: WHEN 'Road-650' THEN 'R750'
6: WHEN 'Touring-1000' THEN 'T1000'
7: ELSE LEFT(Model, 1) + RIGHT(Model, 3)
8: END + ' ' + Region AS ModelRegion
9: ,(CONVERT(int, CalendarYear) * 100) + CONVERT(int, Month) AS TimeIndex
10: ,SUM(Quantity) AS Quantity
11: ,SUM(Amount) AS Amount
12: ,CalendarYear
13: ,Month
14: ,dbo.udfBuildISO8601Date(CalendarYear, Month, 25) AS ReportingDate
15: FROM
16: dbo.vDMPrep
17: WHERE
18: Model IN ('Mountain-100', 'Mountain-200', 'Road-150', 'Road-250', 'Road-650', 'Road-750', 'Touring-1000')
19: GROUP BY
20: CASE Model
21: WHEN 'Mountain-100' THEN 'M200'
22: WHEN 'Road-150' THEN 'R250'
23: WHEN 'Road-650' THEN 'R750'
24: WHEN 'Touring-1000' THEN 'T1000'
25: ELSE LEFT(Model, 1) + RIGHT(Model, 3)
26: END + ' ' + Region
27: ,(CONVERT(int, CalendarYear) * 100) + CONVERT(int, Month)
28: ,CalendarYear
29: ,Month
30: ,dbo.udfBuildISO8601Date(CalendarYear, Month, 25);
Plenty of auto-generated scripts from Microsoft tooling use square brackets as delimiters. That is not a documented recommendation, but it is an observable pattern: SQL Server Management Studio (SSMS) emits brackets in the SELECT and DDL statements generated through the context menu on a table. Microsoft is not entirely consistent here, though: when you create a view through the wizard, or look at the SQL panel of the Edit feature, delimiters are dropped wherever they can be. The two screenshots below — both showing auto-generated SQL — make a fine case study in unmaintainable code:
View
Edit Feature
One restriction applies to quotation marks as delimiters: they only work when the SQL Server setting QUOTED_IDENTIFIER is set to ON (the default in SSMS and in the common client libraries):
SET QUOTED_IDENTIFIER ON;
With the setting OFF, SQL Server reads double quotes as string literals, and non-regular identifiers can only be written with square brackets. The brackets work regardless of this setting — a practical advantage of the proprietary form. ON is more than a matter of style, too: several SQL Server features require it, among them indexed views, indexes on computed columns, and filtered indexes.
Details in the online documentation:
learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql
The Spacer
What is meant here is the vertical spacer, that is, the blank line. The natural reading direction of a SQL statement is left to right and top to bottom.
While general-purpose code consists of many short statements, SQL is designed to do a lot of work in a single statement. An SQL statement can easily span a hundred lines or more. That makes writing a good SQL statement a particular challenge. A key criterion for grasping the structure and intent of a statement quickly isn't only a clear layout and consistent formatting, but also compactness. On a typical monitor at a sensible resolution, SSMS shows around forty lines of SQL when only a query window is open and no result pane. In day-to-day use, twenty-five to thirty lines is the realistic maximum.
There are colleagues in our trade who insert a blank line after every line
of code. Excessive blank lines force the reader to lean on the navigation
keys or the mouse wheel just to get from one end of the statement to the
other. Worse, the overall context of the statement becomes much harder
to take in.
Blank lines can be a useful stylistic device to separate logical blocks. Overusing them, however, makes the statement harder to read.
The Semicolon
The SQL standard provides for the semicolon as the terminator of a statement. SQL Server, at least, is forgiving here and doesn't force you to use one. There are only a handful of cases where the semicolon is mandatory.
One example: when using a Common Table Expression, the statement preceding the keyword WITH must end with a semicolon. To sidestep the problem, many developers write the leading WITH as ;WITH. Formally, the semicolon belongs to the preceding statement — ;WITH is a compatibility idiom, not a syntax of its own.
Other engines are stricter, or more precisely their tooling and procedural languages are. A single statement handed to Postgres directly by a driver works without a trailing semicolon. In psql and in multi-statement scripts such as pg_dump output, the semicolon marks the end of a statement, and inside PL/pgSQL function bodies it is part of the syntax as a statement terminator. Oracle follows the same pattern: in PL/SQL blocks (BEGIN … END;) the semicolon is required, while in clients such as SQL*Plus its role depends on the execution context.
Using the semicolon consistently is a sign of care either way, and it helps readability. What it does not do is make a statement portable — SQL dialects differ in entirely different places, from data types through functions to the procedural language. What it does remove is an unnecessary syntactic dependency on the client and the execution context.
The Comma
Field lists in SQL are separated by commas. Some developers put the comma before the field name, others put it after. The pro/con discussion usually centres on how easy it is to add or remove a field. The real reason the comma belongs at the front, though, is readability and the option to format the statement with box selection (called "column editor" or "Spaltenauswahl" in SSMS, "Box Selection" in VS Code and DataGrip). There is more on box selection in the article The Functional Aesthetics of SQL.
Leading Comma
1: SELECT
2: [EnglishDayNameOfWeek]
3: ,[SpanishDayNameOfWeek]
4: FROM
5: [dbo].[DimDate];
If you want to add another field, say FrenchDayNameOfWeek, it's easier to add it at the end — you only insert the text ,[FrenchDayNameOfWeek] after [SpanishDayNameOfWeek]:
1: SELECT
2: [EnglishDayNameOfWeek]
3: ,[SpanishDayNameOfWeek]
4: ,[FrenchDayNameOfWeek]
5: FROM
6: [dbo].[DimDate];
If you want the new field at position one, you need two edits: insert the line [FrenchDayNameOfWeek] before [EnglishDayNameOfWeek], and prepend a comma to [EnglishDayNameOfWeek]:
1: SELECT
2: [FrenchDayNameOfWeek]
3: ,[EnglishDayNameOfWeek]
4: ,[SpanishDayNameOfWeek]
5: FROM
6: [dbo].[DimDate];
Trailing Comma
When the comma is written after each field, the situation reverses: it's easier to add a new field at position one than at the end.
Readability
The comma is a separator. It marks the transition from one field to the next. When it sits at the end of each field, it loses its separating character because field names have different lengths.
In the statement below, it isn't immediately obvious whether the identifier [ProductName] is a column name or has some other role. The reader has to look at the line above to realise that [ProductName] is the alias for [EnglishProductName]:
1: SELECT
2: [EnglishProductName] AS
3: [ProductName],
4: [Size],
5: [Color],
6: [ListPrice],
7: [DealerPrice]
8: FROM
9: [dbo].[DimProduct];
When the comma sits in front of each field, the ambiguity goes away. It is immediately clear that [ProductName] is not a separate field — there is no comma in front of it, so it must belong to the line above:
1: SELECT
2: [EnglishProductName] AS
3: [ProductName]
4: ,[Size]
5: ,[Color]
6: ,[ListPrice]
7: ,[DealerPrice]
8: FROM
9: [dbo].[DimProduct];
The Comma and Box Selection
Adding aliases to every field of the following statement is a little awkward:
1: SELECT
2: [EnglishProductName]
3: ,[Size]
4: ,[Color]
5: ,[ListPrice]
6: ,[DealerPrice]
7: FROM
8: [dbo].[DimProduct];
Every comma has to move at least one position to the right, and the keyword AS and the alias have to be inserted in front of it. That applies to every row of the field list, one at a time, by hand:
1: SELECT
2: [EnglishProductName] AS [AliasEnglishProductName]
3: ,[Size] AS [AliasSize]
4: ,[Color] AS [AliasColor]
5: ,[ListPrice] AS [AliasListPrice]
6: ,[DealerPrice] AS [AliasDealerPrice]
7: FROM
8: [dbo].[DimProduct];
The same end result can be achieved with far less work if the SQL statement is laid out so that box selection works on it:
No commas need to move. With box selection, AS is typed once, the field names are used as the basis for the alias, copied as a block after AS, and the prefix Alias is prepended — again with box selection. The best part: the effort is essentially independent of the number of rows you have to process.
Box selection becomes truly effective only with leading commas. That is the killer argument for putting the comma at the front.
A note on the SSMS bias. This argument is largely about SSMS and its classic box selection. DataGrip and other modern editors offer more powerful refactoring tools — multi-cursor at arbitrary positions, automatic alias generation via refactor commands, semantic search-and-replace across the whole codebase. In that world you save the same effort even without leading commas. The box-selection argument carries less weight there. The readability arguments (see the "Readability" sub-section) hold regardless of the editor.
Function Names
SSMS highlights function names in pink, which makes them easy enough to spot. Since not every editor does syntax highlighting, function names should also be visually distinguishable by consistent upper- or lower-case spelling.
In other words: function names should be either fully upper case or fully lower case.
Microsoft itself, like most database vendors, writes function names in upper case in the online documentation. That documentation spelling is not a binding standard, but it is a convenient orientation — this blog's convention follows the upper-case form.
Table Aliases
A widely used practice is to derive a table alias as a "speaking" abbreviation from the table name. For the table FactInternetSales one might use the alias IS, taking the starting letters of the compound words in the table name (ignoring the Fact prefix). Since IS is a reserved word, that alias must be wrapped in delimiters. The example also shows a weakness of mnemonic abbreviations: they can collide with reserved words and then force additional quoting. For other tables, you might end up with aliases like these:
| Table | Alias |
|---|---|
| DimCustomer | CUST |
| DimProduct | P |
| DimProductCategory | PC |
| DimProductSubcategory | PSC |
A SELECT statement built on those tables might look like this:
1: SELECT
2: CUST.[LastName]
3: ,CUST.[FirstName]
4: ,P.[EnglishProductName]
5: ,PC.[EnglishProductCategoryName]
6: ,PSC.[EnglishProductSubCategoryName]
7: ,[IS].[OrderDate]
8: FROM
9: [dbo].[FactInternetSales] [IS]
10: LEFT JOIN [dbo].[DimCustomer] CUST
11: ON
12: [IS].[CustomerKey] = CUST.[CustomerKey]
13: LEFT JOIN [dbo].[DimProduct] P
14: ON
15: [IS].[ProductKey] = P.[ProductKey]
16: LEFT JOIN [dbo].[DimProductSubcategory] PSC
17: ON
18: P.[ProductSubcategoryKey] = PSC.[ProductSubcategoryKey]
19: LEFT JOIN [dbo].[DimProductCategory] PC
20: ON
21: PC.[ProductCategoryKey] = PSC.[ProductCategoryKey];
The uneven indentation of the field names is a side-effect of the different lengths of the aliases — somewhere between one and four characters. The field list looks "restless", and as soon as more language elements join the party (functions, CASE expressions etc.) it can become hard to read quickly.
Now imagine a SELECT over twenty tables or more. At some point it becomes hard to come up with a meaningful alias for every table. From roughly the fifth alias onward — that is a rule of thumb, not a hard boundary — the derivation from table names rarely improves readability any further, because the aliases are simply too cryptic.
Wouldn't systematic — possibly even indexed — aliases be easier to identify in a complex statement?
A systematic alias scheme could be defined like this:
- Aliases must be a fixed number of characters
- Aliases are indexed (with one or more leading letters)
If, for instance, you use the letter T for Table (or F for Fact table, D for Dimension, etc.) followed by a two-digit 1-based index, you end up with aliases such as T01, T02, D01, F01, ….
Using those aliases, the statement above reads more cleanly:
1: SELECT
2: T02.[LastName]
3: ,T02.[FirstName]
4: ,T03.[EnglishProductName]
5: ,T05.[EnglishProductCategoryName]
6: ,T04.[EnglishProductSubCategoryName]
7: ,T01.[OrderDate]
8: FROM
9: [dbo].[FactInternetSales] T01
10: LEFT JOIN [dbo].[DimCustomer] T02
11: ON
12: T01.[CustomerKey] = T02.[CustomerKey]
13: LEFT JOIN [dbo].[DimProduct] T03
14: ON
15: T01.[ProductKey] = T03.[ProductKey]
16: LEFT JOIN [dbo].[DimProductSubcategory] T04
17: ON
18: T03.[ProductSubcategoryKey] = T04.[ProductSubcategoryKey]
19: LEFT JOIN [dbo].[DimProductCategory] T05
20: ON
21: T05.[ProductCategoryKey] = T04.[ProductCategoryKey];
The field names in the SELECT list line up cleanly.
The real killer argument is the same here, too: aliases of equal length are what makes box selection useful in the first place.
There's another important reason to use aliases at all, and it applies to mnemonic and systematic ones alike: Intellisense (in SSMS) only really works once aliases are in place. When the developer types an alias followed by a dot, a context menu opens listing only the columns of the corresponding table.
When the cursor sits after the dot, the context menu can also be invoked manually with the shortcut Ctrl+Space. It works without a preceding alias as well. In that case, however, SSMS effectively offers the entire T-SQL vocabulary at once.
Aliases make SQL code easier to read and therefore more maintainable. In statements spanning several tables they are practically indispensable. This blog's convention applies them to simple single-table statements as well, for the sake of uniformity and Intellisense comfort.
Qualified Column Names
As soon as a statement references multiple tables, sooner or later the same column name appears in more than one of them and stops being unambiguous. In the example below we want to return name and phone number for both the employee and the reseller of a FactResellerSales fact:
The statement is not executable because the column Phone exists in both dimensions DimEmployee and DimReseller. SQL Server aborts with two errors:
1: Msg 209, Level 16, State 1, Line 4
2: Ambiguous column name 'Phone'.
3: Msg 209, Level 16, State 1, Line 6
4: Ambiguous column name 'Phone'.
Specifying a table alias in front of the column name is mandatory here:
T01.[Phone] AS [Employee_Phone]
or — if no table aliases are used — the table name itself has to precede the column:
[DimEmployee].[Phone] AS [Employee_Phone]
The latter, however, does nothing for readability.
A fully qualified object name in SQL Server consists of up to four parts: server, database, schema, and object. A column reference adds the column as a fifth element:
[Server].[Database].[Schema].[Table].[Column]
An example with fully qualified table names that include the database name:
Any qualification beyond the schema prevents the application from being deployable in a database that doesn't share the name AdventureWorksDW2017. Cases like that do occur in practice: ETL pipelines whose tables were fully qualified down to the database name ([Database].[Schema].[Table].[Column]) could not be deployed to production once they were finished.
Engine note: Postgres. The five-part scheme Server.Database.Schema.Table.Column only applies in SQL Server. Postgres by default does not allow cross-database queries: a cluster does hold several databases, but a connection is bound to exactly one of them, so the Database. element doesn't exist in practice. If you really need to read or write across database boundaries, you'll need the extensions postgres_fdw (Foreign Data Wrapper) or dblink. Both are extra setup, not a default. In practical terms this means that within a Postgres database, Schema.Table.Column (three parts) is the relevant qualification, while in SQL Server it is one option among several. The recommendation stays the same in both worlds: never qualify beyond the schema level.
Conclusion
A naming convention is the entry point to maintainable SQL code: regular identifiers built from letters and, where needed, digits; delimiters applied consistently; function names in upper case. The small separators have the largest effect of all. The leading comma keeps field lists readable and ready for box selection, the semicolon makes statements unambiguous towards the client and the execution context, and sparing blank lines keep the whole picture on a single screen.
Systematic aliases such as T01 play to their strength in wide statements and line the field list up cleanly. Qualified column names belong up to the schema level and no further. Which individual rule you pick matters less than it seems: SQL becomes maintainable through a consistent convention that makes structure visible and changes easy. Part 2 — Structure and Formatting builds on this and covers the layout of longer statements.
FAQ
Leading comma — isn't that unusual?
When reading, the comma loses its separating role if it sits at the end of a line (see the section "Readability"). When writing, the leading comma saves the most effort the moment box selection comes into play. Modern auto-formatters make the comma position configurable. sqlfluff (T-SQL and Postgres) places commas at the end of the line by default and only switches to leading commas through its layout configuration (line_position = leading). pgFormatter (Postgres) likewise defaults to the trailing comma and offers --comma-start for leading ones.
Do you need square brackets in Postgres too?
No. Postgres does not support [brackets]. Its delimiters are double quotes: "EnglishProductName". There is no QUOTED_IDENTIFIER setting either. Identifier quoting in Postgres is always on. Postgres folds identifiers without quotes to lower case (MyColumn becomes mycolumn). Identifiers in quotes keep their spelling and have to be quoted exactly that way on every reference. If your codebase is multi-engine in the long run, "double quotes" are ANSI-SQL-compliant and work in both worlds — the [brackets] are SQL Server-specific.
Systematic T01-style aliases vs. speaking aliases — which is better?
It's a trade-off. Speaking aliases (CUST, P, PC) are mnemonic and work fine as long as the statement only has three or four tables. As soon as five or more tables come into play — typical for ETL pipelines or wide reporting queries — the mnemonic edge fades and the alignment suffers from the varying lengths. Systematic T01-style aliases scale, stay the same length, and make box selection effortless. The price is semantic: T05.[CustomerKey] doesn't tell you the table. Anyone who wants to know jumps to the FROM clause. Systematic aliases optimise for visual structure and mechanical editing, then, while speaking aliases optimise for orientation in the content. As a rule of thumb, this blog uses speaking aliases up to four tables and systematic ones from five onward.
What if my team uses a different convention?
Consistency beats purity. If the team has settled on trailing commas or speaking aliases, applying that convention consistently is more important than picking the "better" variant.
In practice this is one of the hardest points overall. In every larger team, each developer has a very personal sense of what "looks good" and what doesn't. That individual matter of taste is exactly what stands in the way of team-wide uniformity, and experience across many projects bears this out again and again. All the more reason for the convention that the team has agreed on to be applied consistently across every team member. Only then do the SQL statements stay readable and maintainable for everyone involved.
Multi-cursor — does it replace box selection?
Modern editors like VS Code, DataGrip, and DBeaver provide true multi-cursor support (Ctrl+Alt+Down Arrow or Alt+Click) that goes beyond the rectangular box selection: cursors can sit at arbitrary positions rather than only along a rectangular column. SSMS only has classic box selection (Alt+Shift+Arrow). Azure Data Studio, once part of this line-up, was retired by Microsoft on 28 February 2026; the recommended successor is VS Code with the mssql extension. Conceptually, multi-cursor covers the same need and goes beyond it. Box-selection-friendly formatting remains the prerequisite either way. If you work across several editors, format once cleanly rather than once per tool.


![SSMS editor showing the DimProduct SELECT in box-selection-ready form: to the right of each column are the aligned aliases AS [AliasEnglishProductName], AS [AliasSize], AS [AliasColor] and others. The prefix Alias is highlighted as a blue box-selection band spanning all five rows — a single keystroke applies to every row simultaneously.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx63qjwoclb3c8pone8ib.png)

![SSMS editor showing the FactResellerSales SELECT: the field list contains [Phone] AS [Employee_Phone] and [Phone] AS [Reseller_Phone] twice without any table alias prefix. The FROM clause uses the T01/T02/T03 aliases but the SELECT never references them — the statement is ambiguous and not executable.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr25r7oln125j57oh3m2.png)
![SSMS editor showing the same FactResellerSales SELECT as before, this time with fully qualified table names [AdventureWorksDW2017].[dbo].[FactResellerSales] T01, [AdventureWorksDW2017].[dbo].[DimEmployee] T02, and [AdventureWorksDW2017].[dbo].[DimReseller] T03 in the FROM/JOIN clause — the database name is baked into the table reference and binds the statement to this specific database.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favzm2ya138b2wouf2n7s.png)
Top comments (0)