[{"data":1,"prerenderedAt":3583},["ShallowReactive",2],{"page-\u002Fsql\u002F12-constraints-and-keys":3},{"id":4,"title":5,"body":6,"description":16,"extension":3577,"meta":3578,"navigation":221,"path":3579,"seo":3580,"stem":3581,"__hash__":3582},"content\u002Fsql\u002F12-constraints-and-keys.md","12 — Constraints & Keys",{"type":7,"value":8,"toc":3555},"minimark",[9,13,17,22,129,132,388,394,397,525,534,537,545,729,734,824,848,852,871,874,877,1052,1056,1125,1296,1316,1320,1323,1351,1389,1393,1400,1624,1628,1849,1853,1865,2095,2099,2105,2241,2245,2922,2926,3068,3072,3163,3167,3348,3352,3358,3390,3551],[10,11,5],"h1",{"id":12},"_12-constraints-keys",[14,15,16],"p",{},"Constraints are rules the database enforces on data — the \"guardrails\" that keep data consistent regardless of application bugs. Constraints are the difference between a database and a spreadsheet. This chapter is a code-first reference for every constraint type, their gotchas, and production patterns for zero-downtime constraint management.",[18,19,21],"h2",{"id":20},"the-constraint-toolkit","The Constraint Toolkit",[23,24,25,41],"table",{},[26,27,28],"thead",{},[29,30,31,35,38],"tr",{},[32,33,34],"th",{},"Constraint",[32,36,37],{},"Purpose",[32,39,40],{},"NULL Behavior",[42,43,44,59,72,89,102,115],"tbody",{},[29,45,46,53,56],{},[47,48,49],"td",{},[50,51,52],"code",{},"NOT NULL",[47,54,55],{},"Column can't be NULL.",[47,57,58],{},"Rejects NULL.",[29,60,61,66,69],{},[47,62,63],{},[50,64,65],{},"UNIQUE",[47,67,68],{},"No duplicate non-NULL values.",[47,70,71],{},"Multiple NULLs allowed (PG, standard).",[29,73,74,79,87],{},[47,75,76],{},[50,77,78],{},"PRIMARY KEY",[47,80,81,83,84,86],{},[50,82,52],{}," + ",[50,85,65],{},". One per table.",[47,88,58],{},[29,90,91,96,99],{},[47,92,93],{},[50,94,95],{},"FOREIGN KEY",[47,97,98],{},"Value must exist in another table.",[47,100,101],{},"NULL allowed (means \"no reference\").",[29,103,104,109,112],{},[47,105,106],{},[50,107,108],{},"CHECK",[47,110,111],{},"Arbitrary boolean expression must be TRUE.",[47,113,114],{},"UNKNOWN (NULL) passes.",[29,116,117,123,126],{},[47,118,119,122],{},[50,120,121],{},"EXCLUDE"," (PG)",[47,124,125],{},"No two rows satisfy a custom operator predicate.",[47,127,128],{},"Depends on operator.",[18,130,52],{"id":131},"not-null",[133,134,136],"code-wrapper",{"language":135},"sql",[137,138,142],"pre",{"className":139,"code":140,"language":135,"meta":141,"style":141},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE users (\n  id    BIGSERIAL PRIMARY KEY,\n  email TEXT NOT NULL,        -- required: INSERT without email fails\n  name  TEXT                  -- optional: NULL allowed, default is NULL\n);\n\n-- Adding NOT NULL to an existing column with NULLs requires backfilling first.\n-- The SET NOT NULL must verify no existing row is NULL → full table scan under lock.\n\n-- ❌ WRONG: fails if any NULL exists, and holds ACCESS EXCLUSIVE lock during scan.\nALTER TABLE users ALTER COLUMN name SET NOT NULL;\n\n-- ✅ CORRECT: backfill, then add via NOT VALID + VALIDATE (light lock).\nUPDATE users SET name = 'Unknown' WHERE name IS NULL;\n\nALTER TABLE users ADD CONSTRAINT users_name_nn CHECK (name IS NOT NULL) NOT VALID;\nALTER TABLE users VALIDATE CONSTRAINT users_name_nn;\n-- Then optionally: ALTER TABLE users ALTER COLUMN name SET NOT NULL;\n-- (The CHECK constraint is functionally equivalent and was added with a light lock.)\n","",[50,143,144,164,179,198,210,216,223,229,235,240,246,273,278,284,318,323,360,376,382],{"__ignoreMap":141},[145,146,149,153,156,160],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"svdQ7","CREATE",[145,154,155],{"class":151}," TABLE",[145,157,159],{"class":158},"sIsaT"," users",[145,161,163],{"class":162},"ssxIu"," (\n",[145,165,167,170,173,176],{"class":147,"line":166},2,[145,168,169],{"class":162},"  id    ",[145,171,172],{"class":151},"BIGSERIAL",[145,174,175],{"class":151}," PRIMARY KEY",[145,177,178],{"class":162},",\n",[145,180,182,185,188,191,194],{"class":147,"line":181},3,[145,183,184],{"class":162},"  email ",[145,186,187],{"class":151},"TEXT",[145,189,190],{"class":151}," NOT NULL",[145,192,193],{"class":162},",        ",[145,195,197],{"class":196},"sdCPZ","-- required: INSERT without email fails\n",[145,199,201,204,207],{"class":147,"line":200},4,[145,202,203],{"class":151},"  name",[145,205,206],{"class":151},"  TEXT",[145,208,209],{"class":196},"                  -- optional: NULL allowed, default is NULL\n",[145,211,213],{"class":147,"line":212},5,[145,214,215],{"class":162},");\n",[145,217,219],{"class":147,"line":218},6,[145,220,222],{"emptyLinePlaceholder":221},true,"\n",[145,224,226],{"class":147,"line":225},7,[145,227,228],{"class":196},"-- Adding NOT NULL to an existing column with NULLs requires backfilling first.\n",[145,230,232],{"class":147,"line":231},8,[145,233,234],{"class":196},"-- The SET NOT NULL must verify no existing row is NULL → full table scan under lock.\n",[145,236,238],{"class":147,"line":237},9,[145,239,222],{"emptyLinePlaceholder":221},[145,241,243],{"class":147,"line":242},10,[145,244,245],{"class":196},"-- ❌ WRONG: fails if any NULL exists, and holds ACCESS EXCLUSIVE lock during scan.\n",[145,247,249,252,254,257,259,262,265,268,270],{"class":147,"line":248},11,[145,250,251],{"class":151},"ALTER",[145,253,155],{"class":151},[145,255,256],{"class":162}," users ",[145,258,251],{"class":151},[145,260,261],{"class":162}," COLUMN ",[145,263,264],{"class":151},"name",[145,266,267],{"class":151}," SET",[145,269,190],{"class":151},[145,271,272],{"class":162},";\n",[145,274,276],{"class":147,"line":275},12,[145,277,222],{"emptyLinePlaceholder":221},[145,279,281],{"class":147,"line":280},13,[145,282,283],{"class":196},"-- ✅ CORRECT: backfill, then add via NOT VALID + VALIDATE (light lock).\n",[145,285,287,290,292,295,298,301,305,308,310,313,316],{"class":147,"line":286},14,[145,288,289],{"class":151},"UPDATE",[145,291,256],{"class":162},[145,293,294],{"class":151},"SET",[145,296,297],{"class":151}," name",[145,299,300],{"class":151}," =",[145,302,304],{"class":303},"sJ6F3"," 'Unknown'",[145,306,307],{"class":151}," WHERE",[145,309,297],{"class":151},[145,311,312],{"class":151}," IS",[145,314,315],{"class":151}," NULL",[145,317,272],{"class":162},[145,319,321],{"class":147,"line":320},15,[145,322,222],{"emptyLinePlaceholder":221},[145,324,326,328,330,332,335,338,341,343,346,348,351,354,357],{"class":147,"line":325},16,[145,327,251],{"class":151},[145,329,155],{"class":151},[145,331,256],{"class":162},[145,333,334],{"class":151},"ADD",[145,336,337],{"class":151}," CONSTRAINT",[145,339,340],{"class":162}," users_name_nn ",[145,342,108],{"class":151},[145,344,345],{"class":162}," (",[145,347,264],{"class":151},[145,349,350],{"class":151}," IS NOT NULL",[145,352,353],{"class":162},") ",[145,355,356],{"class":151},"NOT",[145,358,359],{"class":162}," VALID;\n",[145,361,363,365,367,370,373],{"class":147,"line":362},17,[145,364,251],{"class":151},[145,366,155],{"class":151},[145,368,369],{"class":162}," users VALIDATE ",[145,371,372],{"class":151},"CONSTRAINT",[145,374,375],{"class":162}," users_name_nn;\n",[145,377,379],{"class":147,"line":378},18,[145,380,381],{"class":196},"-- Then optionally: ALTER TABLE users ALTER COLUMN name SET NOT NULL;\n",[145,383,385],{"class":147,"line":384},19,[145,386,387],{"class":196},"-- (The CHECK constraint is functionally equivalent and was added with a light lock.)\n",[14,389,390,391,393],{},"Add ",[50,392,52],{}," to every column unless you have a specific reason to allow NULL. NULL handling is error-prone (three-valued logic); fewer NULLs = fewer bugs.",[18,395,65],{"id":396},"unique",[133,398,399],{"language":135},[137,400,402],{"className":139,"code":401,"language":135,"meta":141,"style":141},"CREATE TABLE users (\n  id        BIGSERIAL PRIMARY KEY,\n  email     TEXT UNIQUE,                              -- column-level, single column\n  -- Table-level: composite unique on (tenant_id, email).\n  -- NULLs in any column are allowed (multiple NULL rows don't violate).\n  CONSTRAINT users_tenant_email_unique UNIQUE (tenant_id, email)\n);\n\n-- ⚠️ UNIQUE allows multiple NULLs in PostgreSQL (and standard SQL).\n-- NULL != NULL in three-valued logic, so two NULL rows don't \"conflict.\"\n-- SQL Server is the exception (allows only one NULL).\n\n-- \"At most one non-NULL\" pattern: partial unique index excluding NULLs.\nCREATE UNIQUE INDEX users_email_one_null\n  ON users (email) WHERE email IS NOT NULL;\n",[50,403,404,414,425,441,446,451,464,468,472,477,482,487,491,496,506],{"__ignoreMap":141},[145,405,406,408,410,412],{"class":147,"line":148},[145,407,152],{"class":151},[145,409,155],{"class":151},[145,411,159],{"class":158},[145,413,163],{"class":162},[145,415,416,419,421,423],{"class":147,"line":166},[145,417,418],{"class":162},"  id        ",[145,420,172],{"class":151},[145,422,175],{"class":151},[145,424,178],{"class":162},[145,426,427,430,432,435,438],{"class":147,"line":181},[145,428,429],{"class":162},"  email     ",[145,431,187],{"class":151},[145,433,434],{"class":151}," UNIQUE",[145,436,437],{"class":162},",                              ",[145,439,440],{"class":196},"-- column-level, single column\n",[145,442,443],{"class":147,"line":200},[145,444,445],{"class":196},"  -- Table-level: composite unique on (tenant_id, email).\n",[145,447,448],{"class":147,"line":212},[145,449,450],{"class":196},"  -- NULLs in any column are allowed (multiple NULL rows don't violate).\n",[145,452,453,456,459,461],{"class":147,"line":218},[145,454,455],{"class":151},"  CONSTRAINT",[145,457,458],{"class":162}," users_tenant_email_unique ",[145,460,65],{"class":151},[145,462,463],{"class":162}," (tenant_id, email)\n",[145,465,466],{"class":147,"line":225},[145,467,215],{"class":162},[145,469,470],{"class":147,"line":231},[145,471,222],{"emptyLinePlaceholder":221},[145,473,474],{"class":147,"line":237},[145,475,476],{"class":196},"-- ⚠️ UNIQUE allows multiple NULLs in PostgreSQL (and standard SQL).\n",[145,478,479],{"class":147,"line":242},[145,480,481],{"class":196},"-- NULL != NULL in three-valued logic, so two NULL rows don't \"conflict.\"\n",[145,483,484],{"class":147,"line":248},[145,485,486],{"class":196},"-- SQL Server is the exception (allows only one NULL).\n",[145,488,489],{"class":147,"line":275},[145,490,222],{"emptyLinePlaceholder":221},[145,492,493],{"class":147,"line":280},[145,494,495],{"class":196},"-- \"At most one non-NULL\" pattern: partial unique index excluding NULLs.\n",[145,497,498,500,503],{"class":147,"line":286},[145,499,152],{"class":151},[145,501,502],{"class":151}," UNIQUE INDEX",[145,504,505],{"class":158}," users_email_one_null\n",[145,507,508,511,514,517,520,523],{"class":147,"line":320},[145,509,510],{"class":151},"  ON",[145,512,513],{"class":162}," users (email) ",[145,515,516],{"class":151},"WHERE",[145,518,519],{"class":162}," email ",[145,521,522],{"class":151},"IS NOT NULL",[145,524,272],{"class":162},[14,526,527,528,530,531,533],{},"A ",[50,529,65],{}," constraint is implemented as a unique B-tree index under the hood — so it gives you an index for lookups too. No need to add a separate index on a ",[50,532,65],{}," column.",[18,535,78],{"id":536},"primary-key",[14,538,539,540,83,542,544],{},"The primary key uniquely identifies each row. It's ",[50,541,52],{},[50,543,65],{}," + (in some engines) a clustering hint.",[133,546,547],{"language":135},[137,548,550],{"className":139,"code":549,"language":135,"meta":141,"style":141},"-- Surrogate key: artificial ID, no business meaning. Stable, uniform, simple joins.\nCREATE TABLE users (\n  id    BIGSERIAL PRIMARY KEY,        -- auto-incrementing 8-byte integer\n  email TEXT NOT NULL\n);\n\n-- Natural key: real-world identifier. Meaningful, but can change.\nCREATE TABLE countries (\n  code        CHAR(2) PRIMARY KEY,    -- ISO 3166-1 alpha-2, genuinely immutable\n  name        TEXT NOT NULL\n);\n\n-- Composite PK: natural for junction tables (many-to-many).\nCREATE TABLE order_items (\n  order_id    BIGINT  NOT NULL REFERENCES orders(id),\n  product_id  BIGINT  NOT NULL REFERENCES products(id),\n  quantity    INTEGER NOT NULL,\n  PRIMARY KEY (order_id, product_id)   -- uniqueness on the combination\n);\n",[50,551,552,557,567,580,589,593,597,602,613,638,647,651,655,660,671,688,702,714,725],{"__ignoreMap":141},[145,553,554],{"class":147,"line":148},[145,555,556],{"class":196},"-- Surrogate key: artificial ID, no business meaning. Stable, uniform, simple joins.\n",[145,558,559,561,563,565],{"class":147,"line":166},[145,560,152],{"class":151},[145,562,155],{"class":151},[145,564,159],{"class":158},[145,566,163],{"class":162},[145,568,569,571,573,575,577],{"class":147,"line":181},[145,570,169],{"class":162},[145,572,172],{"class":151},[145,574,175],{"class":151},[145,576,193],{"class":162},[145,578,579],{"class":196},"-- auto-incrementing 8-byte integer\n",[145,581,582,584,586],{"class":147,"line":200},[145,583,184],{"class":162},[145,585,187],{"class":151},[145,587,588],{"class":151}," NOT NULL\n",[145,590,591],{"class":147,"line":212},[145,592,215],{"class":162},[145,594,595],{"class":147,"line":218},[145,596,222],{"emptyLinePlaceholder":221},[145,598,599],{"class":147,"line":225},[145,600,601],{"class":196},"-- Natural key: real-world identifier. Meaningful, but can change.\n",[145,603,604,606,608,611],{"class":147,"line":231},[145,605,152],{"class":151},[145,607,155],{"class":151},[145,609,610],{"class":158}," countries",[145,612,163],{"class":162},[145,614,615,618,621,624,628,630,632,635],{"class":147,"line":237},[145,616,617],{"class":162},"  code        ",[145,619,620],{"class":151},"CHAR",[145,622,623],{"class":162},"(",[145,625,627],{"class":626},"snvgF","2",[145,629,353],{"class":162},[145,631,78],{"class":151},[145,633,634],{"class":162},",    ",[145,636,637],{"class":196},"-- ISO 3166-1 alpha-2, genuinely immutable\n",[145,639,640,642,645],{"class":147,"line":242},[145,641,203],{"class":151},[145,643,644],{"class":151},"        TEXT",[145,646,588],{"class":151},[145,648,649],{"class":147,"line":248},[145,650,215],{"class":162},[145,652,653],{"class":147,"line":275},[145,654,222],{"emptyLinePlaceholder":221},[145,656,657],{"class":147,"line":280},[145,658,659],{"class":196},"-- Composite PK: natural for junction tables (many-to-many).\n",[145,661,662,664,666,669],{"class":147,"line":286},[145,663,152],{"class":151},[145,665,155],{"class":151},[145,667,668],{"class":158}," order_items",[145,670,163],{"class":162},[145,672,673,676,679,682,685],{"class":147,"line":320},[145,674,675],{"class":162},"  order_id    ",[145,677,678],{"class":151},"BIGINT",[145,680,681],{"class":151},"  NOT NULL",[145,683,684],{"class":151}," REFERENCES",[145,686,687],{"class":162}," orders(id),\n",[145,689,690,693,695,697,699],{"class":147,"line":325},[145,691,692],{"class":162},"  product_id  ",[145,694,678],{"class":151},[145,696,681],{"class":151},[145,698,684],{"class":151},[145,700,701],{"class":162}," products(id),\n",[145,703,704,707,710,712],{"class":147,"line":362},[145,705,706],{"class":162},"  quantity    ",[145,708,709],{"class":151},"INTEGER",[145,711,190],{"class":151},[145,713,178],{"class":162},[145,715,716,719,722],{"class":147,"line":378},[145,717,718],{"class":151},"  PRIMARY KEY",[145,720,721],{"class":162}," (order_id, product_id)   ",[145,723,724],{"class":196},"-- uniqueness on the combination\n",[145,726,727],{"class":147,"line":384},[145,728,215],{"class":162},[730,731,733],"h3",{"id":732},"surrogate-vs-natural-keys","Surrogate vs Natural Keys",[23,735,736,763],{},[26,737,738],{},[29,739,740,743,753],{},[32,741,742],{},"Aspect",[32,744,745,746,748,749,752],{},"Surrogate (",[50,747,172],{},"\u002F",[50,750,751],{},"UUID",")",[32,754,755,756,759,760,752],{},"Natural (",[50,757,758],{},"email",", ",[50,761,762],{},"isbn",[42,764,765,776,787,798,809],{},[29,766,767,770,773],{},[47,768,769],{},"Stability",[47,771,772],{},"Never changes",[47,774,775],{},"Can change (email rename, ISBN-10→13)",[29,777,778,781,784],{},[47,779,780],{},"Joins",[47,782,783],{},"Uniform type, simple",[47,785,786],{},"Varying types, wider",[29,788,789,792,795],{},[47,790,791],{},"Readability",[47,793,794],{},"Opaque ID",[47,796,797],{},"Meaningful",[29,799,800,803,806],{},[47,801,802],{},"Storage",[47,804,805],{},"Extra column",[47,807,808],{},"No extra column",[29,810,811,814,821],{},[47,812,813],{},"FK cascade",[47,815,816,817,820],{},"No ",[50,818,819],{},"ON UPDATE CASCADE"," needed",[47,822,823],{},"If key changes, all FKs must cascade",[14,825,826,345,830,832,833,835,836,839,840,844,845,847],{},[827,828,829],"strong",{},"Default to surrogate keys",[50,831,172],{}," or ",[50,834,751],{},"). Use natural keys only when truly immutable and unique (e.g., ",[50,837,838],{},"country_code","). Natural keys that ",[841,842,843],"em",{},"might"," change force ",[50,846,819],{}," across every referencing FK — expensive on large graphs.",[730,849,851],{"id":850},"composite-pk-column-order","Composite PK Column Order",[14,853,854,855,858,859,862,863,866,867,870],{},"A composite PK ",[50,856,857],{},"(tenant_id, id)"," can serve ",[50,860,861],{},"WHERE tenant_id = ?"," (leftmost prefix) but ",[827,864,865],{},"not"," ",[50,868,869],{},"WHERE id = ?"," alone. Order PK columns by selectivity and query pattern.",[18,872,95],{"id":873},"foreign-key",[14,875,876],{},"A FK enforces referential integrity: \"this value must exist in another table.\"",[133,878,879],{"language":135},[137,880,882],{"className":139,"code":881,"language":135,"meta":141,"style":141},"-- Column-level FK (inline, auto-named).\nCREATE TABLE orders (\n  id          BIGSERIAL PRIMARY KEY,\n  customer_id BIGINT NOT NULL REFERENCES customers(id),\n  amount      NUMERIC(10,2) NOT NULL\n);\n\n-- Table-level FK with explicit name and ON DELETE action.\nCREATE TABLE orders (\n  id          BIGSERIAL PRIMARY KEY,\n  customer_id BIGINT NOT NULL,\n  amount      NUMERIC(10,2) NOT NULL,\n  CONSTRAINT orders_customer_fk\n    FOREIGN KEY (customer_id) REFERENCES customers(id)\n    ON DELETE CASCADE           -- delete customer → delete their orders\n    ON UPDATE CASCADE           -- update customer.id → update orders.customer_id\n);\n",[50,883,884,889,900,911,925,948,952,956,961,971,981,991,1011,1018,1032,1040,1048],{"__ignoreMap":141},[145,885,886],{"class":147,"line":148},[145,887,888],{"class":196},"-- Column-level FK (inline, auto-named).\n",[145,890,891,893,895,898],{"class":147,"line":166},[145,892,152],{"class":151},[145,894,155],{"class":151},[145,896,897],{"class":158}," orders",[145,899,163],{"class":162},[145,901,902,905,907,909],{"class":147,"line":181},[145,903,904],{"class":162},"  id          ",[145,906,172],{"class":151},[145,908,175],{"class":151},[145,910,178],{"class":162},[145,912,913,916,918,920,922],{"class":147,"line":200},[145,914,915],{"class":162},"  customer_id ",[145,917,678],{"class":151},[145,919,190],{"class":151},[145,921,684],{"class":151},[145,923,924],{"class":162}," customers(id),\n",[145,926,927,930,933,935,938,941,943,945],{"class":147,"line":212},[145,928,929],{"class":162},"  amount      ",[145,931,932],{"class":151},"NUMERIC",[145,934,623],{"class":162},[145,936,937],{"class":626},"10",[145,939,940],{"class":162},",",[145,942,627],{"class":626},[145,944,353],{"class":162},[145,946,947],{"class":151},"NOT NULL\n",[145,949,950],{"class":147,"line":218},[145,951,215],{"class":162},[145,953,954],{"class":147,"line":225},[145,955,222],{"emptyLinePlaceholder":221},[145,957,958],{"class":147,"line":231},[145,959,960],{"class":196},"-- Table-level FK with explicit name and ON DELETE action.\n",[145,962,963,965,967,969],{"class":147,"line":237},[145,964,152],{"class":151},[145,966,155],{"class":151},[145,968,897],{"class":158},[145,970,163],{"class":162},[145,972,973,975,977,979],{"class":147,"line":242},[145,974,904],{"class":162},[145,976,172],{"class":151},[145,978,175],{"class":151},[145,980,178],{"class":162},[145,982,983,985,987,989],{"class":147,"line":248},[145,984,915],{"class":162},[145,986,678],{"class":151},[145,988,190],{"class":151},[145,990,178],{"class":162},[145,992,993,995,997,999,1001,1003,1005,1007,1009],{"class":147,"line":275},[145,994,929],{"class":162},[145,996,932],{"class":151},[145,998,623],{"class":162},[145,1000,937],{"class":626},[145,1002,940],{"class":162},[145,1004,627],{"class":626},[145,1006,353],{"class":162},[145,1008,52],{"class":151},[145,1010,178],{"class":162},[145,1012,1013,1015],{"class":147,"line":280},[145,1014,455],{"class":151},[145,1016,1017],{"class":162}," orders_customer_fk\n",[145,1019,1020,1023,1026,1029],{"class":147,"line":286},[145,1021,1022],{"class":151},"    FOREIGN KEY",[145,1024,1025],{"class":162}," (customer_id) ",[145,1027,1028],{"class":151},"REFERENCES",[145,1030,1031],{"class":162}," customers(id)\n",[145,1033,1034,1037],{"class":147,"line":320},[145,1035,1036],{"class":151},"    ON DELETE CASCADE",[145,1038,1039],{"class":196},"           -- delete customer → delete their orders\n",[145,1041,1042,1045],{"class":147,"line":325},[145,1043,1044],{"class":151},"    ON UPDATE CASCADE",[145,1046,1047],{"class":196},"           -- update customer.id → update orders.customer_id\n",[145,1049,1050],{"class":147,"line":362},[145,1051,215],{"class":162},[730,1053,1055],{"id":1054},"on-delete-on-update-actions","ON DELETE \u002F ON UPDATE Actions",[23,1057,1058,1068],{},[26,1059,1060],{},[29,1061,1062,1065],{},[32,1063,1064],{},"Action",[32,1066,1067],{},"Behavior on parent delete\u002Fupdate",[42,1069,1070,1081,1091,1101,1111],{},[29,1071,1072,1078],{},[47,1073,1074,1077],{},[50,1075,1076],{},"NO ACTION"," (default)",[47,1079,1080],{},"Blocks if referencing rows exist. Check is deferred to end of statement (can be deferred to commit).",[29,1082,1083,1088],{},[47,1084,1085],{},[50,1086,1087],{},"RESTRICT",[47,1089,1090],{},"Blocks immediately. Cannot be deferred.",[29,1092,1093,1098],{},[47,1094,1095],{},[50,1096,1097],{},"CASCADE",[47,1099,1100],{},"Deletes\u002Fupdates referencing rows too. Propagates through cascade chains.",[29,1102,1103,1108],{},[47,1104,1105],{},[50,1106,1107],{},"SET NULL",[47,1109,1110],{},"Sets FK column to NULL in referencing rows. Column must be nullable.",[29,1112,1113,1118],{},[47,1114,1115],{},[50,1116,1117],{},"SET DEFAULT",[47,1119,1120,1121,1124],{},"Sets FK column to its ",[50,1122,1123],{},"DEFAULT",". Column must have a default.",[133,1126,1127],{"language":135},[137,1128,1130],{"className":139,"code":1129,"language":135,"meta":141,"style":141},"-- CASCADE chain: deleting a customer → orders → order_items → audit_logs.\n-- Always review the full cascade graph before adding CASCADE.\nCREATE TABLE orders (\n  id          BIGSERIAL PRIMARY KEY,\n  customer_id BIGINT REFERENCES customers(id) ON DELETE CASCADE\n);\n\nCREATE TABLE order_items (\n  id         BIGSERIAL PRIMARY KEY,\n  order_id   BIGINT REFERENCES orders(id) ON DELETE CASCADE,\n  -- ^ deleting an order cascades to its items (true composition — correct)\n);\n\n-- Self-referencing FK: manager_id → employees.id.\n-- Root node has manager_id = NULL (must be nullable).\nCREATE TABLE employees (\n  id         BIGSERIAL PRIMARY KEY,\n  manager_id BIGINT REFERENCES employees(id) ON DELETE SET NULL\n  -- ^ deleting a manager sets reports' manager_id to NULL (orphan-safe)\n);\n",[50,1131,1132,1137,1142,1152,1162,1176,1180,1184,1194,1205,1222,1227,1231,1235,1240,1245,1256,1266,1286,1291],{"__ignoreMap":141},[145,1133,1134],{"class":147,"line":148},[145,1135,1136],{"class":196},"-- CASCADE chain: deleting a customer → orders → order_items → audit_logs.\n",[145,1138,1139],{"class":147,"line":166},[145,1140,1141],{"class":196},"-- Always review the full cascade graph before adding CASCADE.\n",[145,1143,1144,1146,1148,1150],{"class":147,"line":181},[145,1145,152],{"class":151},[145,1147,155],{"class":151},[145,1149,897],{"class":158},[145,1151,163],{"class":162},[145,1153,1154,1156,1158,1160],{"class":147,"line":200},[145,1155,904],{"class":162},[145,1157,172],{"class":151},[145,1159,175],{"class":151},[145,1161,178],{"class":162},[145,1163,1164,1166,1168,1170,1173],{"class":147,"line":212},[145,1165,915],{"class":162},[145,1167,678],{"class":151},[145,1169,684],{"class":151},[145,1171,1172],{"class":162}," customers(id) ",[145,1174,1175],{"class":151},"ON DELETE CASCADE\n",[145,1177,1178],{"class":147,"line":218},[145,1179,215],{"class":162},[145,1181,1182],{"class":147,"line":225},[145,1183,222],{"emptyLinePlaceholder":221},[145,1185,1186,1188,1190,1192],{"class":147,"line":231},[145,1187,152],{"class":151},[145,1189,155],{"class":151},[145,1191,668],{"class":158},[145,1193,163],{"class":162},[145,1195,1196,1199,1201,1203],{"class":147,"line":237},[145,1197,1198],{"class":162},"  id         ",[145,1200,172],{"class":151},[145,1202,175],{"class":151},[145,1204,178],{"class":162},[145,1206,1207,1210,1212,1214,1217,1220],{"class":147,"line":242},[145,1208,1209],{"class":162},"  order_id   ",[145,1211,678],{"class":151},[145,1213,684],{"class":151},[145,1215,1216],{"class":162}," orders(id) ",[145,1218,1219],{"class":151},"ON DELETE CASCADE",[145,1221,178],{"class":162},[145,1223,1224],{"class":147,"line":248},[145,1225,1226],{"class":196},"  -- ^ deleting an order cascades to its items (true composition — correct)\n",[145,1228,1229],{"class":147,"line":275},[145,1230,215],{"class":162},[145,1232,1233],{"class":147,"line":280},[145,1234,222],{"emptyLinePlaceholder":221},[145,1236,1237],{"class":147,"line":286},[145,1238,1239],{"class":196},"-- Self-referencing FK: manager_id → employees.id.\n",[145,1241,1242],{"class":147,"line":320},[145,1243,1244],{"class":196},"-- Root node has manager_id = NULL (must be nullable).\n",[145,1246,1247,1249,1251,1254],{"class":147,"line":325},[145,1248,152],{"class":151},[145,1250,155],{"class":151},[145,1252,1253],{"class":158}," employees",[145,1255,163],{"class":162},[145,1257,1258,1260,1262,1264],{"class":147,"line":362},[145,1259,1198],{"class":162},[145,1261,172],{"class":151},[145,1263,175],{"class":151},[145,1265,178],{"class":162},[145,1267,1268,1271,1273,1275,1278,1281,1283],{"class":147,"line":378},[145,1269,1270],{"class":162},"  manager_id ",[145,1272,678],{"class":151},[145,1274,684],{"class":151},[145,1276,1277],{"class":162}," employees(id) ",[145,1279,1280],{"class":151},"ON DELETE",[145,1282,267],{"class":151},[145,1284,1285],{"class":151}," NULL\n",[145,1287,1288],{"class":147,"line":384},[145,1289,1290],{"class":196},"  -- ^ deleting a manager sets reports' manager_id to NULL (orphan-safe)\n",[145,1292,1294],{"class":147,"line":1293},20,[145,1295,215],{"class":162},[14,1297,1298,1303,1304,1307,1308,1310,1311,832,1313,1315],{},[827,1299,1300,1302],{},[50,1301,1097],{}," is powerful and dangerous."," An accidental ",[50,1305,1306],{},"DELETE FROM customers WHERE id = 1"," can wipe orders, order_items, and audit_logs in one statement. Use ",[50,1309,1097],{}," only for true composition (parent owns children). For \"soft\" relationships, use ",[50,1312,1107],{},[50,1314,1087],{},".",[730,1317,1319],{"id":1318},"fks-and-performance","FKs and Performance",[14,1321,1322],{},"A FK adds:",[1324,1325,1326,1339,1345],"ul",{},[1327,1328,527,1329,1332,1333,748,1336,1338],"li",{},[827,1330,1331],{},"lookup"," on every ",[50,1334,1335],{},"INSERT",[50,1337,289],{}," of the FK column — needs an index on the referenced PK (PKs are indexed by default).",[1327,1340,527,1341,1344],{},[827,1342,1343],{},"lock"," on the referenced row (prevents it being deleted before the FK insert commits).",[1327,1346,1347,1350],{},[827,1348,1349],{},"No automatic index on the FK column itself"," — you must add one.",[133,1352,1353],{"language":135},[137,1354,1356],{"className":139,"code":1355,"language":135,"meta":141,"style":141},"-- ❌ MISSING INDEX: ON DELETE CASCADE from customers scans ALL of orders\n--    to find referencing rows, holding a lock the entire time.\n-- ✅ ALWAYS index FK columns:\nCREATE INDEX orders_customer_id_idx ON orders(customer_id);\n",[50,1357,1358,1363,1368,1373],{"__ignoreMap":141},[145,1359,1360],{"class":147,"line":148},[145,1361,1362],{"class":196},"-- ❌ MISSING INDEX: ON DELETE CASCADE from customers scans ALL of orders\n",[145,1364,1365],{"class":147,"line":166},[145,1366,1367],{"class":196},"--    to find referencing rows, holding a lock the entire time.\n",[145,1369,1370],{"class":147,"line":181},[145,1371,1372],{"class":196},"-- ✅ ALWAYS index FK columns:\n",[145,1374,1375,1377,1380,1383,1386],{"class":147,"line":200},[145,1376,152],{"class":151},[145,1378,1379],{"class":151}," INDEX",[145,1381,1382],{"class":158}," orders_customer_id_idx",[145,1384,1385],{"class":151}," ON",[145,1387,1388],{"class":162}," orders(customer_id);\n",[730,1390,1392],{"id":1391},"deferred-fk-checks","Deferred FK Checks",[14,1394,1395,1396,1399],{},"By default, FKs are checked at statement end. With ",[50,1397,1398],{},"DEFERRABLE INITIALLY DEFERRED",", the check is postponed to transaction commit — useful for circular references or multi-step reorderings.",[133,1401,1402],{"language":135},[137,1403,1405],{"className":139,"code":1404,"language":135,"meta":141,"style":141},"-- Circular FK: a has FK to b, b has FK to a. Neither can be inserted first\n-- without deferring one of them.\nCREATE TABLE a (\n  id    BIGSERIAL PRIMARY KEY,\n  b_id  BIGINT,\n  CONSTRAINT a_b_fk FOREIGN KEY (b_id) REFERENCES b(id)\n    DEFERRABLE INITIALLY DEFERRED    -- check at COMMIT, not at INSERT\n);\n\nCREATE TABLE b (\n  id    BIGSERIAL PRIMARY KEY,\n  a_id  BIGINT,\n  CONSTRAINT b_a_fk FOREIGN KEY (a_id) REFERENCES a(id)\n    DEFERRABLE INITIALLY DEFERRED\n);\n\n-- Now you can insert in any order within a transaction:\nBEGIN;\nINSERT INTO a (id, b_id) VALUES (1, 1);\nINSERT INTO b (id, a_id) VALUES (1, 1);\nCOMMIT;  -- FK checks happen here, both references satisfied\n\n-- You can also defer a DEFERRABLE constraint per-transaction:\nSET CONSTRAINTS a_b_fk DEFERRED;\n",[50,1406,1407,1412,1417,1428,1438,1447,1464,1472,1476,1480,1491,1501,1510,1527,1532,1536,1540,1545,1552,1574,1593,1605,1610,1616],{"__ignoreMap":141},[145,1408,1409],{"class":147,"line":148},[145,1410,1411],{"class":196},"-- Circular FK: a has FK to b, b has FK to a. Neither can be inserted first\n",[145,1413,1414],{"class":147,"line":166},[145,1415,1416],{"class":196},"-- without deferring one of them.\n",[145,1418,1419,1421,1423,1426],{"class":147,"line":181},[145,1420,152],{"class":151},[145,1422,155],{"class":151},[145,1424,1425],{"class":158}," a",[145,1427,163],{"class":162},[145,1429,1430,1432,1434,1436],{"class":147,"line":200},[145,1431,169],{"class":162},[145,1433,172],{"class":151},[145,1435,175],{"class":151},[145,1437,178],{"class":162},[145,1439,1440,1443,1445],{"class":147,"line":212},[145,1441,1442],{"class":162},"  b_id  ",[145,1444,678],{"class":151},[145,1446,178],{"class":162},[145,1448,1449,1451,1454,1456,1459,1461],{"class":147,"line":218},[145,1450,455],{"class":151},[145,1452,1453],{"class":162}," a_b_fk ",[145,1455,95],{"class":151},[145,1457,1458],{"class":162}," (b_id) ",[145,1460,1028],{"class":151},[145,1462,1463],{"class":162}," b(id)\n",[145,1465,1466,1469],{"class":147,"line":225},[145,1467,1468],{"class":162},"    DEFERRABLE INITIALLY DEFERRED    ",[145,1470,1471],{"class":196},"-- check at COMMIT, not at INSERT\n",[145,1473,1474],{"class":147,"line":231},[145,1475,215],{"class":162},[145,1477,1478],{"class":147,"line":237},[145,1479,222],{"emptyLinePlaceholder":221},[145,1481,1482,1484,1486,1489],{"class":147,"line":242},[145,1483,152],{"class":151},[145,1485,155],{"class":151},[145,1487,1488],{"class":158}," b",[145,1490,163],{"class":162},[145,1492,1493,1495,1497,1499],{"class":147,"line":248},[145,1494,169],{"class":162},[145,1496,172],{"class":151},[145,1498,175],{"class":151},[145,1500,178],{"class":162},[145,1502,1503,1506,1508],{"class":147,"line":275},[145,1504,1505],{"class":162},"  a_id  ",[145,1507,678],{"class":151},[145,1509,178],{"class":162},[145,1511,1512,1514,1517,1519,1522,1524],{"class":147,"line":280},[145,1513,455],{"class":151},[145,1515,1516],{"class":162}," b_a_fk ",[145,1518,95],{"class":151},[145,1520,1521],{"class":162}," (a_id) ",[145,1523,1028],{"class":151},[145,1525,1526],{"class":162}," a(id)\n",[145,1528,1529],{"class":147,"line":286},[145,1530,1531],{"class":162},"    DEFERRABLE INITIALLY DEFERRED\n",[145,1533,1534],{"class":147,"line":320},[145,1535,215],{"class":162},[145,1537,1538],{"class":147,"line":325},[145,1539,222],{"emptyLinePlaceholder":221},[145,1541,1542],{"class":147,"line":362},[145,1543,1544],{"class":196},"-- Now you can insert in any order within a transaction:\n",[145,1546,1547,1550],{"class":147,"line":378},[145,1548,1549],{"class":151},"BEGIN",[145,1551,272],{"class":162},[145,1553,1554,1557,1560,1563,1565,1568,1570,1572],{"class":147,"line":384},[145,1555,1556],{"class":151},"INSERT INTO",[145,1558,1559],{"class":162}," a (id, b_id) ",[145,1561,1562],{"class":151},"VALUES",[145,1564,345],{"class":162},[145,1566,1567],{"class":626},"1",[145,1569,759],{"class":162},[145,1571,1567],{"class":626},[145,1573,215],{"class":162},[145,1575,1576,1578,1581,1583,1585,1587,1589,1591],{"class":147,"line":1293},[145,1577,1556],{"class":151},[145,1579,1580],{"class":162}," b (id, a_id) ",[145,1582,1562],{"class":151},[145,1584,345],{"class":162},[145,1586,1567],{"class":626},[145,1588,759],{"class":162},[145,1590,1567],{"class":626},[145,1592,215],{"class":162},[145,1594,1596,1599,1602],{"class":147,"line":1595},21,[145,1597,1598],{"class":151},"COMMIT",[145,1600,1601],{"class":162},";  ",[145,1603,1604],{"class":196},"-- FK checks happen here, both references satisfied\n",[145,1606,1608],{"class":147,"line":1607},22,[145,1609,222],{"emptyLinePlaceholder":221},[145,1611,1613],{"class":147,"line":1612},23,[145,1614,1615],{"class":196},"-- You can also defer a DEFERRABLE constraint per-transaction:\n",[145,1617,1619,1621],{"class":147,"line":1618},24,[145,1620,294],{"class":151},[145,1622,1623],{"class":162}," CONSTRAINTS a_b_fk DEFERRED;\n",[18,1625,1627],{"id":1626},"check-constraints","CHECK Constraints",[133,1629,1630],{"language":135},[137,1631,1633],{"className":139,"code":1632,"language":135,"meta":141,"style":141},"CREATE TABLE products (\n  id    BIGSERIAL PRIMARY KEY,\n  price NUMERIC(10,2) NOT NULL CHECK (price >= 0),\n  qty   INTEGER NOT NULL CHECK (qty >= 0),\n  -- Table-level: can reference multiple columns.\n  CONSTRAINT valid_price_qty CHECK (price * qty >= 0)\n);\n\n-- ⚠️ CHECK passes on UNKNOWN (NULL): CHECK (price > 0) allows price = NULL\n--    because NULL > 0 is UNKNOWN, and constraints only reject FALSE.\n--    Add NOT NULL separately to forbid NULL.\n\n-- CHECK can only reference the CURRENT ROW — no subqueries, no other tables.\n-- ❌ This is invalid:\n-- CHECK (price \u003C (SELECT MAX(price) FROM products))\n-- ✅ Use a trigger for cross-row\u002Fcross-table rules.\n\n-- Adding CHECK to a large table without a long lock:\nALTER TABLE products ADD CONSTRAINT positive_price CHECK (price >= 0) NOT VALID;\nALTER TABLE products VALIDATE CONSTRAINT positive_price;\n-- NOT VALID: metadata-only, skips existing rows (instant).\n-- VALIDATE: scans with SHARE UPDATE EXCLUSIVE lock (concurrent reads\u002Fwrites continue).\n",[50,1634,1635,1646,1656,1690,1710,1715,1739,1743,1747,1752,1757,1762,1766,1771,1776,1781,1786,1790,1795,1825,1839,1844],{"__ignoreMap":141},[145,1636,1637,1639,1641,1644],{"class":147,"line":148},[145,1638,152],{"class":151},[145,1640,155],{"class":151},[145,1642,1643],{"class":158}," products",[145,1645,163],{"class":162},[145,1647,1648,1650,1652,1654],{"class":147,"line":166},[145,1649,169],{"class":162},[145,1651,172],{"class":151},[145,1653,175],{"class":151},[145,1655,178],{"class":162},[145,1657,1658,1661,1663,1665,1667,1669,1671,1673,1675,1678,1681,1684,1687],{"class":147,"line":181},[145,1659,1660],{"class":162},"  price ",[145,1662,932],{"class":151},[145,1664,623],{"class":162},[145,1666,937],{"class":626},[145,1668,940],{"class":162},[145,1670,627],{"class":626},[145,1672,353],{"class":162},[145,1674,52],{"class":151},[145,1676,1677],{"class":151}," CHECK",[145,1679,1680],{"class":162}," (price ",[145,1682,1683],{"class":151},">=",[145,1685,1686],{"class":626}," 0",[145,1688,1689],{"class":162},"),\n",[145,1691,1692,1695,1697,1699,1701,1704,1706,1708],{"class":147,"line":200},[145,1693,1694],{"class":162},"  qty   ",[145,1696,709],{"class":151},[145,1698,190],{"class":151},[145,1700,1677],{"class":151},[145,1702,1703],{"class":162}," (qty ",[145,1705,1683],{"class":151},[145,1707,1686],{"class":626},[145,1709,1689],{"class":162},[145,1711,1712],{"class":147,"line":212},[145,1713,1714],{"class":196},"  -- Table-level: can reference multiple columns.\n",[145,1716,1717,1719,1722,1724,1726,1729,1732,1734,1736],{"class":147,"line":218},[145,1718,455],{"class":151},[145,1720,1721],{"class":162}," valid_price_qty ",[145,1723,108],{"class":151},[145,1725,1680],{"class":162},[145,1727,1728],{"class":151},"*",[145,1730,1731],{"class":162}," qty ",[145,1733,1683],{"class":151},[145,1735,1686],{"class":626},[145,1737,1738],{"class":162},")\n",[145,1740,1741],{"class":147,"line":225},[145,1742,215],{"class":162},[145,1744,1745],{"class":147,"line":231},[145,1746,222],{"emptyLinePlaceholder":221},[145,1748,1749],{"class":147,"line":237},[145,1750,1751],{"class":196},"-- ⚠️ CHECK passes on UNKNOWN (NULL): CHECK (price > 0) allows price = NULL\n",[145,1753,1754],{"class":147,"line":242},[145,1755,1756],{"class":196},"--    because NULL > 0 is UNKNOWN, and constraints only reject FALSE.\n",[145,1758,1759],{"class":147,"line":248},[145,1760,1761],{"class":196},"--    Add NOT NULL separately to forbid NULL.\n",[145,1763,1764],{"class":147,"line":275},[145,1765,222],{"emptyLinePlaceholder":221},[145,1767,1768],{"class":147,"line":280},[145,1769,1770],{"class":196},"-- CHECK can only reference the CURRENT ROW — no subqueries, no other tables.\n",[145,1772,1773],{"class":147,"line":286},[145,1774,1775],{"class":196},"-- ❌ This is invalid:\n",[145,1777,1778],{"class":147,"line":320},[145,1779,1780],{"class":196},"-- CHECK (price \u003C (SELECT MAX(price) FROM products))\n",[145,1782,1783],{"class":147,"line":325},[145,1784,1785],{"class":196},"-- ✅ Use a trigger for cross-row\u002Fcross-table rules.\n",[145,1787,1788],{"class":147,"line":362},[145,1789,222],{"emptyLinePlaceholder":221},[145,1791,1792],{"class":147,"line":378},[145,1793,1794],{"class":196},"-- Adding CHECK to a large table without a long lock:\n",[145,1796,1797,1799,1801,1804,1806,1808,1811,1813,1815,1817,1819,1821,1823],{"class":147,"line":384},[145,1798,251],{"class":151},[145,1800,155],{"class":151},[145,1802,1803],{"class":162}," products ",[145,1805,334],{"class":151},[145,1807,337],{"class":151},[145,1809,1810],{"class":162}," positive_price ",[145,1812,108],{"class":151},[145,1814,1680],{"class":162},[145,1816,1683],{"class":151},[145,1818,1686],{"class":626},[145,1820,353],{"class":162},[145,1822,356],{"class":151},[145,1824,359],{"class":162},[145,1826,1827,1829,1831,1834,1836],{"class":147,"line":1293},[145,1828,251],{"class":151},[145,1830,155],{"class":151},[145,1832,1833],{"class":162}," products VALIDATE ",[145,1835,372],{"class":151},[145,1837,1838],{"class":162}," positive_price;\n",[145,1840,1841],{"class":147,"line":1595},[145,1842,1843],{"class":196},"-- NOT VALID: metadata-only, skips existing rows (instant).\n",[145,1845,1846],{"class":147,"line":1607},[145,1847,1848],{"class":196},"-- VALIDATE: scans with SHARE UPDATE EXCLUSIVE lock (concurrent reads\u002Fwrites continue).\n",[18,1850,1852],{"id":1851},"exclude-constraints-postgresql","EXCLUDE Constraints (PostgreSQL)",[14,1854,1855,1856,1858,1859,1861,1862,1315],{},"An ",[50,1857,121],{}," constraint prevents two rows from satisfying a predicate — a generalization of ",[50,1860,65],{},". The classic use is ",[827,1863,1864],{},"no overlapping time ranges",[133,1866,1867],{"language":135},[137,1868,1870],{"className":139,"code":1869,"language":135,"meta":141,"style":141},"-- Requires btree_gist extension for = operator on scalars in a GiST index.\nCREATE EXTENSION IF NOT EXISTS btree_gist;\n\nCREATE TABLE bookings (\n  id          BIGSERIAL PRIMARY KEY,\n  room_id     INTEGER NOT NULL,\n  -- TSTZRANGE: range of timestamps with timezone. '[)' = inclusive start, exclusive end.\n  during      TSTZRANGE NOT NULL,\n\n  -- EXCLUDE: no two rows where room_id is equal AND during overlaps (&&).\n  -- GiST index supports the && (overlaps) operator on range types.\n  EXCLUDE USING gist (\n    room_id WITH =,      -- equality on scalar (needs btree_gist)\n    during  WITH &&      -- overlap on range (native GiST)\n  )\n);\n\n-- This insert succeeds (same room, non-overlapping times):\nINSERT INTO bookings (room_id, during) VALUES\n  (1, '[2024-01-01 09:00, 2024-01-01 10:00)');\n\n-- This insert FAILS (same room, overlapping time):\nINSERT INTO bookings (room_id, during) VALUES\n  (1, '[2024-01-01 09:30, 2024-01-01 11:00)');\n-- ERROR: conflicting key value violates exclusion constraint\n\n-- The constraint is enforced at WRITE TIME — no race conditions,\n-- unlike application-level checks which fail under concurrent inserts.\n",[50,1871,1872,1877,1896,1900,1911,1921,1932,1937,1946,1950,1955,1960,1971,1987,2000,2005,2009,2013,2018,2028,2042,2046,2051,2059,2072,2078,2083,2089],{"__ignoreMap":141},[145,1873,1874],{"class":147,"line":148},[145,1875,1876],{"class":196},"-- Requires btree_gist extension for = operator on scalars in a GiST index.\n",[145,1878,1879,1881,1884,1887,1890,1893],{"class":147,"line":166},[145,1880,152],{"class":151},[145,1882,1883],{"class":162}," EXTENSION ",[145,1885,1886],{"class":151},"IF",[145,1888,1889],{"class":151}," NOT",[145,1891,1892],{"class":151}," EXISTS",[145,1894,1895],{"class":162}," btree_gist;\n",[145,1897,1898],{"class":147,"line":181},[145,1899,222],{"emptyLinePlaceholder":221},[145,1901,1902,1904,1906,1909],{"class":147,"line":200},[145,1903,152],{"class":151},[145,1905,155],{"class":151},[145,1907,1908],{"class":158}," bookings",[145,1910,163],{"class":162},[145,1912,1913,1915,1917,1919],{"class":147,"line":212},[145,1914,904],{"class":162},[145,1916,172],{"class":151},[145,1918,175],{"class":151},[145,1920,178],{"class":162},[145,1922,1923,1926,1928,1930],{"class":147,"line":218},[145,1924,1925],{"class":162},"  room_id     ",[145,1927,709],{"class":151},[145,1929,190],{"class":151},[145,1931,178],{"class":162},[145,1933,1934],{"class":147,"line":225},[145,1935,1936],{"class":196},"  -- TSTZRANGE: range of timestamps with timezone. '[)' = inclusive start, exclusive end.\n",[145,1938,1939,1942,1944],{"class":147,"line":231},[145,1940,1941],{"class":162},"  during      TSTZRANGE ",[145,1943,52],{"class":151},[145,1945,178],{"class":162},[145,1947,1948],{"class":147,"line":237},[145,1949,222],{"emptyLinePlaceholder":221},[145,1951,1952],{"class":147,"line":242},[145,1953,1954],{"class":196},"  -- EXCLUDE: no two rows where room_id is equal AND during overlaps (&&).\n",[145,1956,1957],{"class":147,"line":248},[145,1958,1959],{"class":196},"  -- GiST index supports the && (overlaps) operator on range types.\n",[145,1961,1962,1965,1968],{"class":147,"line":275},[145,1963,1964],{"class":162},"  EXCLUDE ",[145,1966,1967],{"class":151},"USING",[145,1969,1970],{"class":162}," gist (\n",[145,1972,1973,1976,1979,1981,1984],{"class":147,"line":280},[145,1974,1975],{"class":162},"    room_id ",[145,1977,1978],{"class":151},"WITH",[145,1980,300],{"class":151},[145,1982,1983],{"class":162},",      ",[145,1985,1986],{"class":196},"-- equality on scalar (needs btree_gist)\n",[145,1988,1989,1992,1994,1997],{"class":147,"line":286},[145,1990,1991],{"class":162},"    during  ",[145,1993,1978],{"class":151},[145,1995,1996],{"class":162}," &&      ",[145,1998,1999],{"class":196},"-- overlap on range (native GiST)\n",[145,2001,2002],{"class":147,"line":320},[145,2003,2004],{"class":162},"  )\n",[145,2006,2007],{"class":147,"line":325},[145,2008,215],{"class":162},[145,2010,2011],{"class":147,"line":362},[145,2012,222],{"emptyLinePlaceholder":221},[145,2014,2015],{"class":147,"line":378},[145,2016,2017],{"class":196},"-- This insert succeeds (same room, non-overlapping times):\n",[145,2019,2020,2022,2025],{"class":147,"line":384},[145,2021,1556],{"class":151},[145,2023,2024],{"class":162}," bookings (room_id, during) ",[145,2026,2027],{"class":151},"VALUES\n",[145,2029,2030,2033,2035,2037,2040],{"class":147,"line":1293},[145,2031,2032],{"class":162},"  (",[145,2034,1567],{"class":626},[145,2036,759],{"class":162},[145,2038,2039],{"class":303},"'[2024-01-01 09:00, 2024-01-01 10:00)'",[145,2041,215],{"class":162},[145,2043,2044],{"class":147,"line":1595},[145,2045,222],{"emptyLinePlaceholder":221},[145,2047,2048],{"class":147,"line":1607},[145,2049,2050],{"class":196},"-- This insert FAILS (same room, overlapping time):\n",[145,2052,2053,2055,2057],{"class":147,"line":1612},[145,2054,1556],{"class":151},[145,2056,2024],{"class":162},[145,2058,2027],{"class":151},[145,2060,2061,2063,2065,2067,2070],{"class":147,"line":1618},[145,2062,2032],{"class":162},[145,2064,1567],{"class":626},[145,2066,759],{"class":162},[145,2068,2069],{"class":303},"'[2024-01-01 09:30, 2024-01-01 11:00)'",[145,2071,215],{"class":162},[145,2073,2075],{"class":147,"line":2074},25,[145,2076,2077],{"class":196},"-- ERROR: conflicting key value violates exclusion constraint\n",[145,2079,2081],{"class":147,"line":2080},26,[145,2082,222],{"emptyLinePlaceholder":221},[145,2084,2086],{"class":147,"line":2085},27,[145,2087,2088],{"class":196},"-- The constraint is enforced at WRITE TIME — no race conditions,\n",[145,2090,2092],{"class":147,"line":2091},28,[145,2093,2094],{"class":196},"-- unlike application-level checks which fail under concurrent inserts.\n",[18,2096,2098],{"id":2097},"domain-constraints-postgresql","Domain Constraints (PostgreSQL)",[14,2100,527,2101,2104],{},[827,2102,2103],{},"domain"," is a custom type with constraints, reusable across columns:",[133,2106,2107],{"language":135},[137,2108,2110],{"className":139,"code":2109,"language":135,"meta":141,"style":141},"CREATE DOMAIN email_t AS TEXT\n  CHECK (value ~ '^[^@]+@[^@]+\\.[^@]+$');\n\nCREATE DOMAIN positive_int AS INTEGER\n  CHECK (value > 0);\n\nCREATE TABLE users (\n  id    BIGSERIAL PRIMARY KEY,\n  email email_t NOT NULL,           -- reuses the domain constraint\n  age   positive_int                -- reuses the domain constraint\n);\n\n-- Domains can be altered: ALTER DOMAIN email_t ADD CONSTRAINT ...;\n-- Changes propagate to all columns using the domain.\n",[50,2111,2112,2128,2146,2150,2164,2179,2183,2193,2203,2216,2223,2227,2231,2236],{"__ignoreMap":141},[145,2113,2114,2116,2119,2122,2125],{"class":147,"line":148},[145,2115,152],{"class":151},[145,2117,2118],{"class":151}," DOMAIN",[145,2120,2121],{"class":158}," email_t",[145,2123,2124],{"class":151}," AS",[145,2126,2127],{"class":151}," TEXT\n",[145,2129,2130,2133,2135,2138,2141,2144],{"class":147,"line":166},[145,2131,2132],{"class":151},"  CHECK",[145,2134,345],{"class":162},[145,2136,2137],{"class":151},"value",[145,2139,2140],{"class":162}," ~ ",[145,2142,2143],{"class":303},"'^[^@]+@[^@]+\\.[^@]+$'",[145,2145,215],{"class":162},[145,2147,2148],{"class":147,"line":181},[145,2149,222],{"emptyLinePlaceholder":221},[145,2151,2152,2154,2156,2159,2161],{"class":147,"line":200},[145,2153,152],{"class":151},[145,2155,2118],{"class":151},[145,2157,2158],{"class":158}," positive_int",[145,2160,2124],{"class":151},[145,2162,2163],{"class":151}," INTEGER\n",[145,2165,2166,2168,2170,2172,2175,2177],{"class":147,"line":212},[145,2167,2132],{"class":151},[145,2169,345],{"class":162},[145,2171,2137],{"class":151},[145,2173,2174],{"class":151}," >",[145,2176,1686],{"class":626},[145,2178,215],{"class":162},[145,2180,2181],{"class":147,"line":218},[145,2182,222],{"emptyLinePlaceholder":221},[145,2184,2185,2187,2189,2191],{"class":147,"line":225},[145,2186,152],{"class":151},[145,2188,155],{"class":151},[145,2190,159],{"class":158},[145,2192,163],{"class":162},[145,2194,2195,2197,2199,2201],{"class":147,"line":231},[145,2196,169],{"class":162},[145,2198,172],{"class":151},[145,2200,175],{"class":151},[145,2202,178],{"class":162},[145,2204,2205,2208,2210,2213],{"class":147,"line":237},[145,2206,2207],{"class":162},"  email email_t ",[145,2209,52],{"class":151},[145,2211,2212],{"class":162},",           ",[145,2214,2215],{"class":196},"-- reuses the domain constraint\n",[145,2217,2218,2221],{"class":147,"line":242},[145,2219,2220],{"class":162},"  age   positive_int                ",[145,2222,2215],{"class":196},[145,2224,2225],{"class":147,"line":248},[145,2226,215],{"class":162},[145,2228,2229],{"class":147,"line":275},[145,2230,222],{"emptyLinePlaceholder":221},[145,2232,2233],{"class":147,"line":280},[145,2234,2235],{"class":196},"-- Domains can be altered: ALTER DOMAIN email_t ADD CONSTRAINT ...;\n",[145,2237,2238],{"class":147,"line":286},[145,2239,2240],{"class":196},"-- Changes propagate to all columns using the domain.\n",[18,2242,2244],{"id":2243},"complex-implementation-referential-integrity-with-cascades-checks-and-exclude","Complex Implementation: Referential Integrity with Cascades, CHECKs, and EXCLUDE",[133,2246,2247],{"language":135},[137,2248,2250],{"className":139,"code":2249,"language":135,"meta":141,"style":141},"-- A hotel booking system with:\n-- - FK cascades for composition (booking → booking_services)\n-- - FK RESTRICT for soft relationships (guests → bookings)\n-- - CHECK constraints for business rules\n-- - EXCLUDE for no overlapping room bookings\n\nCREATE TABLE guests (\n  id          BIGSERIAL PRIMARY KEY,\n  email       TEXT NOT NULL,\n  name        TEXT NOT NULL,\n  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),\n  CONSTRAINT guests_email_unique UNIQUE (email),\n  CONSTRAINT guests_email_format CHECK (email ~ '^[^@]+@[^@]+\\.[^@]+$')\n);\n\nCREATE TABLE rooms (\n  id       BIGSERIAL PRIMARY KEY,\n  floor    INTEGER NOT NULL,\n  capacity INTEGER NOT NULL,\n  CONSTRAINT rooms_floor CHECK (floor >= 0),\n  CONSTRAINT rooms_capacity CHECK (capacity > 0 AND capacity \u003C= 10)\n);\n\nCREATE TABLE bookings (\n  id         BIGSERIAL PRIMARY KEY,\n  guest_id   BIGINT NOT NULL,\n  room_id    BIGINT NOT NULL,\n  during     TSTZRANGE NOT NULL,\n  -- Business rule: checkout must be after checkin (enforced by range type, but\n  -- explicit CHECK gives a clearer error message).\n  status     TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed','checked_in','checked_out','cancelled')),\n  total      NUMERIC(10,2) NOT NULL DEFAULT 0 CHECK (total >= 0),\n  -- FK: don't delete a guest with bookings (archive instead).\n  CONSTRAINT bookings_guest_fk FOREIGN KEY (guest_id) REFERENCES guests(id) ON DELETE RESTRICT,\n  -- FK: don't delete a room with bookings.\n  CONSTRAINT bookings_room_fk FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE RESTRICT,\n  -- EXCLUDE: no two active bookings for the same room with overlapping dates.\n  -- Only applies to non-cancelled bookings (partial exclusion via WHERE on the constraint).\n  EXCLUDE USING gist (\n    room_id WITH =,\n    during  WITH &&\n  ) WHERE (status \u003C> 'cancelled')\n);\n\nCREATE INDEX bookings_guest_id_idx ON bookings(guest_id);\nCREATE INDEX bookings_room_id_idx  ON bookings(room_id);\n\n-- Booking services: true composition — cascade delete with parent booking.\nCREATE TABLE booking_services (\n  id          BIGSERIAL PRIMARY KEY,\n  booking_id  BIGINT NOT NULL,\n  service_name TEXT NOT NULL,\n  price       NUMERIC(10,2) NOT NULL CHECK (price >= 0),\n  CONSTRAINT services_booking_fk\n    FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE CASCADE\n);\n\nCREATE INDEX booking_services_booking_id_idx ON booking_services(booking_id);\n",[50,2251,2252,2257,2262,2267,2272,2277,2281,2292,2302,2313,2323,2342,2354,2370,2374,2378,2389,2400,2411,2422,2440,2471,2475,2479,2489,2499,2510,2521,2530,2536,2542,2591,2626,2632,2655,2661,2683,2689,2695,2704,2715,2725,2745,2750,2755,2770,2785,2790,2796,2808,2819,2831,2844,2874,2882,2897,2902,2907],{"__ignoreMap":141},[145,2253,2254],{"class":147,"line":148},[145,2255,2256],{"class":196},"-- A hotel booking system with:\n",[145,2258,2259],{"class":147,"line":166},[145,2260,2261],{"class":196},"-- - FK cascades for composition (booking → booking_services)\n",[145,2263,2264],{"class":147,"line":181},[145,2265,2266],{"class":196},"-- - FK RESTRICT for soft relationships (guests → bookings)\n",[145,2268,2269],{"class":147,"line":200},[145,2270,2271],{"class":196},"-- - CHECK constraints for business rules\n",[145,2273,2274],{"class":147,"line":212},[145,2275,2276],{"class":196},"-- - EXCLUDE for no overlapping room bookings\n",[145,2278,2279],{"class":147,"line":218},[145,2280,222],{"emptyLinePlaceholder":221},[145,2282,2283,2285,2287,2290],{"class":147,"line":225},[145,2284,152],{"class":151},[145,2286,155],{"class":151},[145,2288,2289],{"class":158}," guests",[145,2291,163],{"class":162},[145,2293,2294,2296,2298,2300],{"class":147,"line":231},[145,2295,904],{"class":162},[145,2297,172],{"class":151},[145,2299,175],{"class":151},[145,2301,178],{"class":162},[145,2303,2304,2307,2309,2311],{"class":147,"line":237},[145,2305,2306],{"class":162},"  email       ",[145,2308,187],{"class":151},[145,2310,190],{"class":151},[145,2312,178],{"class":162},[145,2314,2315,2317,2319,2321],{"class":147,"line":242},[145,2316,203],{"class":151},[145,2318,644],{"class":151},[145,2320,190],{"class":151},[145,2322,178],{"class":162},[145,2324,2325,2328,2331,2333,2336,2339],{"class":147,"line":248},[145,2326,2327],{"class":162},"  created_at  ",[145,2329,2330],{"class":151},"TIMESTAMPTZ",[145,2332,190],{"class":151},[145,2334,2335],{"class":151}," DEFAULT",[145,2337,2338],{"class":151}," now",[145,2340,2341],{"class":162},"(),\n",[145,2343,2344,2346,2349,2351],{"class":147,"line":275},[145,2345,455],{"class":151},[145,2347,2348],{"class":162}," guests_email_unique ",[145,2350,65],{"class":151},[145,2352,2353],{"class":162}," (email),\n",[145,2355,2356,2358,2361,2363,2366,2368],{"class":147,"line":280},[145,2357,455],{"class":151},[145,2359,2360],{"class":162}," guests_email_format ",[145,2362,108],{"class":151},[145,2364,2365],{"class":162}," (email ~ ",[145,2367,2143],{"class":303},[145,2369,1738],{"class":162},[145,2371,2372],{"class":147,"line":286},[145,2373,215],{"class":162},[145,2375,2376],{"class":147,"line":320},[145,2377,222],{"emptyLinePlaceholder":221},[145,2379,2380,2382,2384,2387],{"class":147,"line":325},[145,2381,152],{"class":151},[145,2383,155],{"class":151},[145,2385,2386],{"class":158}," rooms",[145,2388,163],{"class":162},[145,2390,2391,2394,2396,2398],{"class":147,"line":362},[145,2392,2393],{"class":162},"  id       ",[145,2395,172],{"class":151},[145,2397,175],{"class":151},[145,2399,178],{"class":162},[145,2401,2402,2405,2407,2409],{"class":147,"line":378},[145,2403,2404],{"class":162},"  floor    ",[145,2406,709],{"class":151},[145,2408,190],{"class":151},[145,2410,178],{"class":162},[145,2412,2413,2416,2418,2420],{"class":147,"line":384},[145,2414,2415],{"class":162},"  capacity ",[145,2417,709],{"class":151},[145,2419,190],{"class":151},[145,2421,178],{"class":162},[145,2423,2424,2426,2429,2431,2434,2436,2438],{"class":147,"line":1293},[145,2425,455],{"class":151},[145,2427,2428],{"class":162}," rooms_floor ",[145,2430,108],{"class":151},[145,2432,2433],{"class":162}," (floor ",[145,2435,1683],{"class":151},[145,2437,1686],{"class":626},[145,2439,1689],{"class":162},[145,2441,2442,2444,2447,2449,2452,2455,2457,2460,2463,2466,2469],{"class":147,"line":1595},[145,2443,455],{"class":151},[145,2445,2446],{"class":162}," rooms_capacity ",[145,2448,108],{"class":151},[145,2450,2451],{"class":162}," (capacity ",[145,2453,2454],{"class":151},">",[145,2456,1686],{"class":626},[145,2458,2459],{"class":151}," AND",[145,2461,2462],{"class":162}," capacity ",[145,2464,2465],{"class":151},"\u003C=",[145,2467,2468],{"class":626}," 10",[145,2470,1738],{"class":162},[145,2472,2473],{"class":147,"line":1607},[145,2474,215],{"class":162},[145,2476,2477],{"class":147,"line":1612},[145,2478,222],{"emptyLinePlaceholder":221},[145,2480,2481,2483,2485,2487],{"class":147,"line":1618},[145,2482,152],{"class":151},[145,2484,155],{"class":151},[145,2486,1908],{"class":158},[145,2488,163],{"class":162},[145,2490,2491,2493,2495,2497],{"class":147,"line":2074},[145,2492,1198],{"class":162},[145,2494,172],{"class":151},[145,2496,175],{"class":151},[145,2498,178],{"class":162},[145,2500,2501,2504,2506,2508],{"class":147,"line":2080},[145,2502,2503],{"class":162},"  guest_id   ",[145,2505,678],{"class":151},[145,2507,190],{"class":151},[145,2509,178],{"class":162},[145,2511,2512,2515,2517,2519],{"class":147,"line":2085},[145,2513,2514],{"class":162},"  room_id    ",[145,2516,678],{"class":151},[145,2518,190],{"class":151},[145,2520,178],{"class":162},[145,2522,2523,2526,2528],{"class":147,"line":2091},[145,2524,2525],{"class":162},"  during     TSTZRANGE ",[145,2527,52],{"class":151},[145,2529,178],{"class":162},[145,2531,2533],{"class":147,"line":2532},29,[145,2534,2535],{"class":196},"  -- Business rule: checkout must be after checkin (enforced by range type, but\n",[145,2537,2539],{"class":147,"line":2538},30,[145,2540,2541],{"class":196},"  -- explicit CHECK gives a clearer error message).\n",[145,2543,2545,2548,2551,2553,2555,2558,2560,2562,2565,2568,2570,2573,2575,2578,2580,2583,2585,2588],{"class":147,"line":2544},31,[145,2546,2547],{"class":151},"  status",[145,2549,2550],{"class":151},"     TEXT",[145,2552,190],{"class":151},[145,2554,2335],{"class":151},[145,2556,2557],{"class":303}," 'confirmed'",[145,2559,1677],{"class":151},[145,2561,345],{"class":162},[145,2563,2564],{"class":151},"status",[145,2566,2567],{"class":151}," IN",[145,2569,345],{"class":162},[145,2571,2572],{"class":303},"'confirmed'",[145,2574,940],{"class":162},[145,2576,2577],{"class":303},"'checked_in'",[145,2579,940],{"class":162},[145,2581,2582],{"class":303},"'checked_out'",[145,2584,940],{"class":162},[145,2586,2587],{"class":303},"'cancelled'",[145,2589,2590],{"class":162},")),\n",[145,2592,2594,2597,2599,2601,2603,2605,2607,2609,2611,2613,2615,2617,2620,2622,2624],{"class":147,"line":2593},32,[145,2595,2596],{"class":162},"  total      ",[145,2598,932],{"class":151},[145,2600,623],{"class":162},[145,2602,937],{"class":626},[145,2604,940],{"class":162},[145,2606,627],{"class":626},[145,2608,353],{"class":162},[145,2610,52],{"class":151},[145,2612,2335],{"class":151},[145,2614,1686],{"class":626},[145,2616,1677],{"class":151},[145,2618,2619],{"class":162}," (total ",[145,2621,1683],{"class":151},[145,2623,1686],{"class":626},[145,2625,1689],{"class":162},[145,2627,2629],{"class":147,"line":2628},33,[145,2630,2631],{"class":196},"  -- FK: don't delete a guest with bookings (archive instead).\n",[145,2633,2635,2637,2640,2642,2645,2647,2650,2652],{"class":147,"line":2634},34,[145,2636,455],{"class":151},[145,2638,2639],{"class":162}," bookings_guest_fk ",[145,2641,95],{"class":151},[145,2643,2644],{"class":162}," (guest_id) ",[145,2646,1028],{"class":151},[145,2648,2649],{"class":162}," guests(id) ",[145,2651,1280],{"class":151},[145,2653,2654],{"class":162}," RESTRICT,\n",[145,2656,2658],{"class":147,"line":2657},35,[145,2659,2660],{"class":196},"  -- FK: don't delete a room with bookings.\n",[145,2662,2664,2666,2669,2671,2674,2676,2679,2681],{"class":147,"line":2663},36,[145,2665,455],{"class":151},[145,2667,2668],{"class":162}," bookings_room_fk ",[145,2670,95],{"class":151},[145,2672,2673],{"class":162}," (room_id) ",[145,2675,1028],{"class":151},[145,2677,2678],{"class":162}," rooms(id) ",[145,2680,1280],{"class":151},[145,2682,2654],{"class":162},[145,2684,2686],{"class":147,"line":2685},37,[145,2687,2688],{"class":196},"  -- EXCLUDE: no two active bookings for the same room with overlapping dates.\n",[145,2690,2692],{"class":147,"line":2691},38,[145,2693,2694],{"class":196},"  -- Only applies to non-cancelled bookings (partial exclusion via WHERE on the constraint).\n",[145,2696,2698,2700,2702],{"class":147,"line":2697},39,[145,2699,1964],{"class":162},[145,2701,1967],{"class":151},[145,2703,1970],{"class":162},[145,2705,2707,2709,2711,2713],{"class":147,"line":2706},40,[145,2708,1975],{"class":162},[145,2710,1978],{"class":151},[145,2712,300],{"class":151},[145,2714,178],{"class":162},[145,2716,2718,2720,2722],{"class":147,"line":2717},41,[145,2719,1991],{"class":162},[145,2721,1978],{"class":151},[145,2723,2724],{"class":162}," &&\n",[145,2726,2728,2731,2733,2735,2737,2740,2743],{"class":147,"line":2727},42,[145,2729,2730],{"class":162},"  ) ",[145,2732,516],{"class":151},[145,2734,345],{"class":162},[145,2736,2564],{"class":151},[145,2738,2739],{"class":151}," \u003C>",[145,2741,2742],{"class":303}," 'cancelled'",[145,2744,1738],{"class":162},[145,2746,2748],{"class":147,"line":2747},43,[145,2749,215],{"class":162},[145,2751,2753],{"class":147,"line":2752},44,[145,2754,222],{"emptyLinePlaceholder":221},[145,2756,2758,2760,2762,2765,2767],{"class":147,"line":2757},45,[145,2759,152],{"class":151},[145,2761,1379],{"class":151},[145,2763,2764],{"class":158}," bookings_guest_id_idx",[145,2766,1385],{"class":151},[145,2768,2769],{"class":162}," bookings(guest_id);\n",[145,2771,2773,2775,2777,2780,2782],{"class":147,"line":2772},46,[145,2774,152],{"class":151},[145,2776,1379],{"class":151},[145,2778,2779],{"class":158}," bookings_room_id_idx",[145,2781,510],{"class":151},[145,2783,2784],{"class":162}," bookings(room_id);\n",[145,2786,2788],{"class":147,"line":2787},47,[145,2789,222],{"emptyLinePlaceholder":221},[145,2791,2793],{"class":147,"line":2792},48,[145,2794,2795],{"class":196},"-- Booking services: true composition — cascade delete with parent booking.\n",[145,2797,2799,2801,2803,2806],{"class":147,"line":2798},49,[145,2800,152],{"class":151},[145,2802,155],{"class":151},[145,2804,2805],{"class":158}," booking_services",[145,2807,163],{"class":162},[145,2809,2811,2813,2815,2817],{"class":147,"line":2810},50,[145,2812,904],{"class":162},[145,2814,172],{"class":151},[145,2816,175],{"class":151},[145,2818,178],{"class":162},[145,2820,2822,2825,2827,2829],{"class":147,"line":2821},51,[145,2823,2824],{"class":162},"  booking_id  ",[145,2826,678],{"class":151},[145,2828,190],{"class":151},[145,2830,178],{"class":162},[145,2832,2834,2837,2840,2842],{"class":147,"line":2833},52,[145,2835,2836],{"class":151},"  service_name",[145,2838,2839],{"class":151}," TEXT",[145,2841,190],{"class":151},[145,2843,178],{"class":162},[145,2845,2847,2850,2852,2854,2856,2858,2860,2862,2864,2866,2868,2870,2872],{"class":147,"line":2846},53,[145,2848,2849],{"class":162},"  price       ",[145,2851,932],{"class":151},[145,2853,623],{"class":162},[145,2855,937],{"class":626},[145,2857,940],{"class":162},[145,2859,627],{"class":626},[145,2861,353],{"class":162},[145,2863,52],{"class":151},[145,2865,1677],{"class":151},[145,2867,1680],{"class":162},[145,2869,1683],{"class":151},[145,2871,1686],{"class":626},[145,2873,1689],{"class":162},[145,2875,2877,2879],{"class":147,"line":2876},54,[145,2878,455],{"class":151},[145,2880,2881],{"class":162}," services_booking_fk\n",[145,2883,2885,2887,2890,2892,2895],{"class":147,"line":2884},55,[145,2886,1022],{"class":151},[145,2888,2889],{"class":162}," (booking_id) ",[145,2891,1028],{"class":151},[145,2893,2894],{"class":162}," bookings(id) ",[145,2896,1175],{"class":151},[145,2898,2900],{"class":147,"line":2899},56,[145,2901,215],{"class":162},[145,2903,2905],{"class":147,"line":2904},57,[145,2906,222],{"emptyLinePlaceholder":221},[145,2908,2910,2912,2914,2917,2919],{"class":147,"line":2909},58,[145,2911,152],{"class":151},[145,2913,1379],{"class":151},[145,2915,2916],{"class":158}," booking_services_booking_id_idx",[145,2918,1385],{"class":151},[145,2920,2921],{"class":162}," booking_services(booking_id);\n",[18,2923,2925],{"id":2924},"anti-pattern-application-level-validation-instead-of-database-constraints","Anti-Pattern: Application-Level Validation Instead of Database Constraints",[133,2927,2928],{"language":135},[137,2929,2931],{"className":139,"code":2930,"language":135,"meta":141,"style":141},"-- ❌ WRONG: checking uniqueness in application code.\n-- Two concurrent requests both check: \"does email exist?\" → both see \"no\" → both insert.\n-- This is a TOCTOU (time-of-check-time-of-use) race condition.\n\n-- Pseudo-code (application layer):\n--   if not db.query(\"SELECT 1 FROM users WHERE email = ?\", email):\n--       db.execute(\"INSERT INTO users (email) VALUES (?)\", email)\n--   else:\n--       raise \"email already exists\"\n\n-- Under concurrency:\n--   T1: SELECT 1 FROM users WHERE email = 'a@x.com' → 0 rows (no)\n--   T2: SELECT 1 FROM users WHERE email = 'a@x.com' → 0 rows (no)\n--   T1: INSERT INTO users (email) VALUES ('a@x.com') → success\n--   T2: INSERT INTO users (email) VALUES ('a@x.com') → success → DUPLICATE!\n\n-- ✅ CORRECT: enforce in the database with a UNIQUE constraint.\nALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);\n\n-- Now the second INSERT fails atomically, regardless of timing:\n--   T2: INSERT INTO users (email) VALUES ('a@x.com')\n--   → ERROR: duplicate key value violates unique constraint \"users_email_unique\"\n\n-- Application catches the error and returns \"email already exists.\"\n-- No race condition possible — the constraint is checked at write time under a lock.\n",[50,2932,2933,2938,2943,2948,2952,2957,2962,2967,2972,2977,2981,2986,2991,2996,3001,3006,3010,3015,3035,3039,3044,3049,3054,3058,3063],{"__ignoreMap":141},[145,2934,2935],{"class":147,"line":148},[145,2936,2937],{"class":196},"-- ❌ WRONG: checking uniqueness in application code.\n",[145,2939,2940],{"class":147,"line":166},[145,2941,2942],{"class":196},"-- Two concurrent requests both check: \"does email exist?\" → both see \"no\" → both insert.\n",[145,2944,2945],{"class":147,"line":181},[145,2946,2947],{"class":196},"-- This is a TOCTOU (time-of-check-time-of-use) race condition.\n",[145,2949,2950],{"class":147,"line":200},[145,2951,222],{"emptyLinePlaceholder":221},[145,2953,2954],{"class":147,"line":212},[145,2955,2956],{"class":196},"-- Pseudo-code (application layer):\n",[145,2958,2959],{"class":147,"line":218},[145,2960,2961],{"class":196},"--   if not db.query(\"SELECT 1 FROM users WHERE email = ?\", email):\n",[145,2963,2964],{"class":147,"line":225},[145,2965,2966],{"class":196},"--       db.execute(\"INSERT INTO users (email) VALUES (?)\", email)\n",[145,2968,2969],{"class":147,"line":231},[145,2970,2971],{"class":196},"--   else:\n",[145,2973,2974],{"class":147,"line":237},[145,2975,2976],{"class":196},"--       raise \"email already exists\"\n",[145,2978,2979],{"class":147,"line":242},[145,2980,222],{"emptyLinePlaceholder":221},[145,2982,2983],{"class":147,"line":248},[145,2984,2985],{"class":196},"-- Under concurrency:\n",[145,2987,2988],{"class":147,"line":275},[145,2989,2990],{"class":196},"--   T1: SELECT 1 FROM users WHERE email = 'a@x.com' → 0 rows (no)\n",[145,2992,2993],{"class":147,"line":280},[145,2994,2995],{"class":196},"--   T2: SELECT 1 FROM users WHERE email = 'a@x.com' → 0 rows (no)\n",[145,2997,2998],{"class":147,"line":286},[145,2999,3000],{"class":196},"--   T1: INSERT INTO users (email) VALUES ('a@x.com') → success\n",[145,3002,3003],{"class":147,"line":320},[145,3004,3005],{"class":196},"--   T2: INSERT INTO users (email) VALUES ('a@x.com') → success → DUPLICATE!\n",[145,3007,3008],{"class":147,"line":325},[145,3009,222],{"emptyLinePlaceholder":221},[145,3011,3012],{"class":147,"line":362},[145,3013,3014],{"class":196},"-- ✅ CORRECT: enforce in the database with a UNIQUE constraint.\n",[145,3016,3017,3019,3021,3023,3025,3027,3030,3032],{"class":147,"line":378},[145,3018,251],{"class":151},[145,3020,155],{"class":151},[145,3022,256],{"class":162},[145,3024,334],{"class":151},[145,3026,337],{"class":151},[145,3028,3029],{"class":162}," users_email_unique ",[145,3031,65],{"class":151},[145,3033,3034],{"class":162}," (email);\n",[145,3036,3037],{"class":147,"line":384},[145,3038,222],{"emptyLinePlaceholder":221},[145,3040,3041],{"class":147,"line":1293},[145,3042,3043],{"class":196},"-- Now the second INSERT fails atomically, regardless of timing:\n",[145,3045,3046],{"class":147,"line":1595},[145,3047,3048],{"class":196},"--   T2: INSERT INTO users (email) VALUES ('a@x.com')\n",[145,3050,3051],{"class":147,"line":1607},[145,3052,3053],{"class":196},"--   → ERROR: duplicate key value violates unique constraint \"users_email_unique\"\n",[145,3055,3056],{"class":147,"line":1612},[145,3057,222],{"emptyLinePlaceholder":221},[145,3059,3060],{"class":147,"line":1618},[145,3061,3062],{"class":196},"-- Application catches the error and returns \"email already exists.\"\n",[145,3064,3065],{"class":147,"line":2074},[145,3066,3067],{"class":196},"-- No race condition possible — the constraint is checked at write time under a lock.\n",[18,3069,3071],{"id":3070},"tips-tricks","💡 Tips & Tricks",[1324,3073,3074,3089,3107,3119,3128,3145,3151],{},[1327,3075,3076,3079,3080,748,3082,3084,3085,3088],{},[827,3077,3078],{},"Idiom — always index FK columns",": the FK constraint doesn't create an index on the referencing column. Without it, ",[50,3081,1219],{},[50,3083,1107],{}," and parent-table joins do full scans. ",[50,3086,3087],{},"CREATE INDEX ON orders(customer_id)"," is a near-universal best practice.",[1327,3090,3091,3100,3101,3103,3104,3106],{},[827,3092,3093,3094,3097,3098],{},"Idiom — ",[50,3095,3096],{},"ON DELETE RESTRICT"," (default) vs ",[50,3099,1097],{},": use ",[50,3102,1087],{}," when the child shouldn't vanish with the parent (a user's orders shouldn't disappear — archive instead). Use ",[50,3105,1097],{}," only for true composition (an order's items).",[1327,3108,3109,3112,3113,3115,3116,3118],{},[827,3110,3111],{},"Idiom — deferrable constraints for circular references",": ",[50,3114,1398],{}," postpones FK checks to ",[50,3117,1598],{},", allowing inserts in any order within a transaction. Essential for circular FK graphs.",[1327,3120,3121,3112,3124,3127],{},[827,3122,3123],{},"Idiom — EXCLUDE for temporal\u002Frange data",[50,3125,3126],{},"EXCLUDE USING gist (room_id WITH =, during WITH &&)"," prevents overlapping bookings at write time — no race conditions, far more reliable than application-level checks.",[1327,3129,3130,3112,3133,3136,3137,3140,3141,3144],{},[827,3131,3132],{},"Performance — NOT VALID + VALIDATE for large tables",[50,3134,3135],{},"ADD CONSTRAINT ... NOT VALID"," is metadata-only (no scan). ",[50,3138,3139],{},"VALIDATE"," scans with ",[50,3142,3143],{},"SHARE UPDATE EXCLUSIVE"," (concurrent reads\u002Fwrites continue). Standard for zero-downtime migrations.",[1327,3146,3147,3150],{},[827,3148,3149],{},"Idiom — name every constraint",": named constraints give readable errors and can be dropped\u002Freplaced by name. Auto-generated names are brittle across schema diff tools.",[1327,3152,3153,3112,3156,3159,3160,3162],{},[827,3154,3155],{},"Idiom — partial unique index for \"at most one NULL\"",[50,3157,3158],{},"CREATE UNIQUE INDEX ... WHERE col IS NOT NULL"," enforces uniqueness only on non-NULL rows, since ",[50,3161,65],{}," constraints allow multiple NULLs.",[18,3164,3166],{"id":3165},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[1324,3168,3169,3185,3215,3228,3243,3253,3273,3285,3299,3308,3326],{},[1327,3170,3171,3176,3177,3180,3181,3184],{},[827,3172,3173,3175],{},[50,3174,65],{}," allows multiple NULLs"," (PG, standard; SQL Server allows one): two NULL rows don't violate uniqueness because ",[50,3178,3179],{},"NULL != NULL"," in three-valued logic. Use a partial unique index (",[50,3182,3183],{},"WHERE col IS NOT NULL",") for \"at most one non-NULL.\"",[1327,3186,3187,3112,3192,3195,3196,3199,3200,3203,3204,3207,3208,3211,3212,3214],{},[827,3188,3189,3191],{},[50,3190,108],{}," passes on NULL (UNKNOWN)",[50,3193,3194],{},"CHECK (price > 0)"," allows ",[50,3197,3198],{},"price = NULL"," because ",[50,3201,3202],{},"NULL > 0"," is ",[50,3205,3206],{},"UNKNOWN",", and constraints only reject ",[50,3209,3210],{},"FALSE",". Add ",[50,3213,52],{}," separately to forbid NULL.",[1327,3216,3217,3222,3223,3225,3226,1315],{},[827,3218,3219,3221],{},[50,3220,108],{}," can't reference other rows",": a ",[50,3224,108],{}," expression can only reference the current row's columns — no subqueries, no other tables. For cross-row\u002Fcross-table rules, use triggers or ",[50,3227,121],{},[1327,3229,3230,3236,3237,3239,3240,3242],{},[827,3231,3232,3235],{},[50,3233,3234],{},"ON DELETE SET DEFAULT"," requires a default",": the FK column must have a ",[50,3238,1123],{}," set, or ",[50,3241,1117],{}," fails at delete time.",[1327,3244,3245,3250,3251,1315],{},[827,3246,3247,3249],{},[50,3248,1097],{}," chains propagate through the entire FK graph",": deleting a customer can cascade to orders → order_items → audit_logs. Always review the full cascade graph before adding ",[50,3252,1097],{},[1327,3254,3255,3258,3259,759,3262,3264,3265,3268,3269,3272],{},[827,3256,3257],{},"FK columns aren't auto-indexed",": without an index on ",[50,3260,3261],{},"orders.customer_id",[50,3263,1219],{}," from ",[50,3266,3267],{},"customers"," scans all of ",[50,3270,3271],{},"orders"," to find referencing rows — and holds a lock while doing so. Always index FK columns.",[1327,3274,3275,3280,3281,3284],{},[827,3276,3277,3279],{},[50,3278,78],{}," is clustered in MySQL\u002FSQL Server, heap in PG",": in MySQL\u002FSQL Server, the PK determines physical row order. In PG, the PK is a B-tree but physical row order is heap (unless you ",[50,3282,3283],{},"CLUSTER","). This affects range-scan performance.",[1327,3286,3287,3112,3290,3292,3293,3295,3296,3298],{},[827,3288,3289],{},"Composite PK column order matters",[50,3291,857],{}," serves ",[50,3294,861],{}," (leftmost prefix) but not ",[50,3297,869],{}," alone. Order by selectivity and query pattern.",[1327,3300,3301,3307],{},[827,3302,3303,3306],{},[50,3304,3305],{},"DEFERRABLE"," must be set at creation",": you can't make a non-deferrable FK deferred mid-transaction. Plan for deferrability if you have circular references.",[1327,3309,3310,3112,3315,3318,3319,3322,3323,1315],{},[827,3311,3312,3314],{},[50,3313,121],{}," needs a matching index operator class",[50,3316,3317],{},"EXCLUDE USING gist (during WITH &&)"," requires GiST support for ",[50,3320,3321],{},"&&"," on the column's type. For scalar columns in an EXCLUDE, install ",[50,3324,3325],{},"btree_gist",[1327,3327,3328,3112,3334,3337,3338,3341,3342,83,3345,3347],{},[827,3329,3330,3331,3333],{},"Adding ",[50,3332,52],{}," to a column with existing data",[50,3335,3336],{},"ALTER TABLE ... ALTER COLUMN col SET NOT NULL"," scans the entire table to verify no NULLs, under ",[50,3339,3340],{},"ACCESS EXCLUSIVE"," lock. Use the ",[50,3343,3344],{},"NOT VALID",[50,3346,3139],{}," CHECK constraint pattern for zero-downtime.",[18,3349,3351],{"id":3350},"spot-the-bug","🧠 Spot the Bug",[14,3353,3354,3355,3357],{},"A team adds this constraint to enforce that prices are positive, then discovers that a row with ",[50,3356,3198],{}," exists and the constraint didn't catch it. Why?",[133,3359,3360],{"language":135},[137,3361,3363],{"className":139,"code":3362,"language":135,"meta":141,"style":141},"ALTER TABLE products ADD CONSTRAINT price_positive CHECK (price > 0);\n",[50,3364,3365],{"__ignoreMap":141},[145,3366,3367,3369,3371,3373,3375,3377,3380,3382,3384,3386,3388],{"class":147,"line":148},[145,3368,251],{"class":151},[145,3370,155],{"class":151},[145,3372,1803],{"class":162},[145,3374,334],{"class":151},[145,3376,337],{"class":151},[145,3378,3379],{"class":162}," price_positive ",[145,3381,108],{"class":151},[145,3383,1680],{"class":162},[145,3385,2454],{"class":151},[145,3387,1686],{"class":626},[145,3389,215],{"class":162},[3391,3392,3393,3397,3427,3443,3493,3499,3537],"details",{},[3394,3395,3396],"summary",{},"Answer",[14,3398,3399,3401,3402,866,3405,3410,3411,3203,3413,3415,3416,3418,3419,3421,3422,3424,3425,1315],{},[50,3400,108],{}," constraints pass when the expression evaluates to ",[50,3403,3404],{},"TRUE",[827,3406,3407,3408],{},"or ",[50,3409,3206],{},". ",[50,3412,3202],{},[50,3414,3206],{}," (any comparison with NULL is UNKNOWN in three-valued logic), and a ",[50,3417,108],{}," constraint treats ",[50,3420,3206],{}," as \"not a violation\" — the row passes. So ",[50,3423,3198],{}," satisfies ",[50,3426,3194],{},[14,3428,3429,3430,3432,3433,3435,3436,3438,3439,3442],{},"This is by design: ",[50,3431,108],{}," constraints only reject rows where the expression is ",[50,3434,3210],{},". They don't enforce ",[50,3437,52],{}," — that's a separate constraint. If you want ",[50,3440,3441],{},"price"," to be both non-NULL and positive, you need both:",[133,3444,3445],{"language":135},[137,3446,3448],{"className":139,"code":3447,"language":135,"meta":141,"style":141},"ALTER TABLE products ALTER COLUMN price SET NOT NULL;\nALTER TABLE products ADD CONSTRAINT price_positive CHECK (price > 0);\n",[50,3449,3450,3469],{"__ignoreMap":141},[145,3451,3452,3454,3456,3458,3460,3463,3465,3467],{"class":147,"line":148},[145,3453,251],{"class":151},[145,3455,155],{"class":151},[145,3457,1803],{"class":162},[145,3459,251],{"class":151},[145,3461,3462],{"class":162}," COLUMN price ",[145,3464,294],{"class":151},[145,3466,190],{"class":151},[145,3468,272],{"class":162},[145,3470,3471,3473,3475,3477,3479,3481,3483,3485,3487,3489,3491],{"class":147,"line":166},[145,3472,251],{"class":151},[145,3474,155],{"class":151},[145,3476,1803],{"class":162},[145,3478,334],{"class":151},[145,3480,337],{"class":151},[145,3482,3379],{"class":162},[145,3484,108],{"class":151},[145,3486,1680],{"class":162},[145,3488,2454],{"class":151},[145,3490,1686],{"class":626},[145,3492,215],{"class":162},[14,3494,3495,3496,3498],{},"Or combine them in the CHECK with an explicit NULL test (less idiomatic — the ",[50,3497,52],{}," produces a more specific error message):",[133,3500,3501],{"language":135},[137,3502,3504],{"className":139,"code":3503,"language":135,"meta":141,"style":141},"ALTER TABLE products ADD CONSTRAINT price_positive CHECK (price IS NOT NULL AND price > 0);\n",[50,3505,3506],{"__ignoreMap":141},[145,3507,3508,3510,3512,3514,3516,3518,3520,3522,3524,3526,3528,3531,3533,3535],{"class":147,"line":148},[145,3509,251],{"class":151},[145,3511,155],{"class":151},[145,3513,1803],{"class":162},[145,3515,334],{"class":151},[145,3517,337],{"class":151},[145,3519,3379],{"class":162},[145,3521,108],{"class":151},[145,3523,1680],{"class":162},[145,3525,522],{"class":151},[145,3527,2459],{"class":151},[145,3529,3530],{"class":162}," price ",[145,3532,2454],{"class":151},[145,3534,1686],{"class":626},[145,3536,215],{"class":162},[14,3538,3539,3112,3542,3544,3545,3547,3548,3550],{},[827,3540,3541],{},"The lesson",[50,3543,108],{}," constraints allow NULLs (UNKNOWN passes). Use ",[50,3546,52],{}," to forbid NULL, and ",[50,3549,108],{}," to enforce a domain on non-NULL values.",[3552,3553,3554],"style",{},"html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}",{"title":141,"searchDepth":166,"depth":166,"links":3556},[3557,3558,3559,3560,3564,3569,3570,3571,3572,3573,3574,3575,3576],{"id":20,"depth":166,"text":21},{"id":131,"depth":166,"text":52},{"id":396,"depth":166,"text":65},{"id":536,"depth":166,"text":78,"children":3561},[3562,3563],{"id":732,"depth":181,"text":733},{"id":850,"depth":181,"text":851},{"id":873,"depth":166,"text":95,"children":3565},[3566,3567,3568],{"id":1054,"depth":181,"text":1055},{"id":1318,"depth":181,"text":1319},{"id":1391,"depth":181,"text":1392},{"id":1626,"depth":166,"text":1627},{"id":1851,"depth":166,"text":1852},{"id":2097,"depth":166,"text":2098},{"id":2243,"depth":166,"text":2244},{"id":2924,"depth":166,"text":2925},{"id":3070,"depth":166,"text":3071},{"id":3165,"depth":166,"text":3166},{"id":3350,"depth":166,"text":3351},"md",{},"\u002Fsql\u002F12-constraints-and-keys",{"title":5,"description":16},"sql\u002F12-constraints-and-keys","Gt5iYzitHOjuiiOE3gyIQmFFISxsWfYvG_Ms41vaVYs",1789924654544]