GO Is Not T-SQL: Batches, Variable Scope and GO With a Count

A script runs in SSMS but fails when an application sends the same text to SQL Server. GO is not T-SQL; SSMS and sqlcmd read it and send separate batches, while the Database Engine never receives that word as a command. That one distinction explains vanished variables and surprising deployment flow.

A checkout belt with three groups of groceries split by red divider bars

GO Lives in the Client, Not in T-SQL

SSMS and sqlcmd use GO to mark the end of a batch. They send the accumulated T-SQL to SQL Server, then start a new batch. SQL Server can receive many statements in one batch, but GO is not T-SQL, so the server does not parse it as a language element. A driver call that sends SELECT 1; GO SELECT 2; as one command text will get a syntax error near GO.

I ask which program executed a failing script before reading its SQL. A deployment file that works in SSMS but not through an application runner can be a client parsing issue, not a server version issue. Does the runner know SQLCMD batches, or does it send the whole file verbatim?

Watch a T-SQL Variable Vanish at GO

Local T-SQL variables live for one batch. The first SELECT below succeeds. After GO, a new batch starts and @message no longer exists. A temporary table can remain in the session across batches, which is why the two rules are easy to confuse. The block below fails on purpose at its second SELECT.

DECLARE @message varchar(20) = 'ready';
SELECT @message AS first_batch_value;
GO
SELECT @message AS second_batch_value;

The second SELECT raises an undeclared-variable error. Declare the variable again in the second batch, or store a value in a temp table if it must cross a batch boundary. You can also reorganize the script so dependent statements stay together. Do not remove GO blindly; CREATE PROCEDURE and some other statements have batch-position requirements.

Understand a Compile Error Between Batches

A compile error stops its batch, but a client can continue sending later batches unless configured to stop on errors. That is dangerous in a deployment: a later data migration can run even though an earlier schema change failed. SQLCMD has :on error exit; other runners need explicit exception handling and a stop policy. The first batch below fails on purpose.

SELECT DefinitelyNotAColumn FROM sys.objects;
GO
SELECT N'Next batch still reached' AS warning_text;
GO

I ran it with sqlcmd and no -b switch. The first batch failed with Msg 207, and the second batch still returned its row, because GO created separate submissions. I add a postcondition after every schema phase and configure the release runner to stop on the first failure. A printed error in Messages is not the same as a rolled-back release.

GO With a Count Is Not a T-SQL Loop

GO 3 asks SSMS or sqlcmd to execute the preceding batch three times. It is a client repetition feature, not a loop sent to the server. Variables are declared anew on each execution of that batch. Use it for a small demonstration or controlled test, not as a hidden retry mechanism for a production change.

DECLARE @run_at datetime2(3) = SYSDATETIME();
SELECT @run_at AS executed_at;
GO 3

A batch that inserts rows will insert them three times. A batch that creates a table will succeed once and fail on the next attempts unless guarded. Review the count as part of the change, and avoid keeping a GO count in a script that was copied from a load test.

What survives a GO boundary: a diagram about the GO is not T-SQL

Respect T-SQL Module Rules Around GO

CREATE OR ALTER PROCEDURE needs the correct batch boundary. A deployment script can set options in one batch, then define the module in the next. Removing GO to satisfy an application runner can make the CREATE statement invalid or capture the wrong SET options. Split the script into explicit batches that preserve those boundaries.

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.DemoBatch
AS SELECT 1 AS value;
GO
EXEC dbo.DemoBatch;

The driver should send the SET batches and CREATE batch separately on the intended connection. Some connection options persist at session level; some variables do not. Test the exact runner and compare the saved module flags after deployment.

Split Safely in Application Code

The most reliable application design keeps an ordered list of known batch texts or uses a SQLCMD-aware execution library. It sends each batch separately and stops on error. Do not split a large file with a naive string Split("GO"). GO can appear in a comment, string literal, identifier, or longer word, and GO 3 carries a repeat count. A real parser must recognize a GO command on its own line under the client's syntax.

I keep batch number, source file, target database, start time, outcome, and error in the deployment log. If batch four fails, the application reports that boundary and does not send batch five. Transactions should be scoped around operations that support them; GO does not itself commit or roll back. A batch split is a client protocol decision, while transaction atomicity is a database design decision.

Rehearse the Failure Path Before Release

Run the script in the same tool that production will use, against a restored test database. Deliberately introduce a compile error before a later harmless marker and confirm the marker does not run. Then test a runtime error and a connection interruption. Check the final schema rather than relying on the runner's exit code alone.

A good release package says whether it requires SSMS SQLCMD mode, the sqlcmd utility, or an application batch runner. The same text can behave differently across those clients. Make the boundary explicit, and GO becomes a useful separator rather than a mysterious SQL keyword.

Do Not Confuse GO With a T-SQL Transaction

A transaction can span batches on the same connection, but GO by itself neither commits nor rolls back. If a deployment opens a transaction, crosses GO, and then fails, the connection can still hold locks until it explicitly handles the transaction or disconnects. Keep transaction ownership visible and avoid leaving an open transaction across a long manual review. I inspect @@TRANCOUNT and XACT_STATE() in error paths before a retry.

For automated releases, I prefer bounded batches with their own checks and recovery steps. The runner records which batches committed and which did not. That evidence is more useful than a single final “script failed” line after several independent operations have already changed the database.

Related reading on this blog: Interview Question of the Week #057: What is GO Statement in SQL SERVER? and SQL SERVER Management Studio and SQLCMD Mode.

Before a script leaves SSMS: a checklist on the GO is not T-SQL

GO is not T-SQL, it is a client batch separator that scripts must preserve.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.

Batch, SQL Scripts, SQL Server, sqlcmd
Previous Post
SQL SERVER – Adding Column Defaulting to Current Datetime in Table
Next Post
SQL SERVER – Interesting Observation of CONCAT_NULL_YIELDS_NULL and CONCAT

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.