[{"data":1,"prerenderedAt":5194},["ShallowReactive",2],{"page-\u002Fsql\u002F18-json-and-arrays":3},{"id":4,"title":5,"body":6,"description":5187,"extension":5188,"meta":5189,"navigation":207,"path":5190,"seo":5191,"stem":5192,"__hash__":5193},"content\u002Fsql\u002F18-json-and-arrays.md","18 — JSON & Array Columns",{"type":7,"value":8,"toc":5160},"minimark",[9,13,22,25,30,133,145,244,246,250,557,563,565,569,757,1064,1080,1090,1243,1245,1249,1253,1400,1409,1413,1555,1558,1562,1644,1648,1738,1747,1749,1753,1759,1898,1909,1911,1915,1918,2141,2153,2155,2159,2162,2324,2328,2434,2710,2714,2920,2926,2928,2932,2935,4046,4052,4095,4097,4101,4379,4384,4404,4406,4410,4540,4542,4546,4670,4672,4676,4897,4899,4903,4910,4966,5121,5123,5126,5156],[10,11,5],"h1",{"id":12},"_18-json-array-columns",[14,15,16,17,21],"p",{},"PostgreSQL has first-class support for semi-structured data: ",[18,19,20],"code",{},"JSONB"," (binary JSON) and array columns. This lets you store variable-structure data (event payloads, user preferences, nested objects) without a separate document database, while keeping relational power for the rest of the schema.",[23,24],"hr",{},[26,27,29],"h2",{"id":28},"json-vs-jsonb","JSON vs JSONB",[31,32,33,53],"table",{},[34,35,36],"thead",{},[37,38,39,43,48],"tr",{},[40,41,42],"th",{},"Property",[40,44,45],{},[18,46,47],{},"json",[40,49,50],{},[18,51,52],{},"jsonb",[54,55,56,68,79,90,101,111,122],"tbody",{},[37,57,58,62,65],{},[59,60,61],"td",{},"Storage",[59,63,64],{},"Raw text (as input)",[59,66,67],{},"Decomposed binary",[37,69,70,73,76],{},[59,71,72],{},"Indexing",[59,74,75],{},"Limited (expression indexes only)",[59,77,78],{},"Full GIN, expression, partial",[37,80,81,84,87],{},[59,82,83],{},"Key deduplication",[59,85,86],{},"No (duplicate keys preserved)",[59,88,89],{},"Yes (last value wins)",[37,91,92,95,98],{},[59,93,94],{},"Preserves key order",[59,96,97],{},"✅ Yes",[59,99,100],{},"❌ No (keys sorted)",[37,102,103,106,108],{},[59,104,105],{},"Preserves whitespace",[59,107,97],{},[59,109,110],{},"❌ No",[37,112,113,116,119],{},[59,114,115],{},"Query speed",[59,117,118],{},"Slower (re-parsed each time)",[59,120,121],{},"Faster (pre-parsed binary)",[37,123,124,127,130],{},[59,125,126],{},"Write speed",[59,128,129],{},"Faster (no parsing)",[59,131,132],{},"Slower (parse to binary)",[14,134,135,141,142,144],{},[136,137,138,139],"strong",{},"Use ",[18,140,52],{}," for almost everything — it's indexable, faster to query, and deduplicates keys. Use ",[18,143,47],{}," only when you need to preserve the exact input (key order, duplicate keys, whitespace) — rare.",[146,147,149],"code-wrapper",{"language":148},"sql",[150,151,155],"pre",{"className":152,"code":153,"language":148,"meta":154,"style":154},"language-sql shiki shiki-themes github-light github-dark","-- jsonb deduplicates keys (last wins), json preserves duplicates\nSELECT '{\"a\": 1, \"a\": 2}'::json;     -- {\"a\": 1, \"a\": 2}  (both keys preserved)\nSELECT '{\"a\": 1, \"a\": 2}'::jsonb;    -- {\"a\": 2}           (last value wins)\n\n-- jsonb sorts keys, json preserves input order\nSELECT '{\"b\": 1, \"a\": 2}'::json;     -- {\"b\": 1, \"a\": 2}   (input order)\nSELECT '{\"b\": 1, \"a\": 2}'::jsonb;    -- {\"a\": 2, \"b\": 1}   (sorted)\n","",[18,156,157,166,189,202,209,215,232],{"__ignoreMap":154},[158,159,162],"span",{"class":160,"line":161},"line",1,[158,163,165],{"class":164},"sdCPZ","-- jsonb deduplicates keys (last wins), json preserves duplicates\n",[158,167,169,173,177,181,183,186],{"class":160,"line":168},2,[158,170,172],{"class":171},"svdQ7","SELECT",[158,174,176],{"class":175},"sJ6F3"," '{\"a\": 1, \"a\": 2}'",[158,178,180],{"class":179},"ssxIu","::",[158,182,47],{"class":171},[158,184,185],{"class":179},";     ",[158,187,188],{"class":164},"-- {\"a\": 1, \"a\": 2}  (both keys preserved)\n",[158,190,192,194,196,199],{"class":160,"line":191},3,[158,193,172],{"class":171},[158,195,176],{"class":175},[158,197,198],{"class":179},"::jsonb;    ",[158,200,201],{"class":164},"-- {\"a\": 2}           (last value wins)\n",[158,203,205],{"class":160,"line":204},4,[158,206,208],{"emptyLinePlaceholder":207},true,"\n",[158,210,212],{"class":160,"line":211},5,[158,213,214],{"class":164},"-- jsonb sorts keys, json preserves input order\n",[158,216,218,220,223,225,227,229],{"class":160,"line":217},6,[158,219,172],{"class":171},[158,221,222],{"class":175}," '{\"b\": 1, \"a\": 2}'",[158,224,180],{"class":179},[158,226,47],{"class":171},[158,228,185],{"class":179},[158,230,231],{"class":164},"-- {\"b\": 1, \"a\": 2}   (input order)\n",[158,233,235,237,239,241],{"class":160,"line":234},7,[158,236,172],{"class":171},[158,238,222],{"class":175},[158,240,198],{"class":179},[158,242,243],{"class":164},"-- {\"a\": 2, \"b\": 1}   (sorted)\n",[23,245],{},[26,247,249],{"id":248},"inserting-json","Inserting JSON",[146,251,252],{"language":148},[150,253,255],{"className":152,"code":254,"language":148,"meta":154,"style":154},"CREATE TABLE events (\n  id BIGSERIAL PRIMARY KEY,\n  payload JSONB NOT NULL\n);\n\n-- From a JSON string literal (PostgreSQL parses and converts to binary jsonb)\nINSERT INTO events (payload) VALUES (\n  '{\"type\": \"click\", \"user\": {\"id\": 42, \"name\": \"Alice\"}, \"tags\": [\"web\", \"mobile\"]}'\n);\n\n-- From columns via jsonb_build_object (handles quoting\u002Fescaping automatically)\nINSERT INTO events (payload)\nSELECT jsonb_build_object(\n  'type', 'click',\n  'user_id', u.id,                -- integer → JSON number\n  'email', u.email,               -- text → JSON string\n  'ts', now()                     -- timestamptz → JSON string (ISO format)\n)\nFROM users u WHERE u.email = 'a@x.com';\n\n-- From multiple rows aggregated into a JSON array\nINSERT INTO summary (daily_events)\nSELECT jsonb_agg(jsonb_build_object('id', e.id, 'type', e.payload->>'type'))\nFROM events e\nWHERE e.created_at >= CURRENT_DATE;\n",[18,256,257,272,286,294,299,303,308,321,327,332,337,343,351,359,373,397,418,435,441,469,474,480,488,530,538],{"__ignoreMap":154},[158,258,259,262,265,269],{"class":160,"line":161},[158,260,261],{"class":171},"CREATE",[158,263,264],{"class":171}," TABLE",[158,266,268],{"class":267},"sIsaT"," events",[158,270,271],{"class":179}," (\n",[158,273,274,277,280,283],{"class":160,"line":168},[158,275,276],{"class":179},"  id ",[158,278,279],{"class":171},"BIGSERIAL",[158,281,282],{"class":171}," PRIMARY KEY",[158,284,285],{"class":179},",\n",[158,287,288,291],{"class":160,"line":191},[158,289,290],{"class":179},"  payload JSONB ",[158,292,293],{"class":171},"NOT NULL\n",[158,295,296],{"class":160,"line":204},[158,297,298],{"class":179},");\n",[158,300,301],{"class":160,"line":211},[158,302,208],{"emptyLinePlaceholder":207},[158,304,305],{"class":160,"line":217},[158,306,307],{"class":164},"-- From a JSON string literal (PostgreSQL parses and converts to binary jsonb)\n",[158,309,310,313,316,319],{"class":160,"line":234},[158,311,312],{"class":171},"INSERT INTO",[158,314,315],{"class":179}," events (payload) ",[158,317,318],{"class":171},"VALUES",[158,320,271],{"class":179},[158,322,324],{"class":160,"line":323},8,[158,325,326],{"class":175},"  '{\"type\": \"click\", \"user\": {\"id\": 42, \"name\": \"Alice\"}, \"tags\": [\"web\", \"mobile\"]}'\n",[158,328,330],{"class":160,"line":329},9,[158,331,298],{"class":179},[158,333,335],{"class":160,"line":334},10,[158,336,208],{"emptyLinePlaceholder":207},[158,338,340],{"class":160,"line":339},11,[158,341,342],{"class":164},"-- From columns via jsonb_build_object (handles quoting\u002Fescaping automatically)\n",[158,344,346,348],{"class":160,"line":345},12,[158,347,312],{"class":171},[158,349,350],{"class":179}," events (payload)\n",[158,352,354,356],{"class":160,"line":353},13,[158,355,172],{"class":171},[158,357,358],{"class":179}," jsonb_build_object(\n",[158,360,362,365,368,371],{"class":160,"line":361},14,[158,363,364],{"class":175},"  'type'",[158,366,367],{"class":179},", ",[158,369,370],{"class":175},"'click'",[158,372,285],{"class":179},[158,374,376,379,381,385,388,391,394],{"class":160,"line":375},15,[158,377,378],{"class":175},"  'user_id'",[158,380,367],{"class":179},[158,382,384],{"class":383},"snvgF","u",[158,386,387],{"class":179},".",[158,389,390],{"class":383},"id",[158,392,393],{"class":179},",                ",[158,395,396],{"class":164},"-- integer → JSON number\n",[158,398,400,403,405,407,409,412,415],{"class":160,"line":399},16,[158,401,402],{"class":175},"  'email'",[158,404,367],{"class":179},[158,406,384],{"class":383},[158,408,387],{"class":179},[158,410,411],{"class":383},"email",[158,413,414],{"class":179},",               ",[158,416,417],{"class":164},"-- text → JSON string\n",[158,419,421,424,426,429,432],{"class":160,"line":420},17,[158,422,423],{"class":175},"  'ts'",[158,425,367],{"class":179},[158,427,428],{"class":171},"now",[158,430,431],{"class":179},"()                     ",[158,433,434],{"class":164},"-- timestamptz → JSON string (ISO format)\n",[158,436,438],{"class":160,"line":437},18,[158,439,440],{"class":179},")\n",[158,442,444,447,450,453,456,458,460,463,466],{"class":160,"line":443},19,[158,445,446],{"class":171},"FROM",[158,448,449],{"class":179}," users u ",[158,451,452],{"class":171},"WHERE",[158,454,455],{"class":383}," u",[158,457,387],{"class":179},[158,459,411],{"class":383},[158,461,462],{"class":171}," =",[158,464,465],{"class":175}," 'a@x.com'",[158,467,468],{"class":179},";\n",[158,470,472],{"class":160,"line":471},20,[158,473,208],{"emptyLinePlaceholder":207},[158,475,477],{"class":160,"line":476},21,[158,478,479],{"class":164},"-- From multiple rows aggregated into a JSON array\n",[158,481,483,485],{"class":160,"line":482},22,[158,484,312],{"class":171},[158,486,487],{"class":179}," summary (daily_events)\n",[158,489,491,493,496,499,501,504,506,508,510,513,515,517,519,522,525,527],{"class":160,"line":490},23,[158,492,172],{"class":171},[158,494,495],{"class":179}," jsonb_agg(jsonb_build_object(",[158,497,498],{"class":175},"'id'",[158,500,367],{"class":179},[158,502,503],{"class":383},"e",[158,505,387],{"class":179},[158,507,390],{"class":383},[158,509,367],{"class":179},[158,511,512],{"class":175},"'type'",[158,514,367],{"class":179},[158,516,503],{"class":383},[158,518,387],{"class":179},[158,520,521],{"class":383},"payload",[158,523,524],{"class":171},"->>",[158,526,512],{"class":175},[158,528,529],{"class":179},"))\n",[158,531,533,535],{"class":160,"line":532},24,[158,534,446],{"class":171},[158,536,537],{"class":179}," events e\n",[158,539,541,543,546,548,551,554],{"class":160,"line":540},25,[158,542,452],{"class":171},[158,544,545],{"class":383}," e",[158,547,387],{"class":179},[158,549,550],{"class":383},"created_at",[158,552,553],{"class":171}," >=",[158,555,556],{"class":179}," CURRENT_DATE;\n",[14,558,559,562],{},[18,560,561],{},"jsonb_build_object"," is safer than string concatenation — it handles NULL values, quoting, and escaping correctly. Use it for constructing JSONB from column data.",[23,564],{},[26,566,568],{"id":567},"querying-operators","Querying Operators",[31,570,571,587],{},[34,572,573],{},[37,574,575,578,581,584],{},[40,576,577],{},"Operator",[40,579,580],{},"Returns",[40,582,583],{},"Example",[40,585,586],{},"Indexable by GIN?",[54,588,589,607,626,644,663,681,698,715,729,746],{},[37,590,591,596,599,604],{},[59,592,593],{},[18,594,595],{},"->",[59,597,598],{},"JSONB value at key\u002Findex",[59,600,601],{},[18,602,603],{},"payload->'user'",[59,605,606],{},"❌ (use expression index)",[37,608,609,613,619,624],{},[59,610,611],{},[18,612,524],{},[59,614,615,618],{},[136,616,617],{},"Text"," value at key\u002Findex",[59,620,621],{},[18,622,623],{},"payload->>'type'",[59,625,606],{},[37,627,628,633,636,641],{},[59,629,630],{},[18,631,632],{},"#>",[59,634,635],{},"JSONB at path (array of keys)",[59,637,638],{},[18,639,640],{},"payload #> '{user,id}'",[59,642,643],{},"❌",[37,645,646,651,656,661],{},[59,647,648],{},[18,649,650],{},"#>>",[59,652,653,655],{},[136,654,617],{}," at path",[59,657,658],{},[18,659,660],{},"payload #>> '{user,name}'",[59,662,643],{},[37,664,665,670,673,678],{},[59,666,667],{},[18,668,669],{},"@>",[59,671,672],{},"Containment (left contains right)",[59,674,675],{},[18,676,677],{},"payload @> '{\"type\": \"click\"}'",[59,679,680],{},"✅",[37,682,683,688,691,696],{},[59,684,685],{},[18,686,687],{},"\u003C@",[59,689,690],{},"Contained by",[59,692,693],{},[18,694,695],{},"payload \u003C@ '{\"type\": \"click\"}'",[59,697,680],{},[37,699,700,705,708,713],{},[59,701,702],{},[18,703,704],{},"?",[59,706,707],{},"Key exists",[59,709,710],{},[18,711,712],{},"payload ? 'user'",[59,714,680],{},[37,716,717,720,723,726],{},[59,718,719],{},"`?",[59,721,722],{},"`",[59,724,725],{},"Any key exists",[59,727,728],{},"`payload ?",[37,730,731,736,739,744],{},[59,732,733],{},[18,734,735],{},"?&",[59,737,738],{},"All keys exist",[59,740,741],{},[18,742,743],{},"payload ?& array['user', 'session']",[59,745,680],{},[37,747,748,750,752,754],{},[59,749,722],{},[59,751],{},[59,753,722],{},[59,755,756],{},"Concatenate two JSONB",[146,758,759],{"language":148},[150,760,762],{"className":152,"code":761,"language":148,"meta":154,"style":154},"-- Get a field as text (->> is the final-step operator — returns text, can't chain)\nSELECT payload->>'type' FROM events;                        -- 'click' (text)\n\n-- Get a nested field: chain -> for traversal, ->> for the final scalar\nSELECT payload->'user'->>'name' FROM events;                -- 'Alice' (text)\nSELECT payload #>> '{user,name}' FROM events;               -- 'Alice' (text, path syntax)\n\n-- Get an array element (arrays are 0-indexed in JSON path)\nSELECT payload->'tags'->>0 FROM events;                     -- 'web' (first tag, as text)\nSELECT payload->'tags'->>1 FROM events;                     -- 'mobile' (second tag)\n\n-- Containment: the indexable operator (use this for filtered queries)\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';                 -- top-level key match\nSELECT * FROM events WHERE payload @> '{\"user\": {\"id\": 42}}';              -- nested object match\nSELECT * FROM events WHERE payload @> '{\"tags\": [\"web\"]}';                 -- array contains element\n\n-- Key existence\nSELECT * FROM events WHERE payload ? 'user_id';                            -- key exists\nSELECT * FROM events WHERE payload ?| array['user_id', 'session_id'];      -- any key exists\nSELECT * FROM events WHERE payload ?& array['user_id', 'session_id'];      -- all keys exist\n",[18,763,764,769,789,793,798,822,843,847,852,876,898,902,907,936,961,985,989,994,1018,1042],{"__ignoreMap":154},[158,765,766],{"class":160,"line":161},[158,767,768],{"class":164},"-- Get a field as text (->> is the final-step operator — returns text, can't chain)\n",[158,770,771,773,776,778,780,783,786],{"class":160,"line":168},[158,772,172],{"class":171},[158,774,775],{"class":179}," payload",[158,777,524],{"class":171},[158,779,512],{"class":175},[158,781,782],{"class":171}," FROM",[158,784,785],{"class":179}," events;                        ",[158,787,788],{"class":164},"-- 'click' (text)\n",[158,790,791],{"class":160,"line":191},[158,792,208],{"emptyLinePlaceholder":207},[158,794,795],{"class":160,"line":204},[158,796,797],{"class":164},"-- Get a nested field: chain -> for traversal, ->> for the final scalar\n",[158,799,800,802,804,806,809,811,814,816,819],{"class":160,"line":211},[158,801,172],{"class":171},[158,803,775],{"class":179},[158,805,595],{"class":171},[158,807,808],{"class":175},"'user'",[158,810,524],{"class":171},[158,812,813],{"class":175},"'name'",[158,815,782],{"class":171},[158,817,818],{"class":179}," events;                ",[158,820,821],{"class":164},"-- 'Alice' (text)\n",[158,823,824,826,829,832,835,837,840],{"class":160,"line":217},[158,825,172],{"class":171},[158,827,828],{"class":179}," payload #",[158,830,831],{"class":171},">>",[158,833,834],{"class":175}," '{user,name}'",[158,836,782],{"class":171},[158,838,839],{"class":179}," events;               ",[158,841,842],{"class":164},"-- 'Alice' (text, path syntax)\n",[158,844,845],{"class":160,"line":234},[158,846,208],{"emptyLinePlaceholder":207},[158,848,849],{"class":160,"line":323},[158,850,851],{"class":164},"-- Get an array element (arrays are 0-indexed in JSON path)\n",[158,853,854,856,858,860,863,865,868,870,873],{"class":160,"line":329},[158,855,172],{"class":171},[158,857,775],{"class":179},[158,859,595],{"class":171},[158,861,862],{"class":175},"'tags'",[158,864,524],{"class":171},[158,866,867],{"class":383},"0",[158,869,782],{"class":171},[158,871,872],{"class":179}," events;                     ",[158,874,875],{"class":164},"-- 'web' (first tag, as text)\n",[158,877,878,880,882,884,886,888,891,893,895],{"class":160,"line":334},[158,879,172],{"class":171},[158,881,775],{"class":179},[158,883,595],{"class":171},[158,885,862],{"class":175},[158,887,524],{"class":171},[158,889,890],{"class":383},"1",[158,892,782],{"class":171},[158,894,872],{"class":179},[158,896,897],{"class":164},"-- 'mobile' (second tag)\n",[158,899,900],{"class":160,"line":339},[158,901,208],{"emptyLinePlaceholder":207},[158,903,904],{"class":160,"line":345},[158,905,906],{"class":164},"-- Containment: the indexable operator (use this for filtered queries)\n",[158,908,909,911,914,916,919,921,924,927,930,933],{"class":160,"line":353},[158,910,172],{"class":171},[158,912,913],{"class":171}," *",[158,915,782],{"class":171},[158,917,918],{"class":179}," events ",[158,920,452],{"class":171},[158,922,923],{"class":179}," payload @",[158,925,926],{"class":171},">",[158,928,929],{"class":175}," '{\"type\": \"click\"}'",[158,931,932],{"class":179},";                 ",[158,934,935],{"class":164},"-- top-level key match\n",[158,937,938,940,942,944,946,948,950,952,955,958],{"class":160,"line":361},[158,939,172],{"class":171},[158,941,913],{"class":171},[158,943,782],{"class":171},[158,945,918],{"class":179},[158,947,452],{"class":171},[158,949,923],{"class":179},[158,951,926],{"class":171},[158,953,954],{"class":175}," '{\"user\": {\"id\": 42}}'",[158,956,957],{"class":179},";              ",[158,959,960],{"class":164},"-- nested object match\n",[158,962,963,965,967,969,971,973,975,977,980,982],{"class":160,"line":375},[158,964,172],{"class":171},[158,966,913],{"class":171},[158,968,782],{"class":171},[158,970,918],{"class":179},[158,972,452],{"class":171},[158,974,923],{"class":179},[158,976,926],{"class":171},[158,978,979],{"class":175}," '{\"tags\": [\"web\"]}'",[158,981,932],{"class":179},[158,983,984],{"class":164},"-- array contains element\n",[158,986,987],{"class":160,"line":399},[158,988,208],{"emptyLinePlaceholder":207},[158,990,991],{"class":160,"line":420},[158,992,993],{"class":164},"-- Key existence\n",[158,995,996,998,1000,1002,1004,1006,1009,1012,1015],{"class":160,"line":437},[158,997,172],{"class":171},[158,999,913],{"class":171},[158,1001,782],{"class":171},[158,1003,918],{"class":179},[158,1005,452],{"class":171},[158,1007,1008],{"class":179}," payload ? ",[158,1010,1011],{"class":175},"'user_id'",[158,1013,1014],{"class":179},";                            ",[158,1016,1017],{"class":164},"-- key exists\n",[158,1019,1020,1022,1024,1026,1028,1030,1033,1036,1039],{"class":160,"line":443},[158,1021,172],{"class":171},[158,1023,913],{"class":171},[158,1025,782],{"class":171},[158,1027,918],{"class":179},[158,1029,452],{"class":171},[158,1031,1032],{"class":179}," payload ?| ",[158,1034,1035],{"class":171},"array",[158,1037,1038],{"class":179},"['user_id', 'session_id'];      ",[158,1040,1041],{"class":164},"-- any key exists\n",[158,1043,1044,1046,1048,1050,1052,1054,1057,1059,1061],{"class":160,"line":471},[158,1045,172],{"class":171},[158,1047,913],{"class":171},[158,1049,782],{"class":171},[158,1051,918],{"class":179},[158,1053,452],{"class":171},[158,1055,1056],{"class":179}," payload ?& ",[158,1058,1035],{"class":171},[158,1060,1038],{"class":179},[158,1062,1063],{"class":164},"-- all keys exist\n",[14,1065,1066,1071,1072,1075,1076,1079],{},[136,1067,1068,1070],{},[18,1069,669],{}," (containment) is the GIN-indexable operator",". Prefer ",[18,1073,1074],{},"WHERE payload @> '{\"type\": \"click\"}'"," over ",[18,1077,1078],{},"WHERE payload->>'type' = 'click'"," — the former can use a GIN index on the whole column; the latter needs a per-path expression index.",[1081,1082,1084,1086,1087,1089],"h3",{"id":1083},"vs-the-return-type-trap",[18,1085,595],{}," vs ",[18,1088,524],{},": The Return Type Trap",[146,1091,1092],{"language":148},[150,1093,1095],{"className":152,"code":1094,"language":148,"meta":154,"style":154},"-- -> returns JSONB (can chain further -> operators)\nSELECT payload->'user'->'id' FROM events;        -- 42 (JSONB number)\n-- SELECT payload->'user'->'id'->>'x';           -- ERROR: can't chain ->> (returns text)\n\n-- ->> returns text (final step — can't chain)\nSELECT payload->'user'->>'name' FROM events;     -- 'Alice' (text)\n\n-- ⚠️ Numeric comparison trap: ->> returns text, so comparison is string-based\nSELECT * FROM events WHERE (payload->>'amount') > 100;\n-- String comparison: '100' > '99' is TRUE (lexicographic), '20' > '100' is TRUE — WRONG\n\n-- ✅ Cast to numeric for correct comparison\nSELECT * FROM events WHERE (payload->>'amount')::numeric > 100;\n",[18,1096,1097,1102,1124,1129,1133,1138,1159,1163,1168,1198,1203,1207,1212],{"__ignoreMap":154},[158,1098,1099],{"class":160,"line":161},[158,1100,1101],{"class":164},"-- -> returns JSONB (can chain further -> operators)\n",[158,1103,1104,1106,1108,1110,1112,1114,1116,1118,1121],{"class":160,"line":168},[158,1105,172],{"class":171},[158,1107,775],{"class":179},[158,1109,595],{"class":171},[158,1111,808],{"class":175},[158,1113,595],{"class":171},[158,1115,498],{"class":175},[158,1117,782],{"class":171},[158,1119,1120],{"class":179}," events;        ",[158,1122,1123],{"class":164},"-- 42 (JSONB number)\n",[158,1125,1126],{"class":160,"line":191},[158,1127,1128],{"class":164},"-- SELECT payload->'user'->'id'->>'x';           -- ERROR: can't chain ->> (returns text)\n",[158,1130,1131],{"class":160,"line":204},[158,1132,208],{"emptyLinePlaceholder":207},[158,1134,1135],{"class":160,"line":211},[158,1136,1137],{"class":164},"-- ->> returns text (final step — can't chain)\n",[158,1139,1140,1142,1144,1146,1148,1150,1152,1154,1157],{"class":160,"line":217},[158,1141,172],{"class":171},[158,1143,775],{"class":179},[158,1145,595],{"class":171},[158,1147,808],{"class":175},[158,1149,524],{"class":171},[158,1151,813],{"class":175},[158,1153,782],{"class":171},[158,1155,1156],{"class":179}," events;     ",[158,1158,821],{"class":164},[158,1160,1161],{"class":160,"line":234},[158,1162,208],{"emptyLinePlaceholder":207},[158,1164,1165],{"class":160,"line":323},[158,1166,1167],{"class":164},"-- ⚠️ Numeric comparison trap: ->> returns text, so comparison is string-based\n",[158,1169,1170,1172,1174,1176,1178,1180,1183,1185,1188,1191,1193,1196],{"class":160,"line":329},[158,1171,172],{"class":171},[158,1173,913],{"class":171},[158,1175,782],{"class":171},[158,1177,918],{"class":179},[158,1179,452],{"class":171},[158,1181,1182],{"class":179}," (payload",[158,1184,524],{"class":171},[158,1186,1187],{"class":175},"'amount'",[158,1189,1190],{"class":179},") ",[158,1192,926],{"class":171},[158,1194,1195],{"class":383}," 100",[158,1197,468],{"class":179},[158,1199,1200],{"class":160,"line":334},[158,1201,1202],{"class":164},"-- String comparison: '100' > '99' is TRUE (lexicographic), '20' > '100' is TRUE — WRONG\n",[158,1204,1205],{"class":160,"line":339},[158,1206,208],{"emptyLinePlaceholder":207},[158,1208,1209],{"class":160,"line":345},[158,1210,1211],{"class":164},"-- ✅ Cast to numeric for correct comparison\n",[158,1213,1214,1216,1218,1220,1222,1224,1226,1228,1230,1233,1236,1239,1241],{"class":160,"line":353},[158,1215,172],{"class":171},[158,1217,913],{"class":171},[158,1219,782],{"class":171},[158,1221,918],{"class":179},[158,1223,452],{"class":171},[158,1225,1182],{"class":179},[158,1227,524],{"class":171},[158,1229,1187],{"class":175},[158,1231,1232],{"class":179},")::",[158,1234,1235],{"class":171},"numeric",[158,1237,1238],{"class":171}," >",[158,1240,1195],{"class":383},[158,1242,468],{"class":179},[23,1244],{},[26,1246,1248],{"id":1247},"indexing-jsonb","Indexing JSONB",[1081,1250,1252],{"id":1251},"gin-index-indexes-everything","GIN Index (Indexes Everything)",[146,1254,1255],{"language":148},[150,1256,1258],{"className":152,"code":1257,"language":148,"meta":154,"style":154},"-- Full GIN index: indexes every key and value in the JSONB column\nCREATE INDEX events_payload_gin ON events USING gin(payload);\n\n-- Now containment, key-existence, and path queries use the index\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';    -- uses GIN\nSELECT * FROM events WHERE payload ? 'user_id';               -- uses GIN\nSELECT * FROM events WHERE payload ?| array['a', 'b'];        -- uses GIN\n\n-- ⚠️ The GIN index does NOT help with ->> extraction + comparison\nSELECT * FROM events WHERE payload->>'type' = 'click';\n-- This needs a separate expression index (see below)\n",[18,1259,1260,1265,1286,1290,1295,1319,1340,1361,1365,1370,1395],{"__ignoreMap":154},[158,1261,1262],{"class":160,"line":161},[158,1263,1264],{"class":164},"-- Full GIN index: indexes every key and value in the JSONB column\n",[158,1266,1267,1269,1272,1275,1278,1280,1283],{"class":160,"line":168},[158,1268,261],{"class":171},[158,1270,1271],{"class":171}," INDEX",[158,1273,1274],{"class":267}," events_payload_gin",[158,1276,1277],{"class":171}," ON",[158,1279,918],{"class":179},[158,1281,1282],{"class":171},"USING",[158,1284,1285],{"class":179}," gin(payload);\n",[158,1287,1288],{"class":160,"line":191},[158,1289,208],{"emptyLinePlaceholder":207},[158,1291,1292],{"class":160,"line":204},[158,1293,1294],{"class":164},"-- Now containment, key-existence, and path queries use the index\n",[158,1296,1297,1299,1301,1303,1305,1307,1309,1311,1313,1316],{"class":160,"line":211},[158,1298,172],{"class":171},[158,1300,913],{"class":171},[158,1302,782],{"class":171},[158,1304,918],{"class":179},[158,1306,452],{"class":171},[158,1308,923],{"class":179},[158,1310,926],{"class":171},[158,1312,929],{"class":175},[158,1314,1315],{"class":179},";    ",[158,1317,1318],{"class":164},"-- uses GIN\n",[158,1320,1321,1323,1325,1327,1329,1331,1333,1335,1338],{"class":160,"line":217},[158,1322,172],{"class":171},[158,1324,913],{"class":171},[158,1326,782],{"class":171},[158,1328,918],{"class":179},[158,1330,452],{"class":171},[158,1332,1008],{"class":179},[158,1334,1011],{"class":175},[158,1336,1337],{"class":179},";               ",[158,1339,1318],{"class":164},[158,1341,1342,1344,1346,1348,1350,1352,1354,1356,1359],{"class":160,"line":234},[158,1343,172],{"class":171},[158,1345,913],{"class":171},[158,1347,782],{"class":171},[158,1349,918],{"class":179},[158,1351,452],{"class":171},[158,1353,1032],{"class":179},[158,1355,1035],{"class":171},[158,1357,1358],{"class":179},"['a', 'b'];        ",[158,1360,1318],{"class":164},[158,1362,1363],{"class":160,"line":323},[158,1364,208],{"emptyLinePlaceholder":207},[158,1366,1367],{"class":160,"line":329},[158,1368,1369],{"class":164},"-- ⚠️ The GIN index does NOT help with ->> extraction + comparison\n",[158,1371,1372,1374,1376,1378,1380,1382,1384,1386,1388,1390,1393],{"class":160,"line":334},[158,1373,172],{"class":171},[158,1375,913],{"class":171},[158,1377,782],{"class":171},[158,1379,918],{"class":179},[158,1381,452],{"class":171},[158,1383,775],{"class":179},[158,1385,524],{"class":171},[158,1387,512],{"class":175},[158,1389,462],{"class":171},[158,1391,1392],{"class":175}," 'click'",[158,1394,468],{"class":179},[158,1396,1397],{"class":160,"line":339},[158,1398,1399],{"class":164},"-- This needs a separate expression index (see below)\n",[14,1401,1402,1403,1405,1406,1408],{},"A full GIN index on a JSONB column indexes every key and value — the most flexible approach (any ",[18,1404,669],{},"\u002F",[18,1407,704],{}," query benefits) but takes more space.",[1081,1410,1412],{"id":1411},"expression-index-indexes-a-specific-path","Expression Index (Indexes a Specific Path)",[146,1414,1415],{"language":148},[150,1416,1418],{"className":152,"code":1417,"language":148,"meta":154,"style":154},"-- Expression index: indexes the extracted text value of a specific path\nCREATE INDEX events_type_idx ON events ((payload->>'type'));\n\n-- Uses this index (the expression must match exactly)\nSELECT * FROM events WHERE payload->>'type' = 'click';\n\n-- Also works for the -> operator returning JSONB (for numeric\u002Fscalar comparisons)\nCREATE INDEX events_user_id_idx ON events (((payload->'user')->>'id')::int);\nSELECT * FROM events WHERE (payload->'user')->>'id'::int = 42;\n",[18,1419,1420,1425,1446,1450,1455,1479,1483,1488,1520],{"__ignoreMap":154},[158,1421,1422],{"class":160,"line":161},[158,1423,1424],{"class":164},"-- Expression index: indexes the extracted text value of a specific path\n",[158,1426,1427,1429,1431,1434,1436,1439,1441,1443],{"class":160,"line":168},[158,1428,261],{"class":171},[158,1430,1271],{"class":171},[158,1432,1433],{"class":267}," events_type_idx",[158,1435,1277],{"class":171},[158,1437,1438],{"class":179}," events ((payload",[158,1440,524],{"class":171},[158,1442,512],{"class":175},[158,1444,1445],{"class":179},"));\n",[158,1447,1448],{"class":160,"line":191},[158,1449,208],{"emptyLinePlaceholder":207},[158,1451,1452],{"class":160,"line":204},[158,1453,1454],{"class":164},"-- Uses this index (the expression must match exactly)\n",[158,1456,1457,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477],{"class":160,"line":211},[158,1458,172],{"class":171},[158,1460,913],{"class":171},[158,1462,782],{"class":171},[158,1464,918],{"class":179},[158,1466,452],{"class":171},[158,1468,775],{"class":179},[158,1470,524],{"class":171},[158,1472,512],{"class":175},[158,1474,462],{"class":171},[158,1476,1392],{"class":175},[158,1478,468],{"class":179},[158,1480,1481],{"class":160,"line":217},[158,1482,208],{"emptyLinePlaceholder":207},[158,1484,1485],{"class":160,"line":234},[158,1486,1487],{"class":164},"-- Also works for the -> operator returning JSONB (for numeric\u002Fscalar comparisons)\n",[158,1489,1490,1492,1494,1497,1499,1502,1504,1506,1509,1511,1513,1515,1518],{"class":160,"line":323},[158,1491,261],{"class":171},[158,1493,1271],{"class":171},[158,1495,1496],{"class":267}," events_user_id_idx",[158,1498,1277],{"class":171},[158,1500,1501],{"class":179}," events (((payload",[158,1503,595],{"class":171},[158,1505,808],{"class":175},[158,1507,1508],{"class":179},")",[158,1510,524],{"class":171},[158,1512,498],{"class":175},[158,1514,1232],{"class":179},[158,1516,1517],{"class":171},"int",[158,1519,298],{"class":179},[158,1521,1522,1524,1526,1528,1530,1532,1534,1536,1538,1540,1542,1544,1546,1548,1550,1553],{"class":160,"line":329},[158,1523,172],{"class":171},[158,1525,913],{"class":171},[158,1527,782],{"class":171},[158,1529,918],{"class":179},[158,1531,452],{"class":171},[158,1533,1182],{"class":179},[158,1535,595],{"class":171},[158,1537,808],{"class":175},[158,1539,1508],{"class":179},[158,1541,524],{"class":171},[158,1543,498],{"class":175},[158,1545,180],{"class":179},[158,1547,1517],{"class":171},[158,1549,462],{"class":171},[158,1551,1552],{"class":383}," 42",[158,1554,468],{"class":179},[14,1556,1557],{},"Expression indexes are smaller and faster for queries on a known path, but they only help that exact expression.",[1081,1559,1561],{"id":1560},"partial-gin-for-hot-subsets","Partial GIN for Hot Subsets",[146,1563,1564],{"language":148},[150,1565,1567],{"className":152,"code":1566,"language":148,"meta":154,"style":154},"-- Index only the 'click' events — much smaller, used when predicate matches\nCREATE INDEX events_clicks_gin ON events USING gin(payload)\n  WHERE payload @> '{\"type\": \"click\"}';\n\n-- This query uses the partial index\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}' AND payload @> '{\"source\": \"homepage\"}';\n",[18,1568,1569,1574,1592,1605,1609,1614],{"__ignoreMap":154},[158,1570,1571],{"class":160,"line":161},[158,1572,1573],{"class":164},"-- Index only the 'click' events — much smaller, used when predicate matches\n",[158,1575,1576,1578,1580,1583,1585,1587,1589],{"class":160,"line":168},[158,1577,261],{"class":171},[158,1579,1271],{"class":171},[158,1581,1582],{"class":267}," events_clicks_gin",[158,1584,1277],{"class":171},[158,1586,918],{"class":179},[158,1588,1282],{"class":171},[158,1590,1591],{"class":179}," gin(payload)\n",[158,1593,1594,1597,1599,1601,1603],{"class":160,"line":191},[158,1595,1596],{"class":171},"  WHERE",[158,1598,923],{"class":179},[158,1600,926],{"class":171},[158,1602,929],{"class":175},[158,1604,468],{"class":179},[158,1606,1607],{"class":160,"line":204},[158,1608,208],{"emptyLinePlaceholder":207},[158,1610,1611],{"class":160,"line":211},[158,1612,1613],{"class":164},"-- This query uses the partial index\n",[158,1615,1616,1618,1620,1622,1624,1626,1628,1630,1632,1635,1637,1639,1642],{"class":160,"line":217},[158,1617,172],{"class":171},[158,1619,913],{"class":171},[158,1621,782],{"class":171},[158,1623,918],{"class":179},[158,1625,452],{"class":171},[158,1627,923],{"class":179},[158,1629,926],{"class":171},[158,1631,929],{"class":175},[158,1633,1634],{"class":171}," AND",[158,1636,923],{"class":179},[158,1638,926],{"class":171},[158,1640,1641],{"class":175}," '{\"source\": \"homepage\"}'",[158,1643,468],{"class":179},[1081,1645,1647],{"id":1646},"gin-index-with-jsonb_path_ops-smaller-containment-only","GIN Index with jsonb_path_ops (Smaller, Containment-Only)",[146,1649,1650],{"language":148},[150,1651,1653],{"className":152,"code":1652,"language":148,"meta":154,"style":154},"-- jsonb_path_ops: smaller index, but only supports @> (no ?\u002F ?| \u002F ?&)\nCREATE INDEX events_payload_path_gin ON events USING gin(payload jsonb_path_ops);\n\n-- @> uses the index (faster lookup, smaller index)\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';\n\n-- ? does NOT use this index type\nSELECT * FROM events WHERE payload ? 'user_id';   -- seq scan (not supported by jsonb_path_ops)\n",[18,1654,1655,1660,1678,1682,1687,1707,1711,1716],{"__ignoreMap":154},[158,1656,1657],{"class":160,"line":161},[158,1658,1659],{"class":164},"-- jsonb_path_ops: smaller index, but only supports @> (no ?\u002F ?| \u002F ?&)\n",[158,1661,1662,1664,1666,1669,1671,1673,1675],{"class":160,"line":168},[158,1663,261],{"class":171},[158,1665,1271],{"class":171},[158,1667,1668],{"class":267}," events_payload_path_gin",[158,1670,1277],{"class":171},[158,1672,918],{"class":179},[158,1674,1282],{"class":171},[158,1676,1677],{"class":179}," gin(payload jsonb_path_ops);\n",[158,1679,1680],{"class":160,"line":191},[158,1681,208],{"emptyLinePlaceholder":207},[158,1683,1684],{"class":160,"line":204},[158,1685,1686],{"class":164},"-- @> uses the index (faster lookup, smaller index)\n",[158,1688,1689,1691,1693,1695,1697,1699,1701,1703,1705],{"class":160,"line":211},[158,1690,172],{"class":171},[158,1692,913],{"class":171},[158,1694,782],{"class":171},[158,1696,918],{"class":179},[158,1698,452],{"class":171},[158,1700,923],{"class":179},[158,1702,926],{"class":171},[158,1704,929],{"class":175},[158,1706,468],{"class":179},[158,1708,1709],{"class":160,"line":217},[158,1710,208],{"emptyLinePlaceholder":207},[158,1712,1713],{"class":160,"line":234},[158,1714,1715],{"class":164},"-- ? does NOT use this index type\n",[158,1717,1718,1720,1722,1724,1726,1728,1730,1732,1735],{"class":160,"line":323},[158,1719,172],{"class":171},[158,1721,913],{"class":171},[158,1723,782],{"class":171},[158,1725,918],{"class":179},[158,1727,452],{"class":171},[158,1729,1008],{"class":179},[158,1731,1011],{"class":175},[158,1733,1734],{"class":179},";   ",[158,1736,1737],{"class":164},"-- seq scan (not supported by jsonb_path_ops)\n",[14,1739,138,1740,1743,1744,1746],{},[18,1741,1742],{},"jsonb_path_ops"," when you only need ",[18,1745,669],{}," containment queries — the index is significantly smaller.",[23,1748],{},[26,1750,1752],{"id":1751},"sqljson-path-queries-postgresql-12","SQL\u002FJSON Path Queries (PostgreSQL 12+)",[14,1754,1755,1758],{},[18,1756,1757],{},"jsonb_path_query"," uses the SQL\u002FJSON path language for complex nested extraction:",[146,1760,1761],{"language":148},[150,1762,1764],{"className":152,"code":1763,"language":148,"meta":154,"style":154},"-- Extract all user IDs from an array of events\nSELECT jsonb_path_query(payload, '$.user.id') FROM events;\n\n-- Filter array elements with a condition\nSELECT jsonb_path_query(payload, '$.tags[*] ? (@ == \"web\")') FROM events;\n\n-- Existence check (returns boolean)\nSELECT jsonb_path_exists(payload, '$.user.id ? (@ > 10)') FROM events;\n\n-- Query with variables ($1 references the first variable)\nSELECT * FROM events\nWHERE jsonb_path_exists(payload, '$.user.id ? (@ == $id)', '{\"id\": 42}');\n\n-- Extract nested arrays and iterate\nSELECT jsonb_path_query(payload, '$.items[*].name') FROM orders_json;\n",[18,1765,1766,1771,1788,1792,1797,1812,1816,1821,1837,1841,1846,1857,1873,1877,1882],{"__ignoreMap":154},[158,1767,1768],{"class":160,"line":161},[158,1769,1770],{"class":164},"-- Extract all user IDs from an array of events\n",[158,1772,1773,1775,1778,1781,1783,1785],{"class":160,"line":168},[158,1774,172],{"class":171},[158,1776,1777],{"class":179}," jsonb_path_query(payload, ",[158,1779,1780],{"class":175},"'$.user.id'",[158,1782,1190],{"class":179},[158,1784,446],{"class":171},[158,1786,1787],{"class":179}," events;\n",[158,1789,1790],{"class":160,"line":191},[158,1791,208],{"emptyLinePlaceholder":207},[158,1793,1794],{"class":160,"line":204},[158,1795,1796],{"class":164},"-- Filter array elements with a condition\n",[158,1798,1799,1801,1803,1806,1808,1810],{"class":160,"line":211},[158,1800,172],{"class":171},[158,1802,1777],{"class":179},[158,1804,1805],{"class":175},"'$.tags[*] ? (@ == \"web\")'",[158,1807,1190],{"class":179},[158,1809,446],{"class":171},[158,1811,1787],{"class":179},[158,1813,1814],{"class":160,"line":217},[158,1815,208],{"emptyLinePlaceholder":207},[158,1817,1818],{"class":160,"line":234},[158,1819,1820],{"class":164},"-- Existence check (returns boolean)\n",[158,1822,1823,1825,1828,1831,1833,1835],{"class":160,"line":323},[158,1824,172],{"class":171},[158,1826,1827],{"class":179}," jsonb_path_exists(payload, ",[158,1829,1830],{"class":175},"'$.user.id ? (@ > 10)'",[158,1832,1190],{"class":179},[158,1834,446],{"class":171},[158,1836,1787],{"class":179},[158,1838,1839],{"class":160,"line":329},[158,1840,208],{"emptyLinePlaceholder":207},[158,1842,1843],{"class":160,"line":334},[158,1844,1845],{"class":164},"-- Query with variables ($1 references the first variable)\n",[158,1847,1848,1850,1852,1854],{"class":160,"line":339},[158,1849,172],{"class":171},[158,1851,913],{"class":171},[158,1853,782],{"class":171},[158,1855,1856],{"class":179}," events\n",[158,1858,1859,1861,1863,1866,1868,1871],{"class":160,"line":345},[158,1860,452],{"class":171},[158,1862,1827],{"class":179},[158,1864,1865],{"class":175},"'$.user.id ? (@ == $id)'",[158,1867,367],{"class":179},[158,1869,1870],{"class":175},"'{\"id\": 42}'",[158,1872,298],{"class":179},[158,1874,1875],{"class":160,"line":353},[158,1876,208],{"emptyLinePlaceholder":207},[158,1878,1879],{"class":160,"line":361},[158,1880,1881],{"class":164},"-- Extract nested arrays and iterate\n",[158,1883,1884,1886,1888,1891,1893,1895],{"class":160,"line":375},[158,1885,172],{"class":171},[158,1887,1777],{"class":179},[158,1889,1890],{"class":175},"'$.items[*].name'",[158,1892,1190],{"class":179},[158,1894,446],{"class":171},[158,1896,1897],{"class":179}," orders_json;\n",[14,1899,1900,1901,1405,1903,1905,1906,1908],{},"JSON path is more expressive than ",[18,1902,595],{},[18,1904,632],{}," chaining — it supports filters, wildcards, and variables. But for simple lookups, ",[18,1907,669],{}," + GIN is faster and more indexable.",[23,1910],{},[26,1912,1914],{"id":1913},"modifying-jsonb","Modifying JSONB",[14,1916,1917],{},"JSONB is immutable in place — you replace it with a modified version:",[146,1919,1920],{"language":148},[150,1921,1923],{"className":152,"code":1922,"language":148,"meta":154,"style":154},"-- Add\u002Fset a key at a path (jsonb_set: target, path, new_value, create_if_missing)\nUPDATE events SET payload = jsonb_set(payload, '{user,name}', '\"Bob\"'::jsonb);\n--                                                                    ^^^^^^^^^^^^\n-- new_value must be jsonb — strings need double quotes and cast\n\n-- Add a key only if it doesn't exist (create_if_missing = true is the default)\nUPDATE events SET payload = jsonb_set(payload, '{processed}', 'true'::jsonb, true);\n\n-- Set only if the key already exists (create_if_missing = false)\nUPDATE events SET payload = jsonb_set(payload, '{existing_key}', '\"new\"'::jsonb, false);\n\n-- Merge at the top level (|| concatenates — does NOT deep-merge)\nUPDATE events SET payload = payload || '{\"processed\": true, \"version\": 2}'::jsonb;\n-- ⚠️ || replaces entire objects: payload || '{\"user\": {\"name\": \"Bob\"}}'\n--    replaces the ENTIRE user object, not just user.name\n\n-- Remove a key (- operator)\nUPDATE events SET payload = payload - 'tags';\n\n-- Remove a nested key (#- with path)\nUPDATE events SET payload = payload #- '{user,id}';\n",[18,1924,1925,1930,1960,1965,1970,1974,1979,2004,2008,2013,2038,2042,2047,2070,2075,2080,2084,2089,2111,2115,2120],{"__ignoreMap":154},[158,1926,1927],{"class":160,"line":161},[158,1928,1929],{"class":164},"-- Add\u002Fset a key at a path (jsonb_set: target, path, new_value, create_if_missing)\n",[158,1931,1932,1935,1937,1940,1943,1946,1949,1952,1954,1957],{"class":160,"line":168},[158,1933,1934],{"class":171},"UPDATE",[158,1936,918],{"class":179},[158,1938,1939],{"class":171},"SET",[158,1941,1942],{"class":179}," payload ",[158,1944,1945],{"class":171},"=",[158,1947,1948],{"class":179}," jsonb_set(payload, ",[158,1950,1951],{"class":175},"'{user,name}'",[158,1953,367],{"class":179},[158,1955,1956],{"class":175},"'\"Bob\"'",[158,1958,1959],{"class":179},"::jsonb);\n",[158,1961,1962],{"class":160,"line":191},[158,1963,1964],{"class":164},"--                                                                    ^^^^^^^^^^^^\n",[158,1966,1967],{"class":160,"line":204},[158,1968,1969],{"class":164},"-- new_value must be jsonb — strings need double quotes and cast\n",[158,1971,1972],{"class":160,"line":211},[158,1973,208],{"emptyLinePlaceholder":207},[158,1975,1976],{"class":160,"line":217},[158,1977,1978],{"class":164},"-- Add a key only if it doesn't exist (create_if_missing = true is the default)\n",[158,1980,1981,1983,1985,1987,1989,1991,1993,1996,1998,2001],{"class":160,"line":234},[158,1982,1934],{"class":171},[158,1984,918],{"class":179},[158,1986,1939],{"class":171},[158,1988,1942],{"class":179},[158,1990,1945],{"class":171},[158,1992,1948],{"class":179},[158,1994,1995],{"class":175},"'{processed}'",[158,1997,367],{"class":179},[158,1999,2000],{"class":175},"'true'",[158,2002,2003],{"class":179},"::jsonb, true);\n",[158,2005,2006],{"class":160,"line":323},[158,2007,208],{"emptyLinePlaceholder":207},[158,2009,2010],{"class":160,"line":329},[158,2011,2012],{"class":164},"-- Set only if the key already exists (create_if_missing = false)\n",[158,2014,2015,2017,2019,2021,2023,2025,2027,2030,2032,2035],{"class":160,"line":334},[158,2016,1934],{"class":171},[158,2018,918],{"class":179},[158,2020,1939],{"class":171},[158,2022,1942],{"class":179},[158,2024,1945],{"class":171},[158,2026,1948],{"class":179},[158,2028,2029],{"class":175},"'{existing_key}'",[158,2031,367],{"class":179},[158,2033,2034],{"class":175},"'\"new\"'",[158,2036,2037],{"class":179},"::jsonb, false);\n",[158,2039,2040],{"class":160,"line":339},[158,2041,208],{"emptyLinePlaceholder":207},[158,2043,2044],{"class":160,"line":345},[158,2045,2046],{"class":164},"-- Merge at the top level (|| concatenates — does NOT deep-merge)\n",[158,2048,2049,2051,2053,2055,2057,2059,2061,2064,2067],{"class":160,"line":353},[158,2050,1934],{"class":171},[158,2052,918],{"class":179},[158,2054,1939],{"class":171},[158,2056,1942],{"class":179},[158,2058,1945],{"class":171},[158,2060,1942],{"class":179},[158,2062,2063],{"class":171},"||",[158,2065,2066],{"class":175}," '{\"processed\": true, \"version\": 2}'",[158,2068,2069],{"class":179},"::jsonb;\n",[158,2071,2072],{"class":160,"line":361},[158,2073,2074],{"class":164},"-- ⚠️ || replaces entire objects: payload || '{\"user\": {\"name\": \"Bob\"}}'\n",[158,2076,2077],{"class":160,"line":375},[158,2078,2079],{"class":164},"--    replaces the ENTIRE user object, not just user.name\n",[158,2081,2082],{"class":160,"line":399},[158,2083,208],{"emptyLinePlaceholder":207},[158,2085,2086],{"class":160,"line":420},[158,2087,2088],{"class":164},"-- Remove a key (- operator)\n",[158,2090,2091,2093,2095,2097,2099,2101,2103,2106,2109],{"class":160,"line":437},[158,2092,1934],{"class":171},[158,2094,918],{"class":179},[158,2096,1939],{"class":171},[158,2098,1942],{"class":179},[158,2100,1945],{"class":171},[158,2102,1942],{"class":179},[158,2104,2105],{"class":171},"-",[158,2107,2108],{"class":175}," 'tags'",[158,2110,468],{"class":179},[158,2112,2113],{"class":160,"line":443},[158,2114,208],{"emptyLinePlaceholder":207},[158,2116,2117],{"class":160,"line":471},[158,2118,2119],{"class":164},"-- Remove a nested key (#- with path)\n",[158,2121,2122,2124,2126,2128,2130,2132,2134,2136,2139],{"class":160,"line":476},[158,2123,1934],{"class":171},[158,2125,918],{"class":179},[158,2127,1939],{"class":171},[158,2129,1942],{"class":179},[158,2131,1945],{"class":171},[158,2133,828],{"class":179},[158,2135,2105],{"class":171},[158,2137,2138],{"class":175}," '{user,id}'",[158,2140,468],{"class":179},[14,2142,2143,2148,2149,2152],{},[136,2144,2145,2147],{},[18,2146,2063],{}," merges at the top level only"," — it does NOT deep-merge nested objects. For deep updates, use ",[18,2150,2151],{},"jsonb_set"," on the specific path.",[23,2154],{},[26,2156,2158],{"id":2157},"array-columns","Array Columns",[14,2160,2161],{},"PostgreSQL columns can be arrays of any type:",[146,2163,2164],{"language":148},[150,2165,2167],{"className":152,"code":2166,"language":148,"meta":154,"style":154},"CREATE TABLE posts (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  tags TEXT[] NOT NULL DEFAULT '{}'   -- array of text, default empty array\n);\n\n-- Insert with array literal syntax\nINSERT INTO posts (title, tags) VALUES ('Hello', '{\"intro\", \"welcome\"}');\n\n-- Insert with ARRAY constructor\nINSERT INTO posts (title, tags) VALUES ('World', ARRAY['news', 'tech']);\n\n-- Insert with array_agg from a subquery\nINSERT INTO archive (id, all_tags)\nSELECT post_id, array_agg(tag) FROM post_tags GROUP BY post_id;\n",[18,2168,2169,2180,2190,2203,2225,2229,2233,2238,2260,2264,2269,2290,2294,2299,2306],{"__ignoreMap":154},[158,2170,2171,2173,2175,2178],{"class":160,"line":161},[158,2172,261],{"class":171},[158,2174,264],{"class":171},[158,2176,2177],{"class":267}," posts",[158,2179,271],{"class":179},[158,2181,2182,2184,2186,2188],{"class":160,"line":168},[158,2183,276],{"class":179},[158,2185,279],{"class":171},[158,2187,282],{"class":171},[158,2189,285],{"class":179},[158,2191,2192,2195,2198,2201],{"class":160,"line":191},[158,2193,2194],{"class":179},"  title ",[158,2196,2197],{"class":171},"TEXT",[158,2199,2200],{"class":171}," NOT NULL",[158,2202,285],{"class":179},[158,2204,2205,2208,2210,2213,2216,2219,2222],{"class":160,"line":204},[158,2206,2207],{"class":179},"  tags ",[158,2209,2197],{"class":171},[158,2211,2212],{"class":179},"[] ",[158,2214,2215],{"class":171},"NOT NULL",[158,2217,2218],{"class":171}," DEFAULT",[158,2220,2221],{"class":175}," '{}'",[158,2223,2224],{"class":164},"   -- array of text, default empty array\n",[158,2226,2227],{"class":160,"line":211},[158,2228,298],{"class":179},[158,2230,2231],{"class":160,"line":217},[158,2232,208],{"emptyLinePlaceholder":207},[158,2234,2235],{"class":160,"line":234},[158,2236,2237],{"class":164},"-- Insert with array literal syntax\n",[158,2239,2240,2242,2245,2247,2250,2253,2255,2258],{"class":160,"line":323},[158,2241,312],{"class":171},[158,2243,2244],{"class":179}," posts (title, tags) ",[158,2246,318],{"class":171},[158,2248,2249],{"class":179}," (",[158,2251,2252],{"class":175},"'Hello'",[158,2254,367],{"class":179},[158,2256,2257],{"class":175},"'{\"intro\", \"welcome\"}'",[158,2259,298],{"class":179},[158,2261,2262],{"class":160,"line":329},[158,2263,208],{"emptyLinePlaceholder":207},[158,2265,2266],{"class":160,"line":334},[158,2267,2268],{"class":164},"-- Insert with ARRAY constructor\n",[158,2270,2271,2273,2275,2277,2279,2282,2284,2287],{"class":160,"line":339},[158,2272,312],{"class":171},[158,2274,2244],{"class":179},[158,2276,318],{"class":171},[158,2278,2249],{"class":179},[158,2280,2281],{"class":175},"'World'",[158,2283,367],{"class":179},[158,2285,2286],{"class":171},"ARRAY",[158,2288,2289],{"class":179},"['news', 'tech']);\n",[158,2291,2292],{"class":160,"line":345},[158,2293,208],{"emptyLinePlaceholder":207},[158,2295,2296],{"class":160,"line":353},[158,2297,2298],{"class":164},"-- Insert with array_agg from a subquery\n",[158,2300,2301,2303],{"class":160,"line":361},[158,2302,312],{"class":171},[158,2304,2305],{"class":179}," archive (id, all_tags)\n",[158,2307,2308,2310,2313,2315,2318,2321],{"class":160,"line":375},[158,2309,172],{"class":171},[158,2311,2312],{"class":179}," post_id, array_agg(tag) ",[158,2314,446],{"class":171},[158,2316,2317],{"class":179}," post_tags ",[158,2319,2320],{"class":171},"GROUP BY",[158,2322,2323],{"class":179}," post_id;\n",[1081,2325,2327],{"id":2326},"array-operators","Array Operators",[31,2329,2330,2342],{},[34,2331,2332],{},[37,2333,2334,2336,2339],{},[40,2335,577],{},[40,2337,2338],{},"Meaning",[40,2340,2341],{},"GIN-indexable?",[54,2343,2344,2363,2374,2385,2397,2409,2422],{},[37,2345,2346,2355,2358],{},[59,2347,2348,2351,2352],{},[18,2349,2350],{},"= ANY(arr)"," \u002F ",[18,2353,2354],{},"val IN",[59,2356,2357],{},"Value equals any element",[59,2359,2360,2361,1508],{},"❌ (use ",[18,2362,669],{},[37,2364,2365,2369,2372],{},[59,2366,2367],{},[18,2368,669],{},[59,2370,2371],{},"Array contains all of right's elements",[59,2373,680],{},[37,2375,2376,2380,2383],{},[59,2377,2378],{},[18,2379,687],{},[59,2381,2382],{},"Array is contained by",[59,2384,680],{},[37,2386,2387,2392,2395],{},[59,2388,2389],{},[18,2390,2391],{},"&&",[59,2393,2394],{},"Overlap (any common element)",[59,2396,680],{},[37,2398,2399,2404,2407],{},[59,2400,2401],{},[18,2402,2403],{},"arr[i]",[59,2405,2406],{},"Element at 1-based index",[59,2408,643],{},[37,2410,2411,2416,2419],{},[59,2412,2413],{},[18,2414,2415],{},"array_length(arr, dim)",[59,2417,2418],{},"Length (NULL if empty!)",[59,2420,2421],{},"N\u002FA",[37,2423,2424,2429,2432],{},[59,2425,2426],{},[18,2427,2428],{},"cardinality(arr)",[59,2430,2431],{},"Length (0 if empty)",[59,2433,2421],{},[146,2435,2436],{"language":148},[150,2437,2439],{"className":152,"code":2438,"language":148,"meta":154,"style":154},"-- Posts with the 'intro' tag (two equivalent forms)\nSELECT * FROM posts WHERE 'intro' = ANY(tags);           -- works, no GIN\nSELECT * FROM posts WHERE tags @> array['intro'];        -- uses GIN index\n\n-- Posts with ALL of these tags (contains both)\nSELECT * FROM posts WHERE tags @> array['intro', 'welcome'];\n\n-- Posts with ANY of these tags (overlap)\nSELECT * FROM posts WHERE tags && array['intro', 'news'];\n\n-- GIN index for array containment queries\nCREATE INDEX posts_tags_gin ON posts USING gin(tags);\n\n-- Element access (1-indexed!)\nSELECT tags[1] FROM posts;        -- first element (tags[0] is NULL, not an error)\nSELECT tags[2] FROM posts;        -- second element\n\n-- Length (⚠️ array_length returns NULL for empty arrays!)\nSELECT array_length(tags, 1) FROM posts WHERE tags = '{}';   -- NULL, not 0\nSELECT cardinality(tags) FROM posts WHERE tags = '{}';        -- 0 (use cardinality)\nSELECT coalesce(array_length(tags, 1), 0) FROM posts;         -- 0 (coalesce fallback)\n",[18,2440,2441,2446,2470,2496,2500,2505,2526,2530,2535,2555,2559,2564,2582,2586,2591,2606,2620,2624,2629,2658,2683],{"__ignoreMap":154},[158,2442,2443],{"class":160,"line":161},[158,2444,2445],{"class":164},"-- Posts with the 'intro' tag (two equivalent forms)\n",[158,2447,2448,2450,2452,2454,2457,2459,2462,2464,2467],{"class":160,"line":168},[158,2449,172],{"class":171},[158,2451,913],{"class":171},[158,2453,782],{"class":171},[158,2455,2456],{"class":179}," posts ",[158,2458,452],{"class":171},[158,2460,2461],{"class":175}," 'intro'",[158,2463,462],{"class":171},[158,2465,2466],{"class":179}," ANY(tags);           ",[158,2468,2469],{"class":164},"-- works, no GIN\n",[158,2471,2472,2474,2476,2478,2480,2482,2485,2487,2490,2493],{"class":160,"line":191},[158,2473,172],{"class":171},[158,2475,913],{"class":171},[158,2477,782],{"class":171},[158,2479,2456],{"class":179},[158,2481,452],{"class":171},[158,2483,2484],{"class":179}," tags @",[158,2486,926],{"class":171},[158,2488,2489],{"class":171}," array",[158,2491,2492],{"class":179},"['intro'];        ",[158,2494,2495],{"class":164},"-- uses GIN index\n",[158,2497,2498],{"class":160,"line":204},[158,2499,208],{"emptyLinePlaceholder":207},[158,2501,2502],{"class":160,"line":211},[158,2503,2504],{"class":164},"-- Posts with ALL of these tags (contains both)\n",[158,2506,2507,2509,2511,2513,2515,2517,2519,2521,2523],{"class":160,"line":217},[158,2508,172],{"class":171},[158,2510,913],{"class":171},[158,2512,782],{"class":171},[158,2514,2456],{"class":179},[158,2516,452],{"class":171},[158,2518,2484],{"class":179},[158,2520,926],{"class":171},[158,2522,2489],{"class":171},[158,2524,2525],{"class":179},"['intro', 'welcome'];\n",[158,2527,2528],{"class":160,"line":234},[158,2529,208],{"emptyLinePlaceholder":207},[158,2531,2532],{"class":160,"line":323},[158,2533,2534],{"class":164},"-- Posts with ANY of these tags (overlap)\n",[158,2536,2537,2539,2541,2543,2545,2547,2550,2552],{"class":160,"line":329},[158,2538,172],{"class":171},[158,2540,913],{"class":171},[158,2542,782],{"class":171},[158,2544,2456],{"class":179},[158,2546,452],{"class":171},[158,2548,2549],{"class":179}," tags && ",[158,2551,1035],{"class":171},[158,2553,2554],{"class":179},"['intro', 'news'];\n",[158,2556,2557],{"class":160,"line":334},[158,2558,208],{"emptyLinePlaceholder":207},[158,2560,2561],{"class":160,"line":339},[158,2562,2563],{"class":164},"-- GIN index for array containment queries\n",[158,2565,2566,2568,2570,2573,2575,2577,2579],{"class":160,"line":345},[158,2567,261],{"class":171},[158,2569,1271],{"class":171},[158,2571,2572],{"class":267}," posts_tags_gin",[158,2574,1277],{"class":171},[158,2576,2456],{"class":179},[158,2578,1282],{"class":171},[158,2580,2581],{"class":179}," gin(tags);\n",[158,2583,2584],{"class":160,"line":353},[158,2585,208],{"emptyLinePlaceholder":207},[158,2587,2588],{"class":160,"line":361},[158,2589,2590],{"class":164},"-- Element access (1-indexed!)\n",[158,2592,2593,2595,2598,2600,2603],{"class":160,"line":375},[158,2594,172],{"class":171},[158,2596,2597],{"class":179}," tags[1] ",[158,2599,446],{"class":171},[158,2601,2602],{"class":179}," posts;        ",[158,2604,2605],{"class":164},"-- first element (tags[0] is NULL, not an error)\n",[158,2607,2608,2610,2613,2615,2617],{"class":160,"line":399},[158,2609,172],{"class":171},[158,2611,2612],{"class":179}," tags[2] ",[158,2614,446],{"class":171},[158,2616,2602],{"class":179},[158,2618,2619],{"class":164},"-- second element\n",[158,2621,2622],{"class":160,"line":420},[158,2623,208],{"emptyLinePlaceholder":207},[158,2625,2626],{"class":160,"line":437},[158,2627,2628],{"class":164},"-- Length (⚠️ array_length returns NULL for empty arrays!)\n",[158,2630,2631,2633,2636,2638,2640,2642,2644,2646,2649,2651,2653,2655],{"class":160,"line":443},[158,2632,172],{"class":171},[158,2634,2635],{"class":179}," array_length(tags, ",[158,2637,890],{"class":383},[158,2639,1190],{"class":179},[158,2641,446],{"class":171},[158,2643,2456],{"class":179},[158,2645,452],{"class":171},[158,2647,2648],{"class":179}," tags ",[158,2650,1945],{"class":171},[158,2652,2221],{"class":175},[158,2654,1734],{"class":179},[158,2656,2657],{"class":164},"-- NULL, not 0\n",[158,2659,2660,2662,2665,2667,2669,2671,2673,2675,2677,2680],{"class":160,"line":471},[158,2661,172],{"class":171},[158,2663,2664],{"class":179}," cardinality(tags) ",[158,2666,446],{"class":171},[158,2668,2456],{"class":179},[158,2670,452],{"class":171},[158,2672,2648],{"class":179},[158,2674,1945],{"class":171},[158,2676,2221],{"class":175},[158,2678,2679],{"class":179},";        ",[158,2681,2682],{"class":164},"-- 0 (use cardinality)\n",[158,2684,2685,2687,2690,2693,2695,2698,2700,2702,2704,2707],{"class":160,"line":476},[158,2686,172],{"class":171},[158,2688,2689],{"class":383}," coalesce",[158,2691,2692],{"class":179},"(array_length(tags, ",[158,2694,890],{"class":383},[158,2696,2697],{"class":179},"), ",[158,2699,867],{"class":383},[158,2701,1190],{"class":179},[158,2703,446],{"class":171},[158,2705,2706],{"class":179}," posts;         ",[158,2708,2709],{"class":164},"-- 0 (coalesce fallback)\n",[1081,2711,2713],{"id":2712},"unnest-array-to-rows","unnest — Array to Rows",[146,2715,2716],{"language":148},[150,2717,2719],{"className":152,"code":2718,"language":148,"meta":154,"style":154},"-- Expand each post's tags into separate rows (one row per tag)\nSELECT id, unnest(tags) AS tag FROM posts;\n--  id | tag\n--  1  | intro\n--  1  | welcome\n--  2  | news\n\n-- unnest + array_agg are inverses: expand, filter, re-aggregate\nSELECT id, array_agg(tag) AS filtered_tags\nFROM (SELECT id, unnest(tags) AS tag FROM posts) t\nWHERE tag NOT LIKE 'test_%'   -- filter out test tags\nGROUP BY id;\n\n-- Turn a comma-separated string into an array\nSELECT string_to_array('a,b,c', ',');              -- {a,b,c}\nSELECT array_to_string(ARRAY['a','b','c'], ',');   -- a,b,c\n\n-- Multi-column unnest (parallel expansion)\nSELECT unnest(ARRAY[1,2,3]), unnest(ARRAY['a','b','c']);\n--  1 | a\n--  2 | b\n--  3 | c\n",[18,2720,2721,2726,2744,2749,2754,2759,2764,2768,2773,2785,2804,2822,2829,2833,2838,2859,2879,2883,2888,2905,2910,2915],{"__ignoreMap":154},[158,2722,2723],{"class":160,"line":161},[158,2724,2725],{"class":164},"-- Expand each post's tags into separate rows (one row per tag)\n",[158,2727,2728,2730,2733,2736,2739,2741],{"class":160,"line":168},[158,2729,172],{"class":171},[158,2731,2732],{"class":179}," id, unnest(tags) ",[158,2734,2735],{"class":171},"AS",[158,2737,2738],{"class":179}," tag ",[158,2740,446],{"class":171},[158,2742,2743],{"class":179}," posts;\n",[158,2745,2746],{"class":160,"line":191},[158,2747,2748],{"class":164},"--  id | tag\n",[158,2750,2751],{"class":160,"line":204},[158,2752,2753],{"class":164},"--  1  | intro\n",[158,2755,2756],{"class":160,"line":211},[158,2757,2758],{"class":164},"--  1  | welcome\n",[158,2760,2761],{"class":160,"line":217},[158,2762,2763],{"class":164},"--  2  | news\n",[158,2765,2766],{"class":160,"line":234},[158,2767,208],{"emptyLinePlaceholder":207},[158,2769,2770],{"class":160,"line":323},[158,2771,2772],{"class":164},"-- unnest + array_agg are inverses: expand, filter, re-aggregate\n",[158,2774,2775,2777,2780,2782],{"class":160,"line":329},[158,2776,172],{"class":171},[158,2778,2779],{"class":179}," id, array_agg(tag) ",[158,2781,2735],{"class":171},[158,2783,2784],{"class":179}," filtered_tags\n",[158,2786,2787,2789,2791,2793,2795,2797,2799,2801],{"class":160,"line":334},[158,2788,446],{"class":171},[158,2790,2249],{"class":179},[158,2792,172],{"class":171},[158,2794,2732],{"class":179},[158,2796,2735],{"class":171},[158,2798,2738],{"class":179},[158,2800,446],{"class":171},[158,2802,2803],{"class":179}," posts) t\n",[158,2805,2806,2808,2810,2813,2816,2819],{"class":160,"line":339},[158,2807,452],{"class":171},[158,2809,2738],{"class":179},[158,2811,2812],{"class":171},"NOT",[158,2814,2815],{"class":171}," LIKE",[158,2817,2818],{"class":175}," 'test_%'",[158,2820,2821],{"class":164},"   -- filter out test tags\n",[158,2823,2824,2826],{"class":160,"line":345},[158,2825,2320],{"class":171},[158,2827,2828],{"class":179}," id;\n",[158,2830,2831],{"class":160,"line":353},[158,2832,208],{"emptyLinePlaceholder":207},[158,2834,2835],{"class":160,"line":361},[158,2836,2837],{"class":164},"-- Turn a comma-separated string into an array\n",[158,2839,2840,2842,2845,2848,2850,2853,2856],{"class":160,"line":375},[158,2841,172],{"class":171},[158,2843,2844],{"class":179}," string_to_array(",[158,2846,2847],{"class":175},"'a,b,c'",[158,2849,367],{"class":179},[158,2851,2852],{"class":175},"','",[158,2854,2855],{"class":179},");              ",[158,2857,2858],{"class":164},"-- {a,b,c}\n",[158,2860,2861,2863,2866,2868,2871,2873,2876],{"class":160,"line":399},[158,2862,172],{"class":171},[158,2864,2865],{"class":179}," array_to_string(",[158,2867,2286],{"class":171},[158,2869,2870],{"class":179},"['a','b','c'], ",[158,2872,2852],{"class":175},[158,2874,2875],{"class":179},");   ",[158,2877,2878],{"class":164},"-- a,b,c\n",[158,2880,2881],{"class":160,"line":420},[158,2882,208],{"emptyLinePlaceholder":207},[158,2884,2885],{"class":160,"line":437},[158,2886,2887],{"class":164},"-- Multi-column unnest (parallel expansion)\n",[158,2889,2890,2892,2895,2897,2900,2902],{"class":160,"line":443},[158,2891,172],{"class":171},[158,2893,2894],{"class":179}," unnest(",[158,2896,2286],{"class":171},[158,2898,2899],{"class":179},"[1,2,3]), unnest(",[158,2901,2286],{"class":171},[158,2903,2904],{"class":179},"['a','b','c']);\n",[158,2906,2907],{"class":160,"line":471},[158,2908,2909],{"class":164},"--  1 | a\n",[158,2911,2912],{"class":160,"line":476},[158,2913,2914],{"class":164},"--  2 | b\n",[158,2916,2917],{"class":160,"line":482},[158,2918,2919],{"class":164},"--  3 | c\n",[14,2921,2922,2925],{},[18,2923,2924],{},"unnest"," is the bridge between array columns and relational operations — it expands arrays for joins, aggregations, and filtering.",[23,2927],{},[26,2929,2931],{"id":2930},"complex-implementation-event-tracking-with-jsonb","Complex Implementation: Event Tracking with JSONB",[14,2933,2934],{},"A flexible event tracking schema with GIN indexes for containment queries and JSON path for nested extraction:",[146,2936,2937],{"language":148},[150,2938,2940],{"className":152,"code":2939,"language":148,"meta":154,"style":154},"-- ============================================================================\n-- Schema: event tracking platform with variable-structure JSONB payloads\n-- ============================================================================\nCREATE TABLE events (\n  id BIGSERIAL PRIMARY KEY,\n  user_id BIGINT,\n  event_type TEXT NOT NULL,                    -- denormalized for fast filtering\n  payload JSONB NOT NULL,                      -- variable-structure event data\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- GIN index on the full payload: supports @>, ?, ?|, ?& for any path\nCREATE INDEX events_payload_gin ON events USING gin(payload);\n\n-- B-tree index on event_type for equality filtering\nCREATE INDEX events_type_idx ON events(event_type);\n\n-- B-tree index on created_at for time-range queries\nCREATE INDEX events_created_at_idx ON events(created_at);\n\n-- Partial GIN for the hot 'click' event subset (much smaller than full GIN)\nCREATE INDEX events_clicks_gin ON events USING gin(payload)\n  WHERE event_type = 'click';\n\n-- ============================================================================\n-- Insert: events with different payload structures (schema flexibility)\n-- ============================================================================\nINSERT INTO events (user_id, event_type, payload) VALUES\n  (42, 'click', '{\n    \"target\": \"button.submit\",\n    \"page\": \"\u002Fcheckout\",\n    \"metadata\": {\"session_id\": \"abc123\", \"ab_variant\": \"B\"}\n  }'),\n  (42, 'purchase', '{\n    \"order_id\": 9821,\n    \"items\": [\n      {\"sku\": \"WIDGET-001\", \"qty\": 2, \"price\": 19.99},\n      {\"sku\": \"GADGET-002\", \"qty\": 1, \"price\": 49.99}\n    ],\n    \"total\": 89.97\n  }'),\n  (99, 'click', '{\n    \"target\": \"link.nav\",\n    \"page\": \"\u002Fhome\",\n    \"metadata\": {\"session_id\": \"def456\", \"ab_variant\": \"A\"}\n  }');\n\n-- ============================================================================\n-- Query 1: containment — uses the full GIN index (fast)\n-- ============================================================================\n-- All click events on the checkout page\nSELECT id, user_id, payload->>'target' AS target\nFROM events\nWHERE event_type = 'click' AND payload @> '{\"page\": \"\u002Fcheckout\"}';\n-- Uses the event_type B-tree index AND the payload GIN index\n\n-- ============================================================================\n-- Query 2: nested object containment — GIN index handles nested paths\n-- ============================================================================\n-- All events from a specific A\u002FB test variant\nSELECT id, event_type, payload->>'page' AS page\nFROM events\nWHERE payload @> '{\"metadata\": {\"ab_variant\": \"B\"}}';\n-- GIN index scans the nested path — no full-table scan\n\n-- ============================================================================\n-- Query 3: JSON path query for deep extraction\n-- ============================================================================\n-- Extract all item SKUs from purchase events\nSELECT\n  e.id,\n  jsonb_path_query(e.payload, '$.items[*].sku')::text AS sku\nFROM events e\nWHERE e.event_type = 'purchase';\n\n-- Extract items with quantity > 1\nSELECT\n  e.id,\n  jsonb_path_query(e.payload, '$.items[*] ? (@.qty > 1)') AS high_qty_item\nFROM events e\nWHERE e.event_type = 'purchase';\n\n-- ============================================================================\n-- Query 4: aggregate JSON from rows (build a summary JSON object)\n-- ============================================================================\n-- Build a per-user event summary as a JSON array\nSELECT\n  user_id,\n  jsonb_agg(\n    jsonb_build_object(\n      'type', event_type,\n      'page', payload->>'page',\n      'ts', created_at\n    )\n  ) AS event_history\nFROM events\nWHERE created_at >= now() - INTERVAL '24 hours'\nGROUP BY user_id;\n\n-- ============================================================================\n-- Query 5: update a nested field with jsonb_set\n-- ============================================================================\n-- Add a 'processed' flag to an event's metadata\nUPDATE events\nSET payload = jsonb_set(payload, '{metadata,processed}', 'true'::jsonb, true)\nWHERE id = 1;\n-- Path '{metadata,processed}' navigates to payload.metadata.processed\n-- create_if_missing=true creates the key if it doesn't exist\n\n-- ============================================================================\n-- Query 6: array column with GIN for tag-based filtering\n-- ============================================================================\nCREATE TABLE user_tags (\n  user_id BIGINT PRIMARY KEY,\n  tags TEXT[] NOT NULL DEFAULT '{}'\n);\nCREATE INDEX user_tags_gin ON user_tags USING gin(tags);\n\n-- Find users who have all of these tags (containment, uses GIN)\nSELECT user_id FROM user_tags WHERE tags @> array['premium', 'newsletter'];\n\n-- Find users who have any of these tags (overlap, uses GIN)\nSELECT user_id FROM user_tags WHERE tags && array['premium', 'trial'];\n\n-- Expand tags for a report (one row per user-tag pair)\nSELECT user_id, tag FROM user_tags, unnest(tags) AS tag ORDER BY user_id;\n",[18,2941,2942,2947,2952,2956,2966,2976,2986,3001,3013,3031,3035,3039,3044,3060,3064,3069,3082,3086,3091,3105,3109,3114,3130,3143,3147,3151,3157,3162,3173,3191,3197,3203,3209,3218,3234,3240,3246,3252,3258,3264,3270,3277,3293,3299,3305,3311,3318,3323,3328,3334,3339,3345,3364,3371,3393,3399,3404,3409,3415,3420,3426,3444,3451,3465,3471,3476,3481,3487,3492,3498,3504,3516,3543,3550,3569,3574,3580,3585,3596,3619,3626,3643,3648,3653,3659,3664,3670,3675,3681,3687,3693,3702,3717,3726,3732,3743,3750,3774,3782,3787,3792,3798,3803,3809,3816,3837,3852,3858,3864,3869,3874,3880,3885,3897,3908,3924,3929,3948,3953,3959,3982,3987,3993,4013,4018,4024],{"__ignoreMap":154},[158,2943,2944],{"class":160,"line":161},[158,2945,2946],{"class":164},"-- ============================================================================\n",[158,2948,2949],{"class":160,"line":168},[158,2950,2951],{"class":164},"-- Schema: event tracking platform with variable-structure JSONB payloads\n",[158,2953,2954],{"class":160,"line":191},[158,2955,2946],{"class":164},[158,2957,2958,2960,2962,2964],{"class":160,"line":204},[158,2959,261],{"class":171},[158,2961,264],{"class":171},[158,2963,268],{"class":267},[158,2965,271],{"class":179},[158,2967,2968,2970,2972,2974],{"class":160,"line":211},[158,2969,276],{"class":179},[158,2971,279],{"class":171},[158,2973,282],{"class":171},[158,2975,285],{"class":179},[158,2977,2978,2981,2984],{"class":160,"line":217},[158,2979,2980],{"class":179},"  user_id ",[158,2982,2983],{"class":171},"BIGINT",[158,2985,285],{"class":179},[158,2987,2988,2991,2993,2995,2998],{"class":160,"line":234},[158,2989,2990],{"class":179},"  event_type ",[158,2992,2197],{"class":171},[158,2994,2200],{"class":171},[158,2996,2997],{"class":179},",                    ",[158,2999,3000],{"class":164},"-- denormalized for fast filtering\n",[158,3002,3003,3005,3007,3010],{"class":160,"line":323},[158,3004,290],{"class":179},[158,3006,2215],{"class":171},[158,3008,3009],{"class":179},",                      ",[158,3011,3012],{"class":164},"-- variable-structure event data\n",[158,3014,3015,3018,3021,3023,3025,3028],{"class":160,"line":329},[158,3016,3017],{"class":179},"  created_at ",[158,3019,3020],{"class":171},"TIMESTAMPTZ",[158,3022,2200],{"class":171},[158,3024,2218],{"class":171},[158,3026,3027],{"class":171}," now",[158,3029,3030],{"class":179},"()\n",[158,3032,3033],{"class":160,"line":334},[158,3034,298],{"class":179},[158,3036,3037],{"class":160,"line":339},[158,3038,208],{"emptyLinePlaceholder":207},[158,3040,3041],{"class":160,"line":345},[158,3042,3043],{"class":164},"-- GIN index on the full payload: supports @>, ?, ?|, ?& for any path\n",[158,3045,3046,3048,3050,3052,3054,3056,3058],{"class":160,"line":353},[158,3047,261],{"class":171},[158,3049,1271],{"class":171},[158,3051,1274],{"class":267},[158,3053,1277],{"class":171},[158,3055,918],{"class":179},[158,3057,1282],{"class":171},[158,3059,1285],{"class":179},[158,3061,3062],{"class":160,"line":361},[158,3063,208],{"emptyLinePlaceholder":207},[158,3065,3066],{"class":160,"line":375},[158,3067,3068],{"class":164},"-- B-tree index on event_type for equality filtering\n",[158,3070,3071,3073,3075,3077,3079],{"class":160,"line":399},[158,3072,261],{"class":171},[158,3074,1271],{"class":171},[158,3076,1433],{"class":267},[158,3078,1277],{"class":171},[158,3080,3081],{"class":179}," events(event_type);\n",[158,3083,3084],{"class":160,"line":420},[158,3085,208],{"emptyLinePlaceholder":207},[158,3087,3088],{"class":160,"line":437},[158,3089,3090],{"class":164},"-- B-tree index on created_at for time-range queries\n",[158,3092,3093,3095,3097,3100,3102],{"class":160,"line":443},[158,3094,261],{"class":171},[158,3096,1271],{"class":171},[158,3098,3099],{"class":267}," events_created_at_idx",[158,3101,1277],{"class":171},[158,3103,3104],{"class":179}," events(created_at);\n",[158,3106,3107],{"class":160,"line":471},[158,3108,208],{"emptyLinePlaceholder":207},[158,3110,3111],{"class":160,"line":476},[158,3112,3113],{"class":164},"-- Partial GIN for the hot 'click' event subset (much smaller than full GIN)\n",[158,3115,3116,3118,3120,3122,3124,3126,3128],{"class":160,"line":482},[158,3117,261],{"class":171},[158,3119,1271],{"class":171},[158,3121,1582],{"class":267},[158,3123,1277],{"class":171},[158,3125,918],{"class":179},[158,3127,1282],{"class":171},[158,3129,1591],{"class":179},[158,3131,3132,3134,3137,3139,3141],{"class":160,"line":490},[158,3133,1596],{"class":171},[158,3135,3136],{"class":179}," event_type ",[158,3138,1945],{"class":171},[158,3140,1392],{"class":175},[158,3142,468],{"class":179},[158,3144,3145],{"class":160,"line":532},[158,3146,208],{"emptyLinePlaceholder":207},[158,3148,3149],{"class":160,"line":540},[158,3150,2946],{"class":164},[158,3152,3154],{"class":160,"line":3153},26,[158,3155,3156],{"class":164},"-- Insert: events with different payload structures (schema flexibility)\n",[158,3158,3160],{"class":160,"line":3159},27,[158,3161,2946],{"class":164},[158,3163,3165,3167,3170],{"class":160,"line":3164},28,[158,3166,312],{"class":171},[158,3168,3169],{"class":179}," events (user_id, event_type, payload) ",[158,3171,3172],{"class":171},"VALUES\n",[158,3174,3176,3179,3182,3184,3186,3188],{"class":160,"line":3175},29,[158,3177,3178],{"class":179},"  (",[158,3180,3181],{"class":383},"42",[158,3183,367],{"class":179},[158,3185,370],{"class":175},[158,3187,367],{"class":179},[158,3189,3190],{"class":175},"'{\n",[158,3192,3194],{"class":160,"line":3193},30,[158,3195,3196],{"class":175},"    \"target\": \"button.submit\",\n",[158,3198,3200],{"class":160,"line":3199},31,[158,3201,3202],{"class":175},"    \"page\": \"\u002Fcheckout\",\n",[158,3204,3206],{"class":160,"line":3205},32,[158,3207,3208],{"class":175},"    \"metadata\": {\"session_id\": \"abc123\", \"ab_variant\": \"B\"}\n",[158,3210,3212,3215],{"class":160,"line":3211},33,[158,3213,3214],{"class":175},"  }'",[158,3216,3217],{"class":179},"),\n",[158,3219,3221,3223,3225,3227,3230,3232],{"class":160,"line":3220},34,[158,3222,3178],{"class":179},[158,3224,3181],{"class":383},[158,3226,367],{"class":179},[158,3228,3229],{"class":175},"'purchase'",[158,3231,367],{"class":179},[158,3233,3190],{"class":175},[158,3235,3237],{"class":160,"line":3236},35,[158,3238,3239],{"class":175},"    \"order_id\": 9821,\n",[158,3241,3243],{"class":160,"line":3242},36,[158,3244,3245],{"class":175},"    \"items\": [\n",[158,3247,3249],{"class":160,"line":3248},37,[158,3250,3251],{"class":175},"      {\"sku\": \"WIDGET-001\", \"qty\": 2, \"price\": 19.99},\n",[158,3253,3255],{"class":160,"line":3254},38,[158,3256,3257],{"class":175},"      {\"sku\": \"GADGET-002\", \"qty\": 1, \"price\": 49.99}\n",[158,3259,3261],{"class":160,"line":3260},39,[158,3262,3263],{"class":175},"    ],\n",[158,3265,3267],{"class":160,"line":3266},40,[158,3268,3269],{"class":175},"    \"total\": 89.97\n",[158,3271,3273,3275],{"class":160,"line":3272},41,[158,3274,3214],{"class":175},[158,3276,3217],{"class":179},[158,3278,3280,3282,3285,3287,3289,3291],{"class":160,"line":3279},42,[158,3281,3178],{"class":179},[158,3283,3284],{"class":383},"99",[158,3286,367],{"class":179},[158,3288,370],{"class":175},[158,3290,367],{"class":179},[158,3292,3190],{"class":175},[158,3294,3296],{"class":160,"line":3295},43,[158,3297,3298],{"class":175},"    \"target\": \"link.nav\",\n",[158,3300,3302],{"class":160,"line":3301},44,[158,3303,3304],{"class":175},"    \"page\": \"\u002Fhome\",\n",[158,3306,3308],{"class":160,"line":3307},45,[158,3309,3310],{"class":175},"    \"metadata\": {\"session_id\": \"def456\", \"ab_variant\": \"A\"}\n",[158,3312,3314,3316],{"class":160,"line":3313},46,[158,3315,3214],{"class":175},[158,3317,298],{"class":179},[158,3319,3321],{"class":160,"line":3320},47,[158,3322,208],{"emptyLinePlaceholder":207},[158,3324,3326],{"class":160,"line":3325},48,[158,3327,2946],{"class":164},[158,3329,3331],{"class":160,"line":3330},49,[158,3332,3333],{"class":164},"-- Query 1: containment — uses the full GIN index (fast)\n",[158,3335,3337],{"class":160,"line":3336},50,[158,3338,2946],{"class":164},[158,3340,3342],{"class":160,"line":3341},51,[158,3343,3344],{"class":164},"-- All click events on the checkout page\n",[158,3346,3348,3350,3353,3355,3358,3361],{"class":160,"line":3347},52,[158,3349,172],{"class":171},[158,3351,3352],{"class":179}," id, user_id, payload",[158,3354,524],{"class":171},[158,3356,3357],{"class":175},"'target'",[158,3359,3360],{"class":171}," AS",[158,3362,3363],{"class":171}," target\n",[158,3365,3367,3369],{"class":160,"line":3366},53,[158,3368,446],{"class":171},[158,3370,1856],{"class":179},[158,3372,3374,3376,3378,3380,3382,3384,3386,3388,3391],{"class":160,"line":3373},54,[158,3375,452],{"class":171},[158,3377,3136],{"class":179},[158,3379,1945],{"class":171},[158,3381,1392],{"class":175},[158,3383,1634],{"class":171},[158,3385,923],{"class":179},[158,3387,926],{"class":171},[158,3389,3390],{"class":175}," '{\"page\": \"\u002Fcheckout\"}'",[158,3392,468],{"class":179},[158,3394,3396],{"class":160,"line":3395},55,[158,3397,3398],{"class":164},"-- Uses the event_type B-tree index AND the payload GIN index\n",[158,3400,3402],{"class":160,"line":3401},56,[158,3403,208],{"emptyLinePlaceholder":207},[158,3405,3407],{"class":160,"line":3406},57,[158,3408,2946],{"class":164},[158,3410,3412],{"class":160,"line":3411},58,[158,3413,3414],{"class":164},"-- Query 2: nested object containment — GIN index handles nested paths\n",[158,3416,3418],{"class":160,"line":3417},59,[158,3419,2946],{"class":164},[158,3421,3423],{"class":160,"line":3422},60,[158,3424,3425],{"class":164},"-- All events from a specific A\u002FB test variant\n",[158,3427,3429,3431,3434,3436,3439,3441],{"class":160,"line":3428},61,[158,3430,172],{"class":171},[158,3432,3433],{"class":179}," id, event_type, payload",[158,3435,524],{"class":171},[158,3437,3438],{"class":175},"'page'",[158,3440,3360],{"class":171},[158,3442,3443],{"class":171}," page\n",[158,3445,3447,3449],{"class":160,"line":3446},62,[158,3448,446],{"class":171},[158,3450,1856],{"class":179},[158,3452,3454,3456,3458,3460,3463],{"class":160,"line":3453},63,[158,3455,452],{"class":171},[158,3457,923],{"class":179},[158,3459,926],{"class":171},[158,3461,3462],{"class":175}," '{\"metadata\": {\"ab_variant\": \"B\"}}'",[158,3464,468],{"class":179},[158,3466,3468],{"class":160,"line":3467},64,[158,3469,3470],{"class":164},"-- GIN index scans the nested path — no full-table scan\n",[158,3472,3474],{"class":160,"line":3473},65,[158,3475,208],{"emptyLinePlaceholder":207},[158,3477,3479],{"class":160,"line":3478},66,[158,3480,2946],{"class":164},[158,3482,3484],{"class":160,"line":3483},67,[158,3485,3486],{"class":164},"-- Query 3: JSON path query for deep extraction\n",[158,3488,3490],{"class":160,"line":3489},68,[158,3491,2946],{"class":164},[158,3493,3495],{"class":160,"line":3494},69,[158,3496,3497],{"class":164},"-- Extract all item SKUs from purchase events\n",[158,3499,3501],{"class":160,"line":3500},70,[158,3502,3503],{"class":171},"SELECT\n",[158,3505,3507,3510,3512,3514],{"class":160,"line":3506},71,[158,3508,3509],{"class":383},"  e",[158,3511,387],{"class":179},[158,3513,390],{"class":383},[158,3515,285],{"class":179},[158,3517,3519,3522,3524,3526,3528,3530,3533,3535,3538,3540],{"class":160,"line":3518},72,[158,3520,3521],{"class":179},"  jsonb_path_query(",[158,3523,503],{"class":383},[158,3525,387],{"class":179},[158,3527,521],{"class":383},[158,3529,367],{"class":179},[158,3531,3532],{"class":175},"'$.items[*].sku'",[158,3534,1232],{"class":179},[158,3536,3537],{"class":171},"text",[158,3539,3360],{"class":171},[158,3541,3542],{"class":179}," sku\n",[158,3544,3546,3548],{"class":160,"line":3545},73,[158,3547,446],{"class":171},[158,3549,537],{"class":179},[158,3551,3553,3555,3557,3559,3562,3564,3567],{"class":160,"line":3552},74,[158,3554,452],{"class":171},[158,3556,545],{"class":383},[158,3558,387],{"class":179},[158,3560,3561],{"class":383},"event_type",[158,3563,462],{"class":171},[158,3565,3566],{"class":175}," 'purchase'",[158,3568,468],{"class":179},[158,3570,3572],{"class":160,"line":3571},75,[158,3573,208],{"emptyLinePlaceholder":207},[158,3575,3577],{"class":160,"line":3576},76,[158,3578,3579],{"class":164},"-- Extract items with quantity > 1\n",[158,3581,3583],{"class":160,"line":3582},77,[158,3584,3503],{"class":171},[158,3586,3588,3590,3592,3594],{"class":160,"line":3587},78,[158,3589,3509],{"class":383},[158,3591,387],{"class":179},[158,3593,390],{"class":383},[158,3595,285],{"class":179},[158,3597,3599,3601,3603,3605,3607,3609,3612,3614,3616],{"class":160,"line":3598},79,[158,3600,3521],{"class":179},[158,3602,503],{"class":383},[158,3604,387],{"class":179},[158,3606,521],{"class":383},[158,3608,367],{"class":179},[158,3610,3611],{"class":175},"'$.items[*] ? (@.qty > 1)'",[158,3613,1190],{"class":179},[158,3615,2735],{"class":171},[158,3617,3618],{"class":179}," high_qty_item\n",[158,3620,3622,3624],{"class":160,"line":3621},80,[158,3623,446],{"class":171},[158,3625,537],{"class":179},[158,3627,3629,3631,3633,3635,3637,3639,3641],{"class":160,"line":3628},81,[158,3630,452],{"class":171},[158,3632,545],{"class":383},[158,3634,387],{"class":179},[158,3636,3561],{"class":383},[158,3638,462],{"class":171},[158,3640,3566],{"class":175},[158,3642,468],{"class":179},[158,3644,3646],{"class":160,"line":3645},82,[158,3647,208],{"emptyLinePlaceholder":207},[158,3649,3651],{"class":160,"line":3650},83,[158,3652,2946],{"class":164},[158,3654,3656],{"class":160,"line":3655},84,[158,3657,3658],{"class":164},"-- Query 4: aggregate JSON from rows (build a summary JSON object)\n",[158,3660,3662],{"class":160,"line":3661},85,[158,3663,2946],{"class":164},[158,3665,3667],{"class":160,"line":3666},86,[158,3668,3669],{"class":164},"-- Build a per-user event summary as a JSON array\n",[158,3671,3673],{"class":160,"line":3672},87,[158,3674,3503],{"class":171},[158,3676,3678],{"class":160,"line":3677},88,[158,3679,3680],{"class":179},"  user_id,\n",[158,3682,3684],{"class":160,"line":3683},89,[158,3685,3686],{"class":179},"  jsonb_agg(\n",[158,3688,3690],{"class":160,"line":3689},90,[158,3691,3692],{"class":179},"    jsonb_build_object(\n",[158,3694,3696,3699],{"class":160,"line":3695},91,[158,3697,3698],{"class":175},"      'type'",[158,3700,3701],{"class":179},", event_type,\n",[158,3703,3705,3708,3711,3713,3715],{"class":160,"line":3704},92,[158,3706,3707],{"class":175},"      'page'",[158,3709,3710],{"class":179},", payload",[158,3712,524],{"class":171},[158,3714,3438],{"class":175},[158,3716,285],{"class":179},[158,3718,3720,3723],{"class":160,"line":3719},93,[158,3721,3722],{"class":175},"      'ts'",[158,3724,3725],{"class":179},", created_at\n",[158,3727,3729],{"class":160,"line":3728},94,[158,3730,3731],{"class":179},"    )\n",[158,3733,3735,3738,3740],{"class":160,"line":3734},95,[158,3736,3737],{"class":179},"  ) ",[158,3739,2735],{"class":171},[158,3741,3742],{"class":179}," event_history\n",[158,3744,3746,3748],{"class":160,"line":3745},96,[158,3747,446],{"class":171},[158,3749,1856],{"class":179},[158,3751,3753,3755,3758,3761,3763,3766,3768,3771],{"class":160,"line":3752},97,[158,3754,452],{"class":171},[158,3756,3757],{"class":179}," created_at ",[158,3759,3760],{"class":171},">=",[158,3762,3027],{"class":171},[158,3764,3765],{"class":179},"() ",[158,3767,2105],{"class":171},[158,3769,3770],{"class":179}," INTERVAL ",[158,3772,3773],{"class":175},"'24 hours'\n",[158,3775,3777,3779],{"class":160,"line":3776},98,[158,3778,2320],{"class":171},[158,3780,3781],{"class":179}," user_id;\n",[158,3783,3785],{"class":160,"line":3784},99,[158,3786,208],{"emptyLinePlaceholder":207},[158,3788,3790],{"class":160,"line":3789},100,[158,3791,2946],{"class":164},[158,3793,3795],{"class":160,"line":3794},101,[158,3796,3797],{"class":164},"-- Query 5: update a nested field with jsonb_set\n",[158,3799,3801],{"class":160,"line":3800},102,[158,3802,2946],{"class":164},[158,3804,3806],{"class":160,"line":3805},103,[158,3807,3808],{"class":164},"-- Add a 'processed' flag to an event's metadata\n",[158,3810,3812,3814],{"class":160,"line":3811},104,[158,3813,1934],{"class":171},[158,3815,1856],{"class":179},[158,3817,3819,3821,3823,3825,3827,3830,3832,3834],{"class":160,"line":3818},105,[158,3820,1939],{"class":171},[158,3822,1942],{"class":179},[158,3824,1945],{"class":171},[158,3826,1948],{"class":179},[158,3828,3829],{"class":175},"'{metadata,processed}'",[158,3831,367],{"class":179},[158,3833,2000],{"class":175},[158,3835,3836],{"class":179},"::jsonb, true)\n",[158,3838,3840,3842,3845,3847,3850],{"class":160,"line":3839},106,[158,3841,452],{"class":171},[158,3843,3844],{"class":179}," id ",[158,3846,1945],{"class":171},[158,3848,3849],{"class":383}," 1",[158,3851,468],{"class":179},[158,3853,3855],{"class":160,"line":3854},107,[158,3856,3857],{"class":164},"-- Path '{metadata,processed}' navigates to payload.metadata.processed\n",[158,3859,3861],{"class":160,"line":3860},108,[158,3862,3863],{"class":164},"-- create_if_missing=true creates the key if it doesn't exist\n",[158,3865,3867],{"class":160,"line":3866},109,[158,3868,208],{"emptyLinePlaceholder":207},[158,3870,3872],{"class":160,"line":3871},110,[158,3873,2946],{"class":164},[158,3875,3877],{"class":160,"line":3876},111,[158,3878,3879],{"class":164},"-- Query 6: array column with GIN for tag-based filtering\n",[158,3881,3883],{"class":160,"line":3882},112,[158,3884,2946],{"class":164},[158,3886,3888,3890,3892,3895],{"class":160,"line":3887},113,[158,3889,261],{"class":171},[158,3891,264],{"class":171},[158,3893,3894],{"class":267}," user_tags",[158,3896,271],{"class":179},[158,3898,3900,3902,3904,3906],{"class":160,"line":3899},114,[158,3901,2980],{"class":179},[158,3903,2983],{"class":171},[158,3905,282],{"class":171},[158,3907,285],{"class":179},[158,3909,3911,3913,3915,3917,3919,3921],{"class":160,"line":3910},115,[158,3912,2207],{"class":179},[158,3914,2197],{"class":171},[158,3916,2212],{"class":179},[158,3918,2215],{"class":171},[158,3920,2218],{"class":171},[158,3922,3923],{"class":175}," '{}'\n",[158,3925,3927],{"class":160,"line":3926},116,[158,3928,298],{"class":179},[158,3930,3932,3934,3936,3939,3941,3944,3946],{"class":160,"line":3931},117,[158,3933,261],{"class":171},[158,3935,1271],{"class":171},[158,3937,3938],{"class":267}," user_tags_gin",[158,3940,1277],{"class":171},[158,3942,3943],{"class":179}," user_tags ",[158,3945,1282],{"class":171},[158,3947,2581],{"class":179},[158,3949,3951],{"class":160,"line":3950},118,[158,3952,208],{"emptyLinePlaceholder":207},[158,3954,3956],{"class":160,"line":3955},119,[158,3957,3958],{"class":164},"-- Find users who have all of these tags (containment, uses GIN)\n",[158,3960,3962,3964,3967,3969,3971,3973,3975,3977,3979],{"class":160,"line":3961},120,[158,3963,172],{"class":171},[158,3965,3966],{"class":179}," user_id ",[158,3968,446],{"class":171},[158,3970,3943],{"class":179},[158,3972,452],{"class":171},[158,3974,2484],{"class":179},[158,3976,926],{"class":171},[158,3978,2489],{"class":171},[158,3980,3981],{"class":179},"['premium', 'newsletter'];\n",[158,3983,3985],{"class":160,"line":3984},121,[158,3986,208],{"emptyLinePlaceholder":207},[158,3988,3990],{"class":160,"line":3989},122,[158,3991,3992],{"class":164},"-- Find users who have any of these tags (overlap, uses GIN)\n",[158,3994,3996,3998,4000,4002,4004,4006,4008,4010],{"class":160,"line":3995},123,[158,3997,172],{"class":171},[158,3999,3966],{"class":179},[158,4001,446],{"class":171},[158,4003,3943],{"class":179},[158,4005,452],{"class":171},[158,4007,2549],{"class":179},[158,4009,1035],{"class":171},[158,4011,4012],{"class":179},"['premium', 'trial'];\n",[158,4014,4016],{"class":160,"line":4015},124,[158,4017,208],{"emptyLinePlaceholder":207},[158,4019,4021],{"class":160,"line":4020},125,[158,4022,4023],{"class":164},"-- Expand tags for a report (one row per user-tag pair)\n",[158,4025,4027,4029,4032,4034,4037,4039,4041,4044],{"class":160,"line":4026},126,[158,4028,172],{"class":171},[158,4030,4031],{"class":179}," user_id, tag ",[158,4033,446],{"class":171},[158,4035,4036],{"class":179}," user_tags, unnest(tags) ",[158,4038,2735],{"class":171},[158,4040,2738],{"class":179},[158,4042,4043],{"class":171},"ORDER BY",[158,4045,3781],{"class":179},[14,4047,4048,4051],{},[136,4049,4050],{},"Key design decisions",":",[4053,4054,4055,4064,4077,4086],"ol",{},[4056,4057,4058,4063],"li",{},[136,4059,4060,4062],{},[18,4061,3561],{}," denormalized"," — a B-tree index on a text column is cheaper than extracting from JSONB for the most common filter.",[4056,4065,4066,4071,4072,1405,4074,4076],{},[136,4067,4068,4069],{},"Full GIN on ",[18,4070,521],{}," — supports any ",[18,4073,669],{},[18,4075,704],{}," query without per-path expression indexes.",[4056,4078,4079,4085],{},[136,4080,4081,4082,268],{},"Partial GIN for ",[18,4083,4084],{},"click"," — the hot subset gets a smaller, faster index.",[4056,4087,4088,4091,4092,4094],{},[136,4089,4090],{},"JSON path for deep extraction"," — ",[18,4093,1757],{}," handles nested arrays and filters.",[23,4096],{},[26,4098,4100],{"id":4099},"anti-pattern-jsonb-for-everything","Anti-Pattern: JSONB for Everything",[146,4102,4103],{"language":148},[150,4104,4106],{"className":152,"code":4105,"language":148,"meta":154,"style":154},"-- ❌ WRONG: stuffing structured data into JSONB when a relational schema is better\nCREATE TABLE bad_orders (\n  id BIGSERIAL PRIMARY KEY,\n  data JSONB NOT NULL   -- {\"customer_id\": 42, \"amount\": 99.95, \"status\": \"shipped\", ...}\n);\n\n-- Now every query extracts fields: slow, no type safety, no foreign keys\nSELECT * FROM bad_orders WHERE (data->>'customer_id')::int = 42;\nSELECT * FROM bad_orders WHERE (data->>'amount')::numeric > 100;\n-- No FK to customers, no CHECK constraints, no type safety, slow queries\n\n-- ✅ RIGHT: relational schema for structured data, JSONB only for variable parts\nCREATE TABLE orders (\n  id BIGSERIAL PRIMARY KEY,\n  customer_id BIGINT NOT NULL REFERENCES customers(id),  -- FK enforced\n  amount NUMERIC(12,2) NOT NULL CHECK (amount >= 0),     -- type + constraint\n  status TEXT NOT NULL DEFAULT 'pending',\n  metadata JSONB                                          -- variable extra data only\n);\n\nCREATE INDEX orders_customer_idx ON orders(customer_id);\nCREATE INDEX orders_status_idx ON orders(status);\n",[18,4107,4108,4113,4124,4134,4147,4151,4155,4160,4191,4219,4224,4228,4233,4244,4254,4272,4313,4330,4338,4342,4346,4360],{"__ignoreMap":154},[158,4109,4110],{"class":160,"line":161},[158,4111,4112],{"class":164},"-- ❌ WRONG: stuffing structured data into JSONB when a relational schema is better\n",[158,4114,4115,4117,4119,4122],{"class":160,"line":168},[158,4116,261],{"class":171},[158,4118,264],{"class":171},[158,4120,4121],{"class":267}," bad_orders",[158,4123,271],{"class":179},[158,4125,4126,4128,4130,4132],{"class":160,"line":191},[158,4127,276],{"class":179},[158,4129,279],{"class":171},[158,4131,282],{"class":171},[158,4133,285],{"class":179},[158,4135,4136,4139,4142,4144],{"class":160,"line":204},[158,4137,4138],{"class":171},"  data",[158,4140,4141],{"class":179}," JSONB ",[158,4143,2215],{"class":171},[158,4145,4146],{"class":164},"   -- {\"customer_id\": 42, \"amount\": 99.95, \"status\": \"shipped\", ...}\n",[158,4148,4149],{"class":160,"line":211},[158,4150,298],{"class":179},[158,4152,4153],{"class":160,"line":217},[158,4154,208],{"emptyLinePlaceholder":207},[158,4156,4157],{"class":160,"line":234},[158,4158,4159],{"class":164},"-- Now every query extracts fields: slow, no type safety, no foreign keys\n",[158,4161,4162,4164,4166,4168,4171,4173,4175,4178,4181,4183,4185,4187,4189],{"class":160,"line":323},[158,4163,172],{"class":171},[158,4165,913],{"class":171},[158,4167,782],{"class":171},[158,4169,4170],{"class":179}," bad_orders ",[158,4172,452],{"class":171},[158,4174,2249],{"class":179},[158,4176,4177],{"class":171},"data->>",[158,4179,4180],{"class":175},"'customer_id'",[158,4182,1232],{"class":179},[158,4184,1517],{"class":171},[158,4186,462],{"class":171},[158,4188,1552],{"class":383},[158,4190,468],{"class":179},[158,4192,4193,4195,4197,4199,4201,4203,4205,4207,4209,4211,4213,4215,4217],{"class":160,"line":329},[158,4194,172],{"class":171},[158,4196,913],{"class":171},[158,4198,782],{"class":171},[158,4200,4170],{"class":179},[158,4202,452],{"class":171},[158,4204,2249],{"class":179},[158,4206,4177],{"class":171},[158,4208,1187],{"class":175},[158,4210,1232],{"class":179},[158,4212,1235],{"class":171},[158,4214,1238],{"class":171},[158,4216,1195],{"class":383},[158,4218,468],{"class":179},[158,4220,4221],{"class":160,"line":334},[158,4222,4223],{"class":164},"-- No FK to customers, no CHECK constraints, no type safety, slow queries\n",[158,4225,4226],{"class":160,"line":339},[158,4227,208],{"emptyLinePlaceholder":207},[158,4229,4230],{"class":160,"line":345},[158,4231,4232],{"class":164},"-- ✅ RIGHT: relational schema for structured data, JSONB only for variable parts\n",[158,4234,4235,4237,4239,4242],{"class":160,"line":353},[158,4236,261],{"class":171},[158,4238,264],{"class":171},[158,4240,4241],{"class":267}," orders",[158,4243,271],{"class":179},[158,4245,4246,4248,4250,4252],{"class":160,"line":361},[158,4247,276],{"class":179},[158,4249,279],{"class":171},[158,4251,282],{"class":171},[158,4253,285],{"class":179},[158,4255,4256,4259,4261,4263,4266,4269],{"class":160,"line":375},[158,4257,4258],{"class":179},"  customer_id ",[158,4260,2983],{"class":171},[158,4262,2200],{"class":171},[158,4264,4265],{"class":171}," REFERENCES",[158,4267,4268],{"class":179}," customers(id),  ",[158,4270,4271],{"class":164},"-- FK enforced\n",[158,4273,4274,4277,4280,4283,4286,4289,4292,4294,4296,4299,4302,4304,4307,4310],{"class":160,"line":399},[158,4275,4276],{"class":179},"  amount ",[158,4278,4279],{"class":171},"NUMERIC",[158,4281,4282],{"class":179},"(",[158,4284,4285],{"class":383},"12",[158,4287,4288],{"class":179},",",[158,4290,4291],{"class":383},"2",[158,4293,1190],{"class":179},[158,4295,2215],{"class":171},[158,4297,4298],{"class":171}," CHECK",[158,4300,4301],{"class":179}," (amount ",[158,4303,3760],{"class":171},[158,4305,4306],{"class":383}," 0",[158,4308,4309],{"class":179},"),     ",[158,4311,4312],{"class":164},"-- type + constraint\n",[158,4314,4315,4318,4321,4323,4325,4328],{"class":160,"line":420},[158,4316,4317],{"class":171},"  status",[158,4319,4320],{"class":171}," TEXT",[158,4322,2200],{"class":171},[158,4324,2218],{"class":171},[158,4326,4327],{"class":175}," 'pending'",[158,4329,285],{"class":179},[158,4331,4332,4335],{"class":160,"line":437},[158,4333,4334],{"class":179},"  metadata JSONB                                          ",[158,4336,4337],{"class":164},"-- variable extra data only\n",[158,4339,4340],{"class":160,"line":443},[158,4341,298],{"class":179},[158,4343,4344],{"class":160,"line":471},[158,4345,208],{"emptyLinePlaceholder":207},[158,4347,4348,4350,4352,4355,4357],{"class":160,"line":476},[158,4349,261],{"class":171},[158,4351,1271],{"class":171},[158,4353,4354],{"class":267}," orders_customer_idx",[158,4356,1277],{"class":171},[158,4358,4359],{"class":179}," orders(customer_id);\n",[158,4361,4362,4364,4366,4369,4371,4374,4377],{"class":160,"line":482},[158,4363,261],{"class":171},[158,4365,1271],{"class":171},[158,4367,4368],{"class":267}," orders_status_idx",[158,4370,1277],{"class":171},[158,4372,4373],{"class":179}," orders(",[158,4375,4376],{"class":171},"status",[158,4378,298],{"class":179},[14,4380,4381,4051],{},[136,4382,4383],{},"When to use what",[4385,4386,4387,4393,4399],"ul",{},[4056,4388,4389,4392],{},[136,4390,4391],{},"Normalized tables"," — structured data queried independently, joined often, with FK constraints.",[4056,4394,4395,4398],{},[136,4396,4397],{},"Arrays"," — homogeneous lists (tags, categories) always read\u002Fwritten together.",[4056,4400,4401,4403],{},[136,4402,20],{}," — variable-structure data (event payloads, API responses, user settings) where the schema evolves per row.",[23,4405],{},[26,4407,4409],{"id":4408},"anti-pattern-querying-jsonb-without-gin-index","Anti-Pattern: Querying JSONB Without GIN Index",[146,4411,4412],{"language":148},[150,4413,4415],{"className":152,"code":4414,"language":148,"meta":154,"style":154},"-- ❌ WRONG: containment query without a GIN index → full table scan\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';\n-- Without a GIN index, PostgreSQL must parse every row's JSONB to check containment\n-- O(n) over all rows — slow on large tables\n\n-- ✅ RIGHT: create a GIN index, then containment queries use it\nCREATE INDEX events_payload_gin ON events USING gin(payload);\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';\n-- GIN index: O(matches) — fast even on millions of rows\n\n-- ⚠️ BUT: ->> extraction + comparison does NOT use the full GIN index\nSELECT * FROM events WHERE payload->>'type' = 'click';\n-- Needs a separate expression index: CREATE INDEX ON events ((payload->>'type'));\n",[18,4416,4417,4422,4442,4447,4452,4456,4461,4477,4497,4502,4506,4511,4535],{"__ignoreMap":154},[158,4418,4419],{"class":160,"line":161},[158,4420,4421],{"class":164},"-- ❌ WRONG: containment query without a GIN index → full table scan\n",[158,4423,4424,4426,4428,4430,4432,4434,4436,4438,4440],{"class":160,"line":168},[158,4425,172],{"class":171},[158,4427,913],{"class":171},[158,4429,782],{"class":171},[158,4431,918],{"class":179},[158,4433,452],{"class":171},[158,4435,923],{"class":179},[158,4437,926],{"class":171},[158,4439,929],{"class":175},[158,4441,468],{"class":179},[158,4443,4444],{"class":160,"line":191},[158,4445,4446],{"class":164},"-- Without a GIN index, PostgreSQL must parse every row's JSONB to check containment\n",[158,4448,4449],{"class":160,"line":204},[158,4450,4451],{"class":164},"-- O(n) over all rows — slow on large tables\n",[158,4453,4454],{"class":160,"line":211},[158,4455,208],{"emptyLinePlaceholder":207},[158,4457,4458],{"class":160,"line":217},[158,4459,4460],{"class":164},"-- ✅ RIGHT: create a GIN index, then containment queries use it\n",[158,4462,4463,4465,4467,4469,4471,4473,4475],{"class":160,"line":234},[158,4464,261],{"class":171},[158,4466,1271],{"class":171},[158,4468,1274],{"class":267},[158,4470,1277],{"class":171},[158,4472,918],{"class":179},[158,4474,1282],{"class":171},[158,4476,1285],{"class":179},[158,4478,4479,4481,4483,4485,4487,4489,4491,4493,4495],{"class":160,"line":323},[158,4480,172],{"class":171},[158,4482,913],{"class":171},[158,4484,782],{"class":171},[158,4486,918],{"class":179},[158,4488,452],{"class":171},[158,4490,923],{"class":179},[158,4492,926],{"class":171},[158,4494,929],{"class":175},[158,4496,468],{"class":179},[158,4498,4499],{"class":160,"line":329},[158,4500,4501],{"class":164},"-- GIN index: O(matches) — fast even on millions of rows\n",[158,4503,4504],{"class":160,"line":334},[158,4505,208],{"emptyLinePlaceholder":207},[158,4507,4508],{"class":160,"line":339},[158,4509,4510],{"class":164},"-- ⚠️ BUT: ->> extraction + comparison does NOT use the full GIN index\n",[158,4512,4513,4515,4517,4519,4521,4523,4525,4527,4529,4531,4533],{"class":160,"line":345},[158,4514,172],{"class":171},[158,4516,913],{"class":171},[158,4518,782],{"class":171},[158,4520,918],{"class":179},[158,4522,452],{"class":171},[158,4524,775],{"class":179},[158,4526,524],{"class":171},[158,4528,512],{"class":175},[158,4530,462],{"class":171},[158,4532,1392],{"class":175},[158,4534,468],{"class":179},[158,4536,4537],{"class":160,"line":353},[158,4538,4539],{"class":164},"-- Needs a separate expression index: CREATE INDEX ON events ((payload->>'type'));\n",[23,4541],{},[26,4543,4545],{"id":4544},"tips-tricks","💡 Tips & Tricks",[4385,4547,4548,4564,4573,4596,4608,4623,4640],{},[4056,4549,4550,4553,4554,4557,4558,4561,4562,387],{},[136,4551,4552],{},"Idiom"," — prefer ",[18,4555,4556],{},"payload @> '{\"key\": \"value\"}'"," (containment) over ",[18,4559,4560],{},"payload->>'key' = 'value'"," (extraction + comparison): containment can use a full GIN index on the JSONB column, while extraction+comparison needs a per-path expression index. Design queries around ",[18,4563,669],{},[4056,4565,4566,4568,4569,4572],{},[136,4567,4552],{}," — use ",[18,4570,4571],{},"jsonb_build_object(...)"," to construct JSONB from columns: cleaner and less error-prone than string concatenation, handles quoting\u002Fescaping correctly.",[4056,4574,4575,4578,4579,4582,4583,367,4585,367,4587,367,4590,4592,4593,4595],{},[136,4576,4577],{},"Performance"," — a ",[136,4580,4581],{},"GIN index on a JSONB column"," is the single most powerful JSONB optimization: it accelerates ",[18,4584,669],{},[18,4586,704],{},[18,4588,4589],{},"?|",[18,4591,735],{}," for any path. For a hot subset, use a partial GIN (",[18,4594,1074],{},") to shrink the index.",[4056,4597,4598,4568,4600,4603,4604,4607],{},[136,4599,4552],{},[18,4601,4602],{},"unnest(array_col)"," to expand an array column into rows for joins\u002Faggregation, and ",[18,4605,4606],{},"array_agg(col)"," to fold rows back into an array: the two are inverses.",[4056,4609,4610,4568,4612,4614,4615,4618,4619,1405,4621,387],{},[136,4611,4552],{},[18,4613,650],{}," for deep path extraction: ",[18,4616,4617],{},"payload #>> '{user,address,city}'"," extracts a nested value as text in one expression, cleaner than chaining ",[18,4620,595],{},[18,4622,524],{},[4056,4624,4625,4568,4627,4629,4630,4632,4633,1405,4635,1405,4637,4639],{},[136,4626,4552],{},[18,4628,1742],{}," GIN for containment-only JSONB: smaller index, but only ",[18,4631,669],{}," (no ",[18,4634,704],{},[18,4636,4589],{},[18,4638,735],{},").",[4056,4641,4642,4645,4646,4649,4650,4652,4653,4656,4657,1405,4659,4661,4662,1405,4664,4666,4667,4669],{},[136,4643,4644],{},"Portability"," — JSONB is PostgreSQL-specific (MySQL has ",[18,4647,4648],{},"JSON",", SQLite stores JSON as ",[18,4651,2197],{}," with ",[18,4654,4655],{},"json_extract","). The ",[18,4658,595],{},[18,4660,524],{}," operators are PostgreSQL (MySQL uses ",[18,4663,595],{},[18,4665,524],{}," too but with some differences; SQLite uses ",[18,4668,4655],{},"). JSON querying is one of the least portable areas.",[23,4671],{},[26,4673,4675],{"id":4674},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[4385,4677,4678,4697,4717,4744,4760,4792,4801,4816,4832,4845,4864,4891],{},[4056,4679,4680,4685,4686,4689,4690,4693,4694,4696],{},[136,4681,4682,4684],{},[18,4683,52],{}," deduplicates keys",": ",[18,4687,4688],{},"jsonb '{\"a\": 1, \"a\": 2}'"," keeps only the last value (",[18,4691,4692],{},"{\"a\": 2}","). ",[18,4695,47],{}," preserves both. Usually fine, but surprising if you expected duplicates.",[4056,4698,4699,4685,4704,4707,4708,4711,4712,4714,4715,387],{},[136,4700,4701,4703],{},[18,4702,52],{}," doesn't preserve key order",[18,4705,4706],{},"jsonb '{\"b\": 1, \"a\": 2}'"," may display as ",[18,4709,4710],{},"{\"a\": 2, \"b\": 1}"," (keys sorted). ",[18,4713,47],{}," preserves input order. If order matters (rare), use ",[18,4716,47],{},[4056,4718,4719,4685,4727,4729,4730,4733,4734,4737,4738,4740,4741,4743],{},[136,4720,4721,4723,4724,4726],{},[18,4722,595],{}," returns JSONB, ",[18,4725,524],{}," returns text",[18,4728,603],{}," is JSONB (you can chain ",[18,4731,4732],{},"->'id'","); ",[18,4735,4736],{},"payload->>'user'"," is text (you can't chain). Use ",[18,4739,595],{}," for nested traversal, ",[18,4742,524],{}," for the final scalar.",[4056,4745,4746,4685,4749,4752,4753,4756,4757,387],{},[136,4747,4748],{},"Numbers in JSONB",[18,4750,4751],{},"payload->>'amount'"," returns text — ",[18,4754,4755],{},"WHERE payload->>'amount' > 100"," does string comparison, not numeric. Cast: ",[18,4758,4759],{},"WHERE (payload->>'amount')::numeric > 100",[4056,4761,4762,4685,4767,4770,4771,4773,4774,4777,4778,4780,4781,4784,4785,4788,4789,4791],{},[136,4763,4764,4766],{},[18,4765,669],{}," requires matching types",[18,4768,4769],{},"payload @> '{\"id\": 42}'"," matches only if ",[18,4772,390],{}," is stored as a number. If it's stored as a string ",[18,4775,4776],{},"\"42\"",", containment fails. JSONB preserves the input type. Also: ",[18,4779,890],{}," and ",[18,4782,4783],{},"1.0"," are equal in JSONB (numeric equality), but ",[18,4786,4787],{},"\"1\""," (string) and ",[18,4790,890],{}," (number) are not.",[4056,4793,4794,4797,4798,4800],{},[136,4795,4796],{},"GIN index size",": a full GIN index on a large JSONB column can be huge (indexes every key\u002Fvalue). For large tables, prefer expression indexes on the specific paths you query, or ",[18,4799,1742],{}," (smaller, containment-only).",[4056,4802,4803,4812,4813,387],{},[136,4804,4805,4807,4808,4811],{},[18,4806,2151],{}," creates the key only if ",[18,4809,4810],{},"create_if_missing"," is true"," (4th arg, default true). To set only existing keys, pass ",[18,4814,4815],{},"false",[4056,4817,4818,4685,4822,4825,4826,4829,4830,2152],{},[136,4819,4820,2147],{},[18,4821,2063],{},[18,4823,4824],{},"payload || '{\"user\": {\"name\": \"Bob\"}}'"," replaces the entire ",[18,4827,4828],{},"user"," object, it doesn't deep-merge. For deep merge, use ",[18,4831,2151],{},[4056,4833,4834,4685,4837,4840,4841,4844],{},[136,4835,4836],{},"Arrays are 1-indexed",[18,4838,4839],{},"arr[1]"," is the first element, ",[18,4842,4843],{},"arr[0]"," is NULL (not an error). Coming from a 0-indexed language, this is a common off-by-one.",[4056,4846,4847,4685,4853,4856,4857,4860,4861,4863],{},[136,4848,4849,4852],{},[18,4850,4851],{},"array_length(arr, 1)"," is NULL for empty arrays",[18,4854,4855],{},"array_length('{}', 1)"," = NULL, not 0. Use ",[18,4858,4859],{},"coalesce(array_length(arr, 1), 0)"," for 0, or ",[18,4862,2428],{}," (returns 0 for empty).",[4056,4865,4866,4869,4870,4873,4874,4877,4878,4881,4882,4884,4885,4888,4889,387],{},[136,4867,4868],{},"NULL vs JSON null",": SQL ",[18,4871,4872],{},"NULL"," (no value) is different from JSON ",[18,4875,4876],{},"null"," (a JSON value). ",[18,4879,4880],{},"payload->>'key'"," returns SQL ",[18,4883,4872],{}," if the key doesn't exist, but returns the text ",[18,4886,4887],{},"'null'"," if the key exists with value ",[18,4890,4876],{},[4056,4892,4893,4896],{},[136,4894,4895],{},"Large JSONB values and TOAST",": very large JSONB values are TOASTed (compressed and stored out-of-line). This is automatic but can affect query performance for large payloads — avoid storing multi-MB JSONB if you query it frequently.",[23,4898],{},[26,4900,4902],{"id":4901},"spot-the-bug","🧠 Spot the Bug",[14,4904,4905,4906,4909],{},"A developer creates a GIN index on a JSONB column and runs a containment query, but ",[18,4907,4908],{},"EXPLAIN"," shows a sequential scan instead of an index scan:",[146,4911,4912],{"language":148},[150,4913,4915],{"className":152,"code":4914,"language":148,"meta":154,"style":154},"CREATE INDEX events_payload_gin ON events USING gin(payload);\n\nSELECT * FROM events WHERE payload->>'type' = 'click';\n-- EXPLAIN shows Seq Scan, not Bitmap Index Scan\n",[18,4916,4917,4933,4937,4961],{"__ignoreMap":154},[158,4918,4919,4921,4923,4925,4927,4929,4931],{"class":160,"line":161},[158,4920,261],{"class":171},[158,4922,1271],{"class":171},[158,4924,1274],{"class":267},[158,4926,1277],{"class":171},[158,4928,918],{"class":179},[158,4930,1282],{"class":171},[158,4932,1285],{"class":179},[158,4934,4935],{"class":160,"line":168},[158,4936,208],{"emptyLinePlaceholder":207},[158,4938,4939,4941,4943,4945,4947,4949,4951,4953,4955,4957,4959],{"class":160,"line":191},[158,4940,172],{"class":171},[158,4942,913],{"class":171},[158,4944,782],{"class":171},[158,4946,918],{"class":179},[158,4948,452],{"class":171},[158,4950,775],{"class":179},[158,4952,524],{"class":171},[158,4954,512],{"class":175},[158,4956,462],{"class":171},[158,4958,1392],{"class":175},[158,4960,468],{"class":179},[158,4962,4963],{"class":160,"line":204},[158,4964,4965],{"class":164},"-- EXPLAIN shows Seq Scan, not Bitmap Index Scan\n",[4967,4968,4969,4973,5009,5095],"details",{},[4970,4971,4972],"summary",{},"Answer",[14,4974,4975,4976,4978,4979,4982,4983,367,4985,367,4987,367,4989,2697,4991,4994,4995,4997,4998,5000,5001,5004,5005,5008],{},"The GIN index on ",[18,4977,521],{}," accelerates ",[136,4980,4981],{},"containment"," operators (",[18,4984,669],{},[18,4986,704],{},[18,4988,4589],{},[18,4990,735],{},[136,4992,4993],{},"not"," extraction + comparison (",[18,4996,524],{}," + ",[18,4999,1945],{},"). The query ",[18,5002,5003],{},"payload->>'type' = 'click'"," extracts the ",[18,5006,5007],{},"type"," field as text and compares it — the GIN index can't serve this operator pattern.",[146,5010,5011],{"language":148},[150,5012,5014],{"className":152,"code":5013,"language":148,"meta":154,"style":154},"-- ✅ Fix 1: rewrite as containment (uses the GIN index)\nSELECT * FROM events WHERE payload @> '{\"type\": \"click\"}';\n\n-- ✅ Fix 2: add an expression index for the specific path\nCREATE INDEX events_type_idx ON events ((payload->>'type'));\nSELECT * FROM events WHERE payload->>'type' = 'click';   -- now uses the expression index\n",[18,5015,5016,5021,5041,5045,5050,5068],{"__ignoreMap":154},[158,5017,5018],{"class":160,"line":161},[158,5019,5020],{"class":164},"-- ✅ Fix 1: rewrite as containment (uses the GIN index)\n",[158,5022,5023,5025,5027,5029,5031,5033,5035,5037,5039],{"class":160,"line":168},[158,5024,172],{"class":171},[158,5026,913],{"class":171},[158,5028,782],{"class":171},[158,5030,918],{"class":179},[158,5032,452],{"class":171},[158,5034,923],{"class":179},[158,5036,926],{"class":171},[158,5038,929],{"class":175},[158,5040,468],{"class":179},[158,5042,5043],{"class":160,"line":191},[158,5044,208],{"emptyLinePlaceholder":207},[158,5046,5047],{"class":160,"line":204},[158,5048,5049],{"class":164},"-- ✅ Fix 2: add an expression index for the specific path\n",[158,5051,5052,5054,5056,5058,5060,5062,5064,5066],{"class":160,"line":211},[158,5053,261],{"class":171},[158,5055,1271],{"class":171},[158,5057,1433],{"class":267},[158,5059,1277],{"class":171},[158,5061,1438],{"class":179},[158,5063,524],{"class":171},[158,5065,512],{"class":175},[158,5067,1445],{"class":179},[158,5069,5070,5072,5074,5076,5078,5080,5082,5084,5086,5088,5090,5092],{"class":160,"line":217},[158,5071,172],{"class":171},[158,5073,913],{"class":171},[158,5075,782],{"class":171},[158,5077,918],{"class":179},[158,5079,452],{"class":171},[158,5081,775],{"class":179},[158,5083,524],{"class":171},[158,5085,512],{"class":175},[158,5087,462],{"class":171},[158,5089,1392],{"class":175},[158,5091,1734],{"class":179},[158,5093,5094],{"class":164},"-- now uses the expression index\n",[14,5096,5097,5100,5101,1405,5103,1405,5105,1405,5107,4091,5109,5111,5112,5114,5115,5117,5118,5120],{},[136,5098,5099],{},"The lesson",": a GIN index on a JSONB column serves ",[18,5102,669],{},[18,5104,704],{},[18,5106,4589],{},[18,5108,735],{},[136,5110,4993],{}," ",[18,5113,524],{}," extraction + comparison. For ",[18,5116,524],{}," queries, either rewrite as containment (",[18,5119,669],{},") or add an expression index on the specific path.",[23,5122],{},[26,5124,5125],{"id":4970},"Summary",[14,5127,5128,5129,5131,5132,1405,5134,1405,5136,1405,5138,5140,5141,1405,5143,1405,5145,5147,5148,1405,5151,1405,5153,5155],{},"You can now store and query semi-structured data with ",[18,5130,20],{}," (using ",[18,5133,595],{},[18,5135,524],{},[18,5137,669],{},[18,5139,704],{},"), index it with GIN or expression indexes, modify it with ",[18,5142,2151],{},[18,5144,2063],{},[18,5146,2105],{},", use array columns with ",[18,5149,5150],{},"ANY",[18,5152,669],{},[18,5154,2924],{},", and query nested data with SQL\u002FJSON path — knowing when JSONB\u002Farrays are appropriate vs. when to normalize. Next: recursive CTEs for tree and graph traversal.",[5157,5158,5159],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}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 .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}",{"title":154,"searchDepth":168,"depth":168,"links":5161},[5162,5163,5164,5168,5174,5175,5176,5180,5181,5182,5183,5184,5185,5186],{"id":28,"depth":168,"text":29},{"id":248,"depth":168,"text":249},{"id":567,"depth":168,"text":568,"children":5165},[5166],{"id":1083,"depth":191,"text":5167},"-> vs ->>: The Return Type Trap",{"id":1247,"depth":168,"text":1248,"children":5169},[5170,5171,5172,5173],{"id":1251,"depth":191,"text":1252},{"id":1411,"depth":191,"text":1412},{"id":1560,"depth":191,"text":1561},{"id":1646,"depth":191,"text":1647},{"id":1751,"depth":168,"text":1752},{"id":1913,"depth":168,"text":1914},{"id":2157,"depth":168,"text":2158,"children":5177},[5178,5179],{"id":2326,"depth":191,"text":2327},{"id":2712,"depth":191,"text":2713},{"id":2930,"depth":168,"text":2931},{"id":4099,"depth":168,"text":4100},{"id":4408,"depth":168,"text":4409},{"id":4544,"depth":168,"text":4545},{"id":4674,"depth":168,"text":4675},{"id":4901,"depth":168,"text":4902},{"id":4970,"depth":168,"text":5125},"PostgreSQL has first-class support for semi-structured data: JSONB (binary JSON) and array columns. This lets you store variable-structure data (event payloads, user preferences, nested objects) without a separate document database, while keeping relational power for the rest of the schema.","md",{},"\u002Fsql\u002F18-json-and-arrays",{"title":5,"description":5187},"sql\u002F18-json-and-arrays","GE7Dil7T2RAEHU19lmMO5QAQk-dtcMr48Af9XjOlVs0",1789924654749]