Stored Procedures
This is where the two dialects diverge the most. MySQL's procedural extensions and T-SQL's are
both "SQL plus a bit of imperative glue," but the glue is shaped differently enough that a stored
procedure rarely ports by find-and-replace alone — the good news is that T-SQL actually drops one
piece of ceremony MySQL developers are used to fighting: there's no DELIMITER dance at
all, because GO (a client-side batch separator, not a real T-SQL keyword) handles the
equivalent job differently.
💡 The Bottom Line
T-SQL local variables are always prefixed with @ and declared with
DECLARE @name TYPE; right inside the procedure body — no separate parameter-mode
ceremony like MySQL's IN/OUT/INOUT tags, because
@-prefixed parameters are just declared before AS and used directly.
There's also no DELIMITER switch to remember: T-SQL parses a whole
CREATE PROCEDURE ... BEGIN ... END statement as one batch, semicolons and all, ending
only at an actual GO batch separator (which most GUI tools insert automatically).
SQL Server also has a real schema concept (dbo.) that MySQL's flat per-database
namespace doesn't.
Basic procedure shape
DELIMITER //
CREATE PROCEDURE get_orders_by_customer(
IN p_customer_id INT
)
BEGIN
SELECT order_id, order_date
FROM orders
WHERE customer_id = p_customer_id;
END //
DELIMITER ;
CREATE PROCEDURE dbo.GetOrdersByCustomer
@CustomerId INT
AS
BEGIN
SELECT OrderId, OrderDate
FROM Orders
WHERE CustomerId = @CustomerId;
END;
📋 Key differences
- MySQL parameters are declared inside the parentheses right after the procedure name, each
tagged
IN,OUT, orINOUT, with no@prefix. SQL Server parameters are declared beforeAS, always prefixed with@, and used the same way inside the body — output parameters are marked with anOUTPUTkeyword after the type instead of anIN/OUTtag before it. - MySQL's
BEGIN ... ENDblock is effectively mandatory for anything beyond a single statement; SQL Server wraps the body inBEGIN ... ENDoptionally — a single statement doesn't strictly need it, though most style guides recommend always including it for clarity. - MySQL procedures live directly inside a database, with no schema layer; SQL Server almost
always prefixes the procedure name with a schema,
dbo.GetOrdersByCustomerbeing the default schema — there's no MySQL equivalent todbo, since MySQL's databases are already the outermost user-facing namespace. - Naming convention differs by culture more than by rule: MySQL shops overwhelmingly use snake_case; T-SQL shops commonly use PascalCase — neither engine enforces either.
⚠️ Gotcha
Muscle memory says "I need to wrap this in DELIMITER // first" — but T-SQL has no
such requirement, and typing MySQL's delimiter syntax into a SQL Server Management Studio query
window just throws a syntax error, since DELIMITER isn't a T-SQL keyword at all. What
T-SQL does have is GO, a batch separator recognized by client tools (not the SQL
Server engine itself) that splits a script into separately-executed batches — you'll typically
see one after a CREATE PROCEDURE statement, mostly because CREATE PROCEDURE
must be the first statement in its batch, not because of any semicolon-counting problem like
MySQL's.
Declaring variables
DELIMITER //
CREATE PROCEDURE count_orders()
BEGIN
DECLARE total INT;
SELECT COUNT(*) INTO total FROM orders;
SELECT total AS total_orders;
END //
DELIMITER ;
CREATE PROCEDURE dbo.CountOrders
AS
BEGIN
DECLARE @Total INT;
SELECT @Total = COUNT(*) FROM Orders;
SELECT @Total AS TotalOrders;
END;
📋 Key differences
- MySQL's
DECLAREd variables have no sigil — they read like plain identifiers, which risks a naming collision with a column of the same name. T-SQL local variables always carry the@prefix, both at declaration and every use, which makes them visually unambiguous from column names at every point in the body — the exact class of bug the gotcha below describes for MySQL simply can't happen in T-SQL. - MySQL assigns a variable from a query with
SELECT COUNT(*) INTO total FROM ...— the target comes afterINTO. T-SQL usesSELECT @Total = COUNT(*) FROM ...instead — the assignment happens in the select list itself, before theFROM, which reads more like a variable assignment statement than a query. - MySQL requires all
DECLAREstatements to appear at the very top of aBEGIN ... ENDblock, before any other statement. T-SQL is more relaxed about where aDECLAREcan appear inside the body — you can declare a variable partway through, right before you first need it.
⚠️ Gotcha
The habit of naming a MySQL local variable the same as a column (protected there only by
convention, e.g. a p_ prefix) doesn't create the same risk in T-SQL — but it's easy to
overcorrect and assume the @ prefix alone prevents every kind of confusion. It
doesn't prevent shadowing between an outer variable and a parameter of the same name, and it
doesn't stop SELECT @Total = COUNT(*) FROM Orders WHERE Total > @Total-style typos
where you meant a column called Total but there's no such column — that just throws
an "invalid column name" error rather than silently comparing the variable to itself the way an
unprefixed MySQL name could.
Calling a procedure
CALL get_orders_by_customer(42);
EXEC dbo.GetOrdersByCustomer @CustomerId = 42;
-- or positionally: EXEC dbo.GetOrdersByCustomer 42;
CALL becomes EXEC/EXECUTE. Where MySQL's arguments are
matched to parameters purely by position, T-SQL supports named arguments at the call site
(@CustomerId = 42), which means you can reorder them, skip optional ones entirely, or
make a multi-parameter call self-documenting — a flexibility MySQL's plain positional
CALL(...) syntax doesn't offer.
🔧 Try It Yourself
Take a simple MySQL procedure you've written — one parameter, one SELECT, one
local variable — and rewrite it for SQL Server: drop the DELIMITER lines entirely,
move the parameter above AS with an @ prefix (no IN tag
needed), and rewrite any SELECT ... INTO var assignment as
SELECT @var = .... Then call it once positionally and once using a named
@ParamName = value argument.
🙋 There Are No Dumb Questions
Q: If T-SQL doesn't need a DELIMITER switch, why do scripts I find online still have
GO scattered everywhere?
A: GO isn't solving the same problem DELIMITER solves in MySQL — it's a
batch separator recognized only by client tools (SSMS, sqlcmd), never sent to the server as SQL
at all. Some statements (like CREATE PROCEDURE) must be the only statement in their
batch, so a GO before and after is common defensive style, but it's not required just
to get the semicolons inside a procedure body to parse correctly — T-SQL never confuses an inner
semicolon for the end of the outer CREATE PROCEDURE statement the way an
un-delimited MySQL script would.
✅ Quick Check
A MySQL developer, out of habit, tries to type DELIMITER // before a
CREATE PROCEDURE statement in SQL Server Management Studio. What happens?
Next up: time for the recap — but first, the gotchas that don't fit neatly into syntax tables: case sensitivity, locking behavior, and a few functions that quietly behave differently.