Tag: Constraints

  • ON DELETE CASCADE in SQL Server: Powerful, and Genuinely Dangerous If Misused

    ON DELETE CASCADE in SQL Server: Powerful, and Genuinely Dangerous If Misused

    Foreign keys (Fundamentals Chapter 5) can do more than just block invalid deletes — they can propagate changes automatically. That power cuts both ways, and this is one of the few topics in this course where the “safe default” genuinely differs from the option that looks most convenient in a demo.

    Cascade: One Push, Many FallsDELETE Category WHERE id=1Categoryid=1 deletedON DELETE CASCADE →ProductFK CASCADEON DELETE CASCADE →ReviewFK CASCADEGotcha: one DELETE can silently wipe out far more thanintended — always COUNT(*) the blast radius first. 📌

    The Four Options

    CREATE TABLE dbo.Category2 (
        category_id INT IDENTITY(1,1) PRIMARY KEY,
        name        NVARCHAR(50) NOT NULL
    );
    
    CREATE TABLE dbo.Product2 (
        product_id  INT IDENTITY(1,1) PRIMARY KEY,
        category_id INT NOT NULL
            REFERENCES dbo.Category2(category_id) ON DELETE CASCADE ON UPDATE CASCADE,
        name        NVARCHAR(100) NOT NULL
    );
    Option Behavior when parent row is deleted
    NO ACTION (default) Blocks the delete if child rows reference it — exactly the Chapter 5 (Fundamentals) behavior you’ve already seen
    CASCADE Automatically deletes matching child rows too
    SET NULL Sets the child’s FK column to NULL (requires the FK column to allow NULL)
    SET DEFAULT Sets the child’s FK column to its DEFAULT value (requires one to be defined)

    The Real Risk

    CASCADE on a deep relationship chain can silently wipe out far more data than intended from one top-level delete

    Seeing this concretely: if Category → Product is CASCADE, and Product → Review is also CASCADE, then one DELETE FROM Category2 WHERE category_id = 1 can silently remove every product in that category and every review those products ever received — all from a single statement, with no confirmation, no listing of what’s about to disappear. Multi-level CASCADE chains are exactly how a well-intentioned “clean up an old category” operation becomes an incident.

    -- Prove the blast radius yourself before trusting a cascade chain in production:
    SELECT COUNT(*) FROM dbo.Product2 WHERE category_id = 1; -- how many products?
    -- If Review also cascades from Product, this number matters too:
    -- SELECT COUNT(*) FROM dbo.Review WHERE product_id IN (SELECT product_id FROM dbo.Product2 WHERE category_id = 1);

    A Concrete Decision Rule

    CASCADE is right for genuinely dependent data — data with no meaning or value once its parent is gone (delete an Order, its OrderItems should go too; nobody wants orphaned line items pointing at a deleted order). It’s the wrong choice for anything with independent value — don’t cascade-delete Product just because a Category is removed; the products still exist and are still sellable, they just need a new category. Usually you want NO ACTION there, forcing an explicit, visible decision (reassign the products, or delete them deliberately) instead of an invisible side effect.

    SQL Server also blocks a specific dangerous configuration outright: you cannot create multiple CASCADE paths that could reach the same table through different routes, since it can’t guarantee a deterministic order of operations — a useful, hardcoded safety net worth knowing about rather than fighting.

    Practice tip: Before adding ON DELETE CASCADE to any real foreign key, write the SELECT COUNT(*) query that shows exactly what a worst-case delete would remove, and actually run it against realistic data volume. If that number would alarm you in production, NO ACTION is very likely the correct default, not CASCADE.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Constraints vs Triggers vs Procedures: Where to Enforce a SQL Server Business Rule

    Constraints vs Triggers vs Procedures: Where to Enforce a SQL Server Business Rule

    You now have four genuinely different tools capable of enforcing the same underlying rule — CHECK constraints (Fundamentals Ch.6), foreign keys, triggers (next up, Chapter 7), and stored procedures (just built). Choosing the right layer is a decision every schema designer eventually faces, and getting it wrong in either direction — too rigid or too loose — causes real, recurring pain.

    Where Should This Rule Live?(push it as close to the data as it can go)DATAcheapest, most guaranteedCHECK / FKsingle-row or existence rule(Fundamentals Ch.5-6)TRIGGERcross-row / cross-table logic(this chapter, next up)PROCEDUREmulti-step business process(sole write path required)most flexible, easiest to bypassfarther from data → more powerful, harder to guarantee

    The Options, Compared

    Mechanism Best for Limitation
    CHECK constraint Simple, single-row rules (price > 0) Cannot reference other tables or other rows
    FOREIGN KEY Referential existence rules Only “must exist,” not conditional logic
    Trigger Cross-row or cross-table logic, automatic side effects Harder to reason about, invisible to callers reading application code alone
    Stored procedure (as sole write path) Complex multi-step processes with clear business logic Only works if literally nothing else can bypass it

    The Guiding Principle

    Push a rule as close to the data as it can go without becoming unmaintainable

    A single-column rule belongs in a CHECK constraint — cheap, guaranteed, self-documenting, and enforced no matter what wrote the data (the exact reasoning from Fundamentals Chapter 6). A rule spanning multiple tables usually needs a trigger, precisely because CHECK constraints can’t reference other tables. A rule that’s genuinely a business process — multiple steps, conditional branches, needing to notify or log along the way — belongs in a stored procedure, provided your architecture guarantees that procedure is the only path data takes into the table.

    Walking a Real Rule Through All Four Layers

    Take: “a booking’s total nights can never exceed 30.” Watch how the same rule looks completely different depending on which layer enforces it:

    -- As a CHECK constraint: works ONLY if check_in/check_out are both plain columns on one row
    ALTER TABLE dbo.Booking ADD CONSTRAINT CK_MaxStay CHECK (DATEDIFF(DAY, check_in, check_out) <= 30);
    
    -- As a trigger: needed instead if the rule required looking at OTHER bookings
    -- (e.g. "no guest can have more than 30 total nights booked across all their reservations")
    CREATE TRIGGER trg_Booking_MaxTotalNights ON dbo.Booking AFTER INSERT AS
    BEGIN
        IF EXISTS (
            SELECT guest_id FROM dbo.Booking
            WHERE guest_id IN (SELECT guest_id FROM inserted)
            GROUP BY guest_id HAVING SUM(DATEDIFF(DAY, check_in, check_out)) > 30
        )
            THROW 51020, 'Guest exceeds 30 total booked nights.', 1;
    END;

    The single-row version is a CHECK constraint’s job exactly. The moment the rule needs to look across multiple rows (all of one guest’s bookings, not just the one being inserted), a CHECK constraint structurally cannot do it — that’s the trigger’s job instead. This is the actual decision criterion, not a vague sense of “complexity.”

    Practice tip: For any business rule you’re about to enforce, ask one concrete question first: “does validating this require looking at data outside the single row being changed?” If no, CHECK constraint. If yes, trigger (or procedure, if it’s really a multi-step process rather than a validation). That one question resolves the large majority of real cases.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • Computed Columns and Multi-Column CHECK Constraints in SQL Server

    Computed Columns and Multi-Column CHECK Constraints in SQL Server

    CHECK constraints get genuinely powerful once they can reference multiple columns in the same row — exactly the boundary the previous lesson drew — and computed columns let you derive values automatically instead of trusting every INSERT/UPDATE to calculate them correctly and consistently.

    Computed Columns: Derived, Not Writtencheck_inDATE columncheck_outDATE columnnightly_rateDECIMAL, CHECK > 0nightsAS DATEDIFF(day,in,out)🔒 auto-derived, read-onlytotal_costAS nights * nightly_rate🔒 PERSISTED, indexableCHECK(check_out > check_in)

    A Table Using Both

    CREATE TABLE dbo.Booking (
        booking_id  INT IDENTITY(1,1) PRIMARY KEY,
        check_in    DATE NOT NULL,
        check_out   DATE NOT NULL,
        nightly_rate DECIMAL(8,2) NOT NULL CHECK (nightly_rate > 0),
        nights      AS DATEDIFF(DAY, check_in, check_out),
        total_cost  AS (DATEDIFF(DAY, check_in, check_out) * nightly_rate) PERSISTED,
        CONSTRAINT CK_Booking_Dates CHECK (check_out > check_in)
    );
    
    INSERT INTO dbo.Booking (check_in, check_out, nightly_rate) VALUES ('2024-06-01', '2024-06-05', 150.00);
    SELECT booking_id, nights, total_cost FROM dbo.Booking;

    Notice nights and total_cost are never written to directly — you can’t INSERT a value into them, and SQL Server rejects the attempt if you try. They’re recalculated automatically from check_in, check_out, and nightly_rate every time those source columns change, which structurally guarantees they can never drift out of sync the way a manually-maintained “total” column could.

    PERSISTED: Store It or Compute It Live?

    Not PERSISTED Recalculated on every SELECT Cannot be indexed PERSISTED Stored on disk, auto-synced Can be indexed

    Worthwhile when the column is read often relative to how often source columns change — total_cost on a booking is a good candidate, since it’s likely queried far more often than a booking’s dates ever change after creation. A table-level CHECK constraint (like CK_Booking_Dates) can reference multiple columns in the same row — just never another table or another row, which was exactly the boundary the previous lesson established.

    A Deterministic Requirement, Revisited

    -- FAILS: a computed column referencing GETDATE() cannot be PERSISTED or indexed,
    -- for the exact same non-deterministic reason a scalar function couldn't be (Chapter 2)
    ALTER TABLE dbo.Booking ADD days_until_checkin AS DATEDIFF(DAY, GETDATE(), check_in) PERSISTED;
    -- Msg 4936: Computed column 'days_until_checkin' in table 'Booking' cannot be persisted
    -- because the column is non-deterministic.
    Common mistake: Trying to PERSIST a computed column that depends on the current date/time or a random value, then being confused by the deterministic-function error. This is the same rule from Chapter 2’s scalar functions, applied to computed columns instead — a persisted value must be reproducible from its inputs alone, with nothing external sneaking in.
    Practice tip: Add a second computed column to Booking — is_long_stay AS CASE WHEN DATEDIFF(DAY, check_in, check_out) > 7 THEN 1 ELSE 0 END — and confirm you can filter on it directly in a WHERE clause, exactly like a normal column.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.

  • PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK: SQL Server Constraints Explained

    PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK: SQL Server Constraints Explained

    You’ve met each of these individually already — PK and FK in Chapter 5, NOT NULL and DEFAULT in Chapter 2. This lesson brings the full constraint family together in one place, adds UNIQUE and CHECK, and is explicit about exactly what each one guarantees and how it fails.

    Four Constraints, One Job: Reject Bad Data(one schema, four different guarantees)PRIMARY KEYunique + never nullFOREIGN KEYmust exist elsewhereUNIQUEno dupes, 1 NULL okCHECKmust pass a ruleStaffstaff_id (PK)email (UNIQUE)department_id (FK)salary (CHECK > 0)one row per stafferGotcha: UNIQUE allows oneNULL row (NULLs aren’t equalto each other) — PRIMARY KEYnever allows any NULL.INSERT salary = 50000INSERT salary = -100CHECK constraint blocks it

    CREATE TABLE dbo.Department (
        department_id   INT IDENTITY(1,1) PRIMARY KEY,
        department_name NVARCHAR(50) NOT NULL UNIQUE
    );
    
    CREATE TABLE dbo.Staff (
        staff_id       INT IDENTITY(1,1) PRIMARY KEY,
        email          NVARCHAR(100) NOT NULL UNIQUE,
        department_id  INT NOT NULL REFERENCES dbo.Department(department_id),
        salary         DECIMAL(10,2) NOT NULL CHECK (salary > 0)
    );

    What Each One Guarantees

    Constraint Guarantees
    PRIMARY KEY Uniquely identifies every row; implies NOT NULL + UNIQUE; a table can have only one
    FOREIGN KEY Value must exist in the referenced table’s PK (or be NULL, if the FK column allows it)
    UNIQUE No two rows share this value — but unlike PK, allows one NULL (NULL isn’t considered equal to another NULL, even here), and a table can have several UNIQUE constraints
    CHECK Value must satisfy a boolean expression, evaluated on every INSERT/UPDATE

    Watching Them Do Their Job

    INSERT INTO dbo.Staff (email, department_id, salary) VALUES ('bad@co.com', 999, 50000);
    -- Error: FOREIGN KEY constraint... department_id 999 doesn't exist
    
    INSERT INTO dbo.Staff (email, department_id, salary) VALUES ('bad2@co.com', 1, -100);
    -- Error: CHECK constraint "CK_Staff_salary" violated
    
    INSERT INTO dbo.Staff (email, department_id, salary) VALUES ('taken@co.com', 1, 60000);
    INSERT INTO dbo.Staff (email, department_id, salary) VALUES ('taken@co.com', 1, 65000);
    -- Error: Violation of UNIQUE KEY constraint... duplicate email

    Naming Your Constraints on Purpose

    -- Unnamed — SQL Server auto-generates a name like CK__Staff__salary__1234ABCD
    salary DECIMAL(10,2) NOT NULL CHECK (salary > 0)
    
    -- Named explicitly — readable in error messages and easy to ALTER/DROP later
    CONSTRAINT CK_Staff_PositiveSalary CHECK (salary > 0)
    Practice tip: Always name your own constraints in real schemas. “Violation of CHECK constraint CK_Staff_PositiveSalary” tells you and your teammates exactly what rule broke; an auto-generated name with a random suffix tells you nothing without looking it up.

    Adding a Constraint to an Existing Table

    -- The table already exists; add the rule after the fact
    ALTER TABLE dbo.Staff ADD CONSTRAINT CK_Staff_ValidEmail CHECK (email LIKE '%_@_%._%');
    
    -- Temporarily allow existing bad rows to be re-checked separately (rare, use with care)
    ALTER TABLE dbo.Staff WITH NOCHECK ADD CONSTRAINT CK_Staff_PositiveSalary CHECK (salary > 0);
    Common mistake: Adding a CHECK constraint with WITH NOCHECK to skip validating existing rows, then assuming the constraint is fully trustworthy going forward. It isn’t — existing violating rows stay in the table untouched, and the constraint is marked “not trusted,” which means the query optimizer can’t safely use it to simplify certain queries either. Only use WITH NOCHECK when you deliberately intend to clean up existing violations separately, and re-validate with WITH CHECK CHECK CONSTRAINT ALL once you have.

    What Happens When You Try to Delete a Constraint’s “Reason”

    -- Trying to drop a department that staff still reference:
    DELETE FROM dbo.Department WHERE department_id = 1;
    -- Error: The DELETE statement conflicted with the REFERENCE constraint
    
    -- The correct sequence: remove or reassign dependents first
    UPDATE dbo.Staff SET department_id = 2 WHERE department_id = 1;
    DELETE FROM dbo.Department WHERE department_id = 1;
    Practice tip: Design the full Staff/Department schema above yourself from a blank query window, including at least one intentional constraint violation for each of PK, FK, UNIQUE, and CHECK, and read each actual error message SQL Server gives you. Recognizing these four error shapes on sight is a genuinely useful, permanent skill.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.

  • Why Push Business Rules Into Your SQL Server Schema, Not Just the App

    Why Push Business Rules Into Your SQL Server Schema, Not Just the App

    Constraints aren’t just data hygiene — they’re how you encode real business rules directly into the schema, so they can never be silently bypassed by a buggy application, a second app added later that talks to the same database, or a stray ad-hoc UPDATE run during an incident.

    Who Actually Enforces This Rule?(app-only validation vs. schema-level)Web App Formvalidates on submitAdmin Scriptno UI validation here!Bulk Import Jobwritten by someone elsedbo.CustomerOrderorder_date DATE NOT NULLship_date DATE NULLCHECK (ship_date >= order_date)one rule, enforced for EVERY writerINSERT ship_date = order_date+3passes the CHECK — acceptedINSERT ship_date = order_date-5shipped before it was orderedCHECK constraint blocks itGotcha: app-only validation is bypassed by a second app,an admin script, or a direct fix during an incident.The CHECK constraint is the only one with zero gaps.

    Encoding a Real Rule

    -- Rule: an order's ship date can never be before its order date
    CREATE TABLE dbo.CustomerOrder (
        order_id    INT IDENTITY(1,1) PRIMARY KEY,
        order_date  DATE NOT NULL,
        ship_date   DATE NULL
            CHECK (ship_date IS NULL OR ship_date >= order_date),
        total_usd   DECIMAL(10,2) NOT NULL CHECK (total_usd >= 0)
    );
    -- Proving the rule holds, not just reading about it:
    INSERT INTO dbo.CustomerOrder (order_date, ship_date, total_usd) VALUES ('2026-01-10', '2026-01-05', 49.99);
    -- Error: CHECK constraint violated — shipped 5 days BEFORE it was ordered, correctly rejected

    A CHECK Constraint Spanning Multiple Columns

    CHECK isn’t limited to validating one column against a literal — it can compare columns on the same row to each other, as shown above (ship_date against order_date). Another common shape:

    CREATE TABLE dbo.Promotion (
        promotion_id  INT IDENTITY(1,1) PRIMARY KEY,
        starts_on     DATE NOT NULL,
        ends_on       DATE NOT NULL,
        CHECK (ends_on > starts_on)
    );

    This kind of cross-column rule is exactly the class of business logic that’s easy to forget to validate in one code path of an application (a bulk-import script, an admin panel, an API endpoint added six months later by someone unfamiliar with the original rule) but structurally impossible to skip once it lives in the schema.

    The Last Line of Defense

    App-only validation Bypassed by bugs, other apps, or a direct DB script during an incident Database constraint Enforced no matter what wrote the data — no gaps, no exceptions

    Applications get replaced, have bugs, or get bypassed by a direct database script during an incident. A CHECK constraint at the database layer is enforced no matter what wrote the data — it’s the guarantee application code alone can never fully provide. This is sometimes summarized as “defense in depth”: validate in the application for a fast, friendly error message to the user, and validate in the database as the guarantee that actually holds under all circumstances.

    Where This Doesn’t Reach — And What Does

    CHECK constraints are limited to logic expressible within a single row’s own columns — they can’t reference other tables or aggregate across rows. “A department can’t have more than 20 staff” or “an order’s total must match the sum of its line items” needs a different tool: a trigger, or logic in a stored procedure. That’s a deliberate scope boundary you’ll meet by name (constraints vs. triggers vs. procedures) as a full decision framework in SQL Server for Developers & DBAs — for now, the key lesson is simply that single-row rules belong in CHECK constraints, full stop, because nothing enforces them more reliably.

    Practice tip: Look at any form you’ve filled out recently (a signup form, a checkout flow) and identify one validation rule it enforces. Ask yourself: is that rule also enforced at the database level, or only in that one form? If you can imagine a second way data could enter that table — an admin tool, a script, a different app — that’s exactly the gap a CHECK constraint closes.

    Enjoyed this?

    Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

    📡 Subscribe via RSS  | 
    Share on X  | 
    Share on LinkedIn  | 
    Share on Facebook

    Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.