Everyday DDL and Idempotent Scripts: IDENTITY and SEQUENCE, Computed Columns, CREATE OR ALTER, DROP IF EXISTS and Synonyms
A rerunnable deploy script ties every step to an OBJECT_ID/COL_LENGTH check, uses CREATE OR ALTER for views and procedures and DROP ... IF EXISTS for the rest, and wraps DDL in a transaction with XACT_ABORT ON. With real output: where identity and sequence gaps come from, IDENTITY_INSERT and CHECKIDENT, a shared order number sequence, PERSISTED computed columns, a view broken by sp_rename, and synonyms; the last post of the series.
Version note: The SQL queries in this post were run on SQL Server 2025 (17.0, Cumulative Update 10) with the series' Bookstore database; the PostgreSQL comparisons were run on PostgreSQL 18.7.
Guess First: Running the Deploy Script a Second Time
A two-line deploy script that adds a loyalty_tier column to customers for the loyalty programme and makes the phone number unique:
ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
Msg 1505, Level 16, State 1, Line 2
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.customers' and the index name 'UX_customers_phone'. The duplicate key value is (<NULL>).
The statement has been terminated.
The index was not created: 21 customers have a NULL phone, and in a unique index SQL Server treats NULLs as equal to each other. A teammate sees the error and runs the script again. What happens?
ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
Msg 2705, Level 16, State 4, Line 1
Column names in each table must be unique. Column name 'loyalty_tier' in table 'dbo.customers' is specified more than once.
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes,
INDEXPROPERTY(OBJECT_ID('dbo.customers'), 'UX_customers_phone', 'IndexId') AS ux_index_id;
loyalty_tier_bytes ux_index_id
------------------ -----------
10 NULL
The first run stopped halfway: the column was added, the index was not. The second run now fails on the first line because the column already exists. The database is neither in its old nor in its new state, and the script can neither fix nor undo that. This is the last post of the series, and it deals with both sides of this problem: everyday DDL tools such as IDENTITY, SEQUENCE and computed columns, and deploy scripts that do not fail when they run twice and do not stop halfway. The examples run on the database of Bookstore, the online bookstore used as the shared example across this site's series.
IDENTITY: Seed, Increment and Gaps
IDENTITY(seed, increment) has been in the primary key of every Bookstore table since post 4. In post 22 we saw that IDENT_CURRENT did not go back after a rolled-back insert; let's see where gaps come from all at once:
SELECT IDENT_SEED('dbo.orders') AS seed, IDENT_INCR('dbo.orders') AS increment, IDENT_CURRENT('dbo.orders') AS current_value;
BEGIN TRAN;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Pending');
ROLLBACK;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Unknown');
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Pending');
SELECT TOP (2) order_id, status FROM dbo.orders ORDER BY order_id DESC;
seed increment current_value
---- --------- -------------
1 1 379
(1 rows affected)
(1 rows affected)
Msg 547, Level 16, State 1, Line 5
The INSERT statement conflicted with the CHECK constraint "CK_orders_status". The conflict occurred in database "sqlseries_ext_25", table "dbo.orders", column 'status'.
The statement has been terminated.
(1 rows affected)
order_id status
-------- -------
382 Pending
379 Pending
The order after 379 became 382. 380 was used up by the rolled-back transaction and 381 by the insert that failed on CK_orders_status. An identity value is allocated at insert time, and neither a rollback nor an error gives it back; that is how concurrent sessions get numbers without waiting for each other. On top of that come values lost when the server restarts: for performance, SQL Server allocates identity values in advance and keeps them in memory, and on an unexpected shutdown those values are skipped. The database-level IDENTITY_CACHE setting (default 1 in sys.database_scoped_configurations) turns this off, at the cost of insert performance; even with it off, rollback gaps remain. The bottom line: identity is an identifier, not a sequence number. If there is a business rule that "order numbers must be consecutive", do not put it on identity.
The state of the identity is read, and changed when needed, with DBCC CHECKIDENT:
DBCC CHECKIDENT ('dbo.orders', NORESEED);
Checking identity information: current identity value '382', current column value '382'.
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
To fill a gap by hand, or to move data from another system together with its ids, you need to write a value into the identity column. By default that is not allowed:
INSERT INTO dbo.orders (order_id, customer_id, ordered_at, status) VALUES (381, 1, '2026-06-30 10:00', 'Pending');
Msg 544, Level 16, State 1, Line 1
Cannot insert explicit value for identity column in table 'orders' when IDENTITY_INSERT is set to OFF.
SET IDENTITY_INSERT dbo.orders ON;
INSERT INTO dbo.orders (order_id, customer_id, ordered_at, status) VALUES (381, 1, '2026-06-30 10:05', 'Pending');
SET IDENTITY_INSERT dbo.orders OFF;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:10', 'Pending');
SELECT TOP (3) order_id, ordered_at FROM dbo.orders ORDER BY order_id DESC;
order_id ordered_at
-------- -------------------
383 2026-06-30 10:10:00
382 2026-06-30 10:00:00
381 2026-06-30 10:05:00
With IDENTITY_INSERT on, the column list is mandatory, and it can be on for only one table per session at a time. If the value you write is larger than the current value, identity continues from there; if it is smaller (like 381), identity stays where it is and the next insert gets 383. To move the counter back after cleaning up test data, use RESEED:
DELETE FROM dbo.orders WHERE order_id > 379;
DBCC CHECKIDENT ('dbo.orders', RESEED, 379);
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:15', 'Pending');
SELECT TOP (1) order_id FROM dbo.orders ORDER BY order_id DESC;
(3 rows affected)
Checking identity information: current identity value '383'.
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
(1 rows affected)
order_id
--------
380
If you reseed below the current largest id, later inserts collide with existing ids and you get a primary key error. There are very few cases where you need RESEED in production; most of the time it is for cleanup in a test environment.
SEQUENCE: Numbers Independent of a Table
A SEQUENCE is a counter object that is not tied to a table; it hands out values with NEXT VALUE FOR. In Bookstore we use it for the order number shown to customers: separate from the id, starting at 100001, and shared by individual orders and institutional orders (the Institutional module). The script is written to be rerunnable from the start:
IF OBJECT_ID('dbo.order_number_seq', 'SO') IS NULL
CREATE SEQUENCE dbo.order_number_seq AS INT START WITH 100001 INCREMENT BY 1 CACHE 50;
IF COL_LENGTH('dbo.orders', 'order_number') IS NULL
ALTER TABLE dbo.orders ADD order_number INT NULL
CONSTRAINT DF_orders_order_number DEFAULT (NEXT VALUE FOR dbo.order_number_seq);
DROP TABLE IF EXISTS dbo.institution_orders;
CREATE TABLE dbo.institution_orders (
institution_order_id INT IDENTITY(1,1) CONSTRAINT PK_institution_orders PRIMARY KEY,
institution_name NVARCHAR(100) NOT NULL,
order_number INT NOT NULL CONSTRAINT DF_institution_orders_order_number DEFAULT (NEXT VALUE FOR dbo.order_number_seq)
);
GO
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 11:00', 'Pending'), (3, '2026-06-30 11:05', 'Pending');
INSERT INTO dbo.institution_orders (institution_name) VALUES (N'Hilltop School Library');
SELECT 'orders' AS source, order_id AS id, order_number FROM dbo.orders WHERE order_number IS NOT NULL
UNION ALL
SELECT 'institution_orders', institution_order_id, order_number FROM dbo.institution_orders
ORDER BY order_number;
source id order_number
------------------ --- ------------
orders 381 100001
orders 382 100002
institution_orders 1 100003
The two tables took numbers from the same counter; you cannot do that with identity, because identity belongs to a column of a single table. We added the column as nullable, and the DEFAULT applied only to new rows; the existing 380 orders stayed NULL. If you also want to number old orders, do it as a separate, deliberate step (with NEXT VALUE FOR ... OVER (ORDER BY ordered_at) you can also fix the order). SEQUENCE has two more subtleties:
SELECT NEXT VALUE FOR dbo.order_number_seq AS peek_consumes;
DECLARE @first SQL_VARIANT, @last SQL_VARIANT;
EXEC sys.sp_sequence_get_range @sequence_name = N'dbo.order_number_seq', @range_size = 100,
@range_first_value = @first OUTPUT, @range_last_value = @last OUTPUT;
SELECT @first AS range_first, @last AS range_last;
SELECT current_value, cache_size, is_cached FROM sys.sequences WHERE name = 'order_number_seq';
peek_consumes
-------------
100004
(1 rows affected)
range_first range_last
----------- ----------
100005 100104
(1 rows affected)
current_value cache_size is_cached
------------- ---------- ---------
100104 50 1
Calling NEXT VALUE FOR to ask "what will the next number be?" uses that number up; 100004 will never be given to an order. If you only want to look, read sys.sequences.current_value. CACHE 50 allocates values in memory in blocks of 50 and does not write to disk for every value; the price is that the rest of the block is skipped on an unexpected shutdown. When the application needs many numbers at once, sys.sp_sequence_get_range reserves a range in one call: a request for 100 numbers returned the range 100005–100104 and current_value jumped to the end of the range; using those numbers is now the application's job. So a sequence has gaps too, just like identity. Numbers that must legally be gapless, such as invoice numbers, need a separate design; the Solution Patterns series post "Numbering: Invoice and Order Numbers, Gapless Sequences and Yearly Resets" covers that and yearly resets.
Computed Columns and PERSISTED
A computed column is calculated from an expression. By default it is not stored and is calculated on every read; with PERSISTED the calculated value is written to the row and kept up to date as the source columns change. The amount of an order line:
IF COL_LENGTH('dbo.order_items', 'line_total') IS NULL
ALTER TABLE dbo.order_items ADD line_total AS (quantity * unit_price) PERSISTED;
GO
SELECT TOP (3) order_item_id, quantity, unit_price, line_total FROM dbo.order_items WHERE quantity > 1 ORDER BY order_item_id;
SELECT c.name, c.is_computed, cc.is_persisted, TYPE_NAME(c.system_type_id) AS type_name, c.precision, c.scale
FROM sys.columns AS c
LEFT JOIN sys.computed_columns AS cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
WHERE c.object_id = OBJECT_ID('dbo.order_items') AND c.name = 'line_total';
order_item_id quantity unit_price line_total
------------- -------- ---------- ----------
1 2 290.90 581.80
5 3 440.00 1320.00
10 2 460.00 920.00
(3 rows affected)
name is_computed is_persisted type_name precision scale
---------- ----------- ------------ --------- --------- -----
line_total 1 1 decimal 21 2
The type comes from the expression: INT * DECIMAL(10,2) → DECIMAL(21,2) (the precision rules from post 22). You cannot write to a computed column directly; UPDATE ... SET line_total = 0 was rejected with Msg 271. To be PERSISTED, the expression must be deterministic, that is, it must always return the same result for the same input:
ALTER TABLE dbo.orders ADD age_days AS DATEDIFF(day, ordered_at, SYSUTCDATETIME()) PERSISTED;
Msg 4936, Level 16, State 1, Line 1
Computed column 'age_days' in table 'orders' cannot be persisted because the column is non-deterministic.
An order's age changes every day; storing it makes no sense. The version without PERSISTED can be added and is calculated on every read. Which one when: make values that are read often and are expensive to calculate PERSISTED; computed columns that you index must be deterministic, but they do not have to be PERSISTED (the JSON_VALUE column in post 23 was not persisted). In post 20 we saw that AT TIME ZONE cannot go into a persisted computed column for the same reason.
Putting DDL in a Transaction and Making It Rerunnable
Back to the script that stopped halfway in the opening. In SQL Server most DDL statements, such as ALTER TABLE and CREATE INDEX, run inside a transaction and can be rolled back. First let's remove the leftover column and put the same script in a transaction:
ALTER TABLE dbo.customers DROP COLUMN IF EXISTS loyalty_tier;
GO
SET XACT_ABORT ON;
BEGIN TRAN;
ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
COMMIT;
GO
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes, @@TRANCOUNT AS open_tran;
Msg 1505, Level 16, State 1, Line 4
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.customers' and the index name 'UX_customers_phone'. The duplicate key value is (<NULL>).
loyalty_tier_bytes open_tran
------------------ ---------
NULL 0
Same error, but this time the column was rolled back too: the database is in its state from before the script. Now let's make the real fix. For customers without a phone we use a filtered index (an index that contains only the rows matching its WHERE condition), and we make every step depend on an "already done?" check. GO 2 runs the script twice in a row:
SET XACT_ABORT ON;
BEGIN TRAN;
IF COL_LENGTH('dbo.customers', 'loyalty_tier') IS NULL
ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.customers') AND name = 'UX_customers_phone')
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone) WHERE phone IS NOT NULL;
COMMIT;
PRINT 'migration applied';
GO 2
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes,
INDEXPROPERTY(OBJECT_ID('dbo.customers'), 'UX_customers_phone', 'IndexId') AS ux_index_id;
migration applied
migration applied
loyalty_tier_bytes ux_index_id
------------------ -----------
10 3
Both runs finished without errors; in the second one neither IF found any work to do. The tools for the checks are the metadata functions from post 22: OBJECT_ID(name, type) for tables, views, procedures and sequences ('U' table, 'V' view, 'P' procedure, 'SO' sequence), COL_LENGTH for columns, sys.indexes for indexes, and sys.objects or OBJECT_ID for constraints.
There is one more trap in such scripts. Adding a column and using it in the same batch:
IF COL_LENGTH('dbo.orders', 'order_code') IS NULL
ALTER TABLE dbo.orders ADD order_code AS CONCAT('BK-', order_number);
SELECT TOP (2) order_id, order_number, order_code FROM dbo.orders WHERE order_number IS NOT NULL ORDER BY order_id;
Msg 207, Level 16, State 1, Line 3
Invalid column name 'order_code'.
The batch is compiled as a whole; at compile time order_code does not exist, so the ALTER TABLE did not run either. This is the DDL counterpart of the batch rule from post 24: put the statement that uses the new column in the next batch (after GO).
IF COL_LENGTH('dbo.orders', 'order_code') IS NULL
ALTER TABLE dbo.orders ADD order_code AS CONCAT('BK-', order_number);
GO
SELECT TOP (2) order_id, order_number, order_code FROM dbo.orders WHERE order_number IS NOT NULL ORDER BY order_id;
order_id order_number order_code
-------- ------------ ----------
381 100001 BK-100001
382 100002 BK-100002
In a script split into batches, a transaction can stay open across batches, but if XACT_ABORT rolls the transaction back in one batch, the following batches keep running outside a transaction. That is why you should run a multi-batch deploy script with sqlcmd and :on error exit (post 24) and write every step to be rerunnable; together the two make the "if it stops halfway, fix it and run again" flow safe.
CREATE OR ALTER and DROP ... IF EXISTS
Programmable objects (views, stored procedures, functions, triggers) need no check; CREATE OR ALTER has been available since SQL Server 2016 SP1. If the object does not exist it creates it; if it exists it changes its definition and keeps its permissions:
CREATE OR ALTER VIEW dbo.v_book_prices AS
SELECT book_id, title, list_price FROM dbo.books WHERE is_active = 1;
GO
CREATE OR ALTER VIEW dbo.v_book_prices AS
SELECT book_id, title, list_price, listed_on FROM dbo.books WHERE is_active = 1;
GO
SELECT TOP (2) * FROM dbo.v_book_prices ORDER BY book_id;
book_id title list_price listed_on
------- ------------------- ---------- ----------
1 Pride and Prejudice 230.90 2024-06-01
2 Emma 680.50 2024-06-01
The old pattern, "DROP if it exists, then CREATE", also drops the permissions granted on the object, and between the two steps the object briefly does not exist; CREATE OR ALTER does neither. For tables, indexes and other objects the shortcut is DROP ... IF EXISTS:
ALTER TABLE dbo.customers DROP COLUMN IF EXISTS no_such_column;
ALTER TABLE dbo.customers DROP CONSTRAINT IF EXISTS no_such_constraint;
DROP INDEX IF EXISTS no_such_index ON dbo.customers;
DROP SEQUENCE IF EXISTS dbo.no_such_seq;
DROP SYNONYM IF EXISTS dbo.no_such_synonym;
PRINT 'nothing to drop, no error';
nothing to drop, no error
There is no CREATE OR ALTER for tables, and there cannot be: "changing" the definition of an existing table raises the question of what happens to the data. For tables the pattern IF OBJECT_ID(..., 'U') IS NULL CREATE TABLE applies, and for columns IF COL_LENGTH(...) IS NULL ALTER TABLE ... ADD. Use DROP TABLE IF EXISTS carefully in a deploy script: it is rerunnable, but it deletes all the data; it belongs only with temporary or staging tables.
sp_rename, Dependencies and Synonyms
Renaming a column is a single command, but you have to ask first who uses that name:
SELECT referencing_schema_name, referencing_entity_name FROM sys.dm_sql_referencing_entities('dbo.books', 'OBJECT');
referencing_schema_name referencing_entity_name
----------------------- -----------------------
dbo CK_books_list_price
dbo CK_books_page_count
dbo v_book_prices
EXEC sp_rename 'dbo.books.listed_on', 'listed_date', 'COLUMN';
GO
SELECT TOP (2) * FROM dbo.v_book_prices ORDER BY book_id;
Caution: Changing any part of an object name could break scripts and stored procedures.
Msg 207, Level 16, State 1, Procedure v_book_prices, Line 2
Invalid column name 'listed_on'.
Msg 4413, Level 16, State 1, Line 1
Could not use view or function 'dbo.v_book_prices' because of binding errors.
sp_rename only gave a warning and did its job; the breakage showed up the first time the view was used. The text inside views, procedures and functions is not updated; application code, reports and ETL jobs keep using the old name too. Let's undo it:
EXEC sp_rename 'dbo.books.listed_date', 'listed_on', 'COLUMN';
GO
SELECT TOP (1) book_id, listed_on FROM dbo.v_book_prices ORDER BY book_id;
Caution: Changing any part of an object name could break scripts and stored procedures.
book_id listed_on
------- ----------
1 2024-06-01
If you define the view WITH SCHEMABINDING, this silent breakage cannot happen: SQL Server refuses up front to rename a referenced column. When I tried to rename list_price, I got "Msg 15336 ... cannot be renamed because the object participates in enforced dependencies." for the same reason, because CK_books_list_price sits on it:
CREATE OR ALTER VIEW dbo.v_book_prices WITH SCHEMABINDING AS
SELECT book_id, title, list_price, listed_on FROM dbo.books WHERE is_active = 1;
GO
EXEC sp_rename 'dbo.books.listed_on', 'listed_date', 'COLUMN';
Msg 15336, Level 16, State 1, Procedure sp_rename, Line 774
Object 'dbo.books.listed_on' cannot be renamed because the object participates in enforced dependencies.
In production, a rename is usually done not in one step but with a transition period: the new column is added, the two columns live side by side for a while, reading and writing code is moved over, and finally the old column is dropped. When you move a table, a synonym helps. A synonym is an alias for another object; code uses the synonym, and you change the object behind it:
CREATE OR ALTER SYNONYM dbo.catalog_books FOR dbo.books;
Msg 102, Level 15, State 1, Line 1
Incorrect syntax near 'SYNONYM'.
DROP SYNONYM IF EXISTS dbo.catalog_books;
CREATE SYNONYM dbo.catalog_books FOR dbo.books;
SELECT COUNT(*) AS via_synonym FROM dbo.catalog_books;
SELECT name, base_object_name FROM sys.synonyms;
via_synonym
-----------
77
(1 rows affected)
name base_object_name
------------- ----------------
catalog_books [dbo].[books]
CREATE OR ALTER is not supported for synonyms; the rerunnable form is DROP SYNONYM IF EXISTS + CREATE SYNONYM. Typical use: preparing catalog data in a new table (for example books_v2) in the background and switching the synonym to the new table in one short transaction, or pointing to a table in another database without changing the application's code. A synonym is a name redirect; if the columns of the table behind it change, the code still breaks.
SQL Server and PostgreSQL Counterparts
I ran the expressions in the PostgreSQL column in my own PostgreSQL 18 container:
| SQL Server | Behaviour | PostgreSQL |
|---|---|---|
IDENTITY(1,1) | Rollbacks and errors leave gaps | GENERATED ALWAYS AS IDENTITY; OVERRIDING SYSTEM VALUE for explicit values |
SET IDENTITY_INSERT ON | One table per session | OVERRIDING SYSTEM VALUE (per statement) |
CREATE SEQUENCE ... CACHE, NEXT VALUE FOR | Has gaps; reading consumes | CREATE SEQUENCE, nextval() |
Computed column, PERSISTED | Persisted needs determinism | GENERATED ALWAYS AS (...); VIRTUAL by default in 18, STORED available |
CREATE OR ALTER | Views, procedures, functions, triggers | CREATE OR REPLACE (views, functions, procedures) |
DROP ... IF EXISTS | Silently skips if missing | DROP ... IF EXISTS, ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS |
| Unique index | Does not allow more than one NULL | NULLs count as distinct; NULLS NOT DISTINCT behaves like SQL Server |
sp_rename column | Dependent views break | ALTER TABLE ... RENAME COLUMN; views keep working |
| Synonym | Alias for an object | None; search_path or a view |
Two differences matter most for deploy scripts. In PostgreSQL the phone index from the opening was created without errors, because NULLs count as distinct from each other in a unique index. And in PostgreSQL almost all DDL runs inside a transaction: when I added a column in a transaction and then ran an insert that failed, the column was rolled back too. When I renamed a column, the view kept working; PostgreSQL stores the view against the column itself, not its name, and updates the definition to phone_number AS phone.
In Production
Every deploy script should answer two questions: what state is the database in if it stops halfway, and what happens if it runs a second time? The answers should be "the state before the script" and "nothing". For that, make every step depend on an existence check (OBJECT_ID, COL_LENGTH, sys.indexes, CREATE OR ALTER, DROP ... IF EXISTS), run the steps in a single transaction with XACT_ABORT ON where possible, and run multi-batch scripts with :on error exit. Put long-running, lock-holding steps on large tables, such as CREATE INDEX and column type changes, into a separate window and a separate script; online index operations and their locking behaviour are covered in the SQL Server Derinlemesine series.
Treat identity and sequence gaps as normal and do not base business rules on them. Before sp_rename, check dependencies with sys.dm_sql_referencing_entities, but remember that this query cannot see users outside the database (application, reports, ETL). Versioning schema changes and managing them with tools (SQL Database Projects, DbUp, Flyway) is the subject of the upcoming schema change post in the SQL Server Derinlemesine series; the patterns in this post are also the basis of the scripts those tools generate or run.
Common Pitfalls
- Assuming identity or sequence values have no gaps; rollbacks, errors, caching and restarts leave gaps.
- Calling
NEXT VALUE FORto see "what is the next number" and using the number up; readsys.sequences.current_value. - Letting a deploy script without a transaction or checks stop halfway; the second run fails with errors such as Msg 2705.
- Adding a new column and using it in the same batch (Msg 207, the batch does not run at all).
- Ignoring the warning from
sp_rename; dependent views and procedures break on first use. - Trying to make a nondeterministic expression
PERSISTED(Msg 4936). - Assuming a unique index allows more than one
NULL; use a filtered index. - Making a table that holds data "rerunnable" with
DROP TABLE IF EXISTSin a deploy script.
A Small Experiment
★☆☆ Write a script that adds a weight_grams column and a CHECK (weight_grams > 0) constraint to the books table and does not fail when it runs twice. Check whether the constraint exists with OBJECT_ID('dbo.CK_books_weight_grams', 'C').
★★☆ Give the existing 380 orders an order_number: the numbers should follow the order date, and when the script runs again it should not touch orders that already have a number. Then make order_number NOT NULL and add a unique constraint on it.
★★★ Write a script that copies dbo.books to dbo.books_v2 with list_price as DECIMAL(12,2), then switches the dbo.catalog_books synonym to the new table inside a single transaction. What happens if a view uses the synonym? Does sys.dm_sql_referencing_entities see what is behind the synonym?
With this post we complete the SQL Temelleri series. In 25 posts we covered the query language itself, starting from the relational model and data types and moving through SELECT, joins, grouping, subqueries, CTEs, window functions and data modification; then dialect differences, what SQL Server 2022 and 2025 added, the everyday T-SQL function catalog, JSON, writing scripts and deploy scripts. The SQL Server Derinlemesine series, which continues with the same Bookstore database, goes behind these queries: indexes, execution plans, statistics, transaction isolation and locks, stored procedures and tempdb. In this series you learned what a query returns; there you will learn why it is slow or why it is blocked.