[{"data":1,"prerenderedAt":4955},["ShallowReactive",2],{"page-\u002Fsql\u002F27-advanced-patterns":3},{"id":4,"title":5,"body":6,"description":32,"extension":4949,"meta":4950,"navigation":555,"path":4951,"seo":4952,"stem":4953,"__hash__":4954},"content\u002Fsql\u002F27-advanced-patterns.md","27 — Advanced SQL Patterns",{"type":7,"value":8,"toc":4916},"minimark",[9,13,18,23,312,316,496,504,668,672,676,974,978,1178,1182,1186,1411,1415,1609,1613,1617,1892,1896,2053,2057,2061,2397,2401,2548,2552,2705,2709,3120,3124,3376,3380,3546,3550,3725,3729,4188,4192,4341,4345,4504,4508,4511,4629,4912],[10,11,5],"h1",{"id":12},"_27-advanced-sql-patterns",[14,15,17],"h2",{"id":16},"pivot-cross-tabulation","Pivot \u002F Cross-Tabulation",[19,20,22],"h3",{"id":21},"conditional-aggregation-with-filter-sql-standard-portable","Conditional Aggregation with FILTER (SQL Standard, Portable)",[24,25,27],"code-wrapper",{"language":26},"sql",[28,29,33],"pre",{"className":30,"code":31,"language":26,"meta":32,"style":32},"language-sql shiki shiki-themes github-light github-dark","-- Pivot: orders per customer per month, months as columns\n-- FILTER clause: evaluates the aggregate ONLY for rows matching the condition\n-- Rows that don't match are skipped entirely (not counted as 0 — they're absent)\n-- This is different from CASE: SUM(CASE WHEN ... THEN x ELSE 0 END) counts\n-- non-matching rows as 0 (affecting AVG), while FILTER excludes them\nSELECT\n  customer_id,\n  SUM(amount) FILTER (WHERE EXTRACT(MONTH FROM ordered_on) = 1) AS jan,\n  SUM(amount) FILTER (WHERE EXTRACT(MONTH FROM ordered_on) = 2) AS feb,\n  SUM(amount) FILTER (WHERE EXTRACT(MONTH FROM ordered_on) = 3) AS mar,\n  SUM(amount) FILTER (WHERE EXTRACT(MONTH FROM ordered_on) = 4) AS apr,\n  SUM(amount)                                          AS total\nFROM orders\nWHERE ordered_on >= '2024-01-01' AND ordered_on \u003C '2024-05-01'\nGROUP BY customer_id\nORDER BY customer_id;\n-- A customer with orders only in Feb: jan=NULL, feb=500, mar=NULL, apr=NULL\n-- FILTER returns NULL for months with no matching rows (SUM of empty set = NULL)\n-- Use COALESCE(SUM(...) FILTER (...), 0) if you want 0 instead of NULL\n","",[34,35,36,45,51,57,63,69,76,83,129,162,195,228,241,250,276,285,294,300,306],"code",{"__ignoreMap":32},[37,38,41],"span",{"class":39,"line":40},"line",1,[37,42,44],{"class":43},"sdCPZ","-- Pivot: orders per customer per month, months as columns\n",[37,46,48],{"class":39,"line":47},2,[37,49,50],{"class":43},"-- FILTER clause: evaluates the aggregate ONLY for rows matching the condition\n",[37,52,54],{"class":39,"line":53},3,[37,55,56],{"class":43},"-- Rows that don't match are skipped entirely (not counted as 0 — they're absent)\n",[37,58,60],{"class":39,"line":59},4,[37,61,62],{"class":43},"-- This is different from CASE: SUM(CASE WHEN ... THEN x ELSE 0 END) counts\n",[37,64,66],{"class":39,"line":65},5,[37,67,68],{"class":43},"-- non-matching rows as 0 (affecting AVG), while FILTER excludes them\n",[37,70,72],{"class":39,"line":71},6,[37,73,75],{"class":74},"svdQ7","SELECT\n",[37,77,79],{"class":39,"line":78},7,[37,80,82],{"class":81},"ssxIu","  customer_id,\n",[37,84,86,90,93,96,99,102,105,108,111,114,117,120,123,126],{"class":39,"line":85},8,[37,87,89],{"class":88},"snvgF","  SUM",[37,91,92],{"class":81},"(amount) ",[37,94,95],{"class":74},"FILTER",[37,97,98],{"class":81}," (",[37,100,101],{"class":74},"WHERE",[37,103,104],{"class":81}," EXTRACT(",[37,106,107],{"class":74},"MONTH",[37,109,110],{"class":74}," FROM",[37,112,113],{"class":81}," ordered_on) ",[37,115,116],{"class":74},"=",[37,118,119],{"class":88}," 1",[37,121,122],{"class":81},") ",[37,124,125],{"class":74},"AS",[37,127,128],{"class":81}," jan,\n",[37,130,132,134,136,138,140,142,144,146,148,150,152,155,157,159],{"class":39,"line":131},9,[37,133,89],{"class":88},[37,135,92],{"class":81},[37,137,95],{"class":74},[37,139,98],{"class":81},[37,141,101],{"class":74},[37,143,104],{"class":81},[37,145,107],{"class":74},[37,147,110],{"class":74},[37,149,113],{"class":81},[37,151,116],{"class":74},[37,153,154],{"class":88}," 2",[37,156,122],{"class":81},[37,158,125],{"class":74},[37,160,161],{"class":81}," feb,\n",[37,163,165,167,169,171,173,175,177,179,181,183,185,188,190,192],{"class":39,"line":164},10,[37,166,89],{"class":88},[37,168,92],{"class":81},[37,170,95],{"class":74},[37,172,98],{"class":81},[37,174,101],{"class":74},[37,176,104],{"class":81},[37,178,107],{"class":74},[37,180,110],{"class":74},[37,182,113],{"class":81},[37,184,116],{"class":74},[37,186,187],{"class":88}," 3",[37,189,122],{"class":81},[37,191,125],{"class":74},[37,193,194],{"class":81}," mar,\n",[37,196,198,200,202,204,206,208,210,212,214,216,218,221,223,225],{"class":39,"line":197},11,[37,199,89],{"class":88},[37,201,92],{"class":81},[37,203,95],{"class":74},[37,205,98],{"class":81},[37,207,101],{"class":74},[37,209,104],{"class":81},[37,211,107],{"class":74},[37,213,110],{"class":74},[37,215,113],{"class":81},[37,217,116],{"class":74},[37,219,220],{"class":88}," 4",[37,222,122],{"class":81},[37,224,125],{"class":74},[37,226,227],{"class":81}," apr,\n",[37,229,231,233,236,238],{"class":39,"line":230},12,[37,232,89],{"class":88},[37,234,235],{"class":81},"(amount)                                          ",[37,237,125],{"class":74},[37,239,240],{"class":81}," total\n",[37,242,244,247],{"class":39,"line":243},13,[37,245,246],{"class":74},"FROM",[37,248,249],{"class":81}," orders\n",[37,251,253,255,258,261,265,268,270,273],{"class":39,"line":252},14,[37,254,101],{"class":74},[37,256,257],{"class":81}," ordered_on ",[37,259,260],{"class":74},">=",[37,262,264],{"class":263},"sJ6F3"," '2024-01-01'",[37,266,267],{"class":74}," AND",[37,269,257],{"class":81},[37,271,272],{"class":74},"\u003C",[37,274,275],{"class":263}," '2024-05-01'\n",[37,277,279,282],{"class":39,"line":278},15,[37,280,281],{"class":74},"GROUP BY",[37,283,284],{"class":81}," customer_id\n",[37,286,288,291],{"class":39,"line":287},16,[37,289,290],{"class":74},"ORDER BY",[37,292,293],{"class":81}," customer_id;\n",[37,295,297],{"class":39,"line":296},17,[37,298,299],{"class":43},"-- A customer with orders only in Feb: jan=NULL, feb=500, mar=NULL, apr=NULL\n",[37,301,303],{"class":39,"line":302},18,[37,304,305],{"class":43},"-- FILTER returns NULL for months with no matching rows (SUM of empty set = NULL)\n",[37,307,309],{"class":39,"line":308},19,[37,310,311],{"class":43},"-- Use COALESCE(SUM(...) FILTER (...), 0) if you want 0 instead of NULL\n",[19,313,315],{"id":314},"portable-case-form-universal-slightly-slower","Portable CASE Form (Universal, Slightly Slower)",[24,317,318],{"language":26},[28,319,321],{"className":30,"code":320,"language":26,"meta":32,"style":32},"-- CASE form: works on every SQL engine, but the ELSE 0 is included in\n-- the SUM. For SUM this is equivalent to FILTER (0 doesn't change the sum).\n-- For AVG\u002FCOUNT it differs: AVG(CASE WHEN ... THEN x ELSE 0 END) counts\n-- the 0s in the denominator, skewing the average down.\nSELECT\n  customer_id,\n  SUM(CASE WHEN EXTRACT(MONTH FROM ordered_on) = 1 THEN amount ELSE 0 END) AS jan,\n  SUM(CASE WHEN EXTRACT(MONTH FROM ordered_on) = 2 THEN amount ELSE 0 END) AS feb,\n  SUM(CASE WHEN EXTRACT(MONTH FROM ordered_on) = 3 THEN amount ELSE 0 END) AS mar\nFROM orders\nGROUP BY customer_id;\n-- For COUNT: COUNT(CASE WHEN ... THEN 1 END) — no ELSE, so non-matching\n-- rows produce NULL, which COUNT skips. This is the portable FILTER equivalent.\n",[34,322,323,328,333,338,343,347,351,397,435,474,480,486,491],{"__ignoreMap":32},[37,324,325],{"class":39,"line":40},[37,326,327],{"class":43},"-- CASE form: works on every SQL engine, but the ELSE 0 is included in\n",[37,329,330],{"class":39,"line":47},[37,331,332],{"class":43},"-- the SUM. For SUM this is equivalent to FILTER (0 doesn't change the sum).\n",[37,334,335],{"class":39,"line":53},[37,336,337],{"class":43},"-- For AVG\u002FCOUNT it differs: AVG(CASE WHEN ... THEN x ELSE 0 END) counts\n",[37,339,340],{"class":39,"line":59},[37,341,342],{"class":43},"-- the 0s in the denominator, skewing the average down.\n",[37,344,345],{"class":39,"line":65},[37,346,75],{"class":74},[37,348,349],{"class":39,"line":71},[37,350,82],{"class":81},[37,352,353,355,358,361,364,366,368,370,372,374,376,379,382,385,388,391,393,395],{"class":39,"line":78},[37,354,89],{"class":88},[37,356,357],{"class":81},"(",[37,359,360],{"class":74},"CASE",[37,362,363],{"class":74}," WHEN",[37,365,104],{"class":81},[37,367,107],{"class":74},[37,369,110],{"class":74},[37,371,113],{"class":81},[37,373,116],{"class":74},[37,375,119],{"class":88},[37,377,378],{"class":74}," THEN",[37,380,381],{"class":81}," amount ",[37,383,384],{"class":74},"ELSE",[37,386,387],{"class":88}," 0",[37,389,390],{"class":74}," END",[37,392,122],{"class":81},[37,394,125],{"class":74},[37,396,128],{"class":81},[37,398,399,401,403,405,407,409,411,413,415,417,419,421,423,425,427,429,431,433],{"class":39,"line":85},[37,400,89],{"class":88},[37,402,357],{"class":81},[37,404,360],{"class":74},[37,406,363],{"class":74},[37,408,104],{"class":81},[37,410,107],{"class":74},[37,412,110],{"class":74},[37,414,113],{"class":81},[37,416,116],{"class":74},[37,418,154],{"class":88},[37,420,378],{"class":74},[37,422,381],{"class":81},[37,424,384],{"class":74},[37,426,387],{"class":88},[37,428,390],{"class":74},[37,430,122],{"class":81},[37,432,125],{"class":74},[37,434,161],{"class":81},[37,436,437,439,441,443,445,447,449,451,453,455,457,459,461,463,465,467,469,471],{"class":39,"line":131},[37,438,89],{"class":88},[37,440,357],{"class":81},[37,442,360],{"class":74},[37,444,363],{"class":74},[37,446,104],{"class":81},[37,448,107],{"class":74},[37,450,110],{"class":74},[37,452,113],{"class":81},[37,454,116],{"class":74},[37,456,187],{"class":88},[37,458,378],{"class":74},[37,460,381],{"class":81},[37,462,384],{"class":74},[37,464,387],{"class":88},[37,466,390],{"class":74},[37,468,122],{"class":81},[37,470,125],{"class":74},[37,472,473],{"class":81}," mar\n",[37,475,476,478],{"class":39,"line":164},[37,477,246],{"class":74},[37,479,249],{"class":81},[37,481,482,484],{"class":39,"line":197},[37,483,281],{"class":74},[37,485,293],{"class":81},[37,487,488],{"class":39,"line":230},[37,489,490],{"class":43},"-- For COUNT: COUNT(CASE WHEN ... THEN 1 END) — no ELSE, so non-matching\n",[37,492,493],{"class":39,"line":243},[37,494,495],{"class":43},"-- rows produce NULL, which COUNT skips. This is the portable FILTER equivalent.\n",[19,497,499,500,503],{"id":498},"crosstab-function-postgresql-tablefunc","crosstab Function (PostgreSQL ",[34,501,502],{},"tablefunc",")",[24,505,506],{"language":26},[28,507,509],{"className":30,"code":508,"language":26,"meta":32,"style":32},"-- crosstab is faster for many fixed categories but requires:\n-- 1. The source query ordered by row_id, then category\n-- 2. A static output schema declaration (columns must be known at parse time)\n-- 3. Missing category values produce NULL (not 0) for that row\nCREATE EXTENSION IF NOT EXISTS tablefunc;\n\nSELECT * FROM crosstab(\n  'SELECT customer_id,\n          EXTRACT(MONTH FROM ordered_on)::int AS month,\n          SUM(amount)::numeric AS total\n   FROM orders\n   WHERE ordered_on >= ''2024-01-01'' AND ordered_on \u003C ''2024-04-01''\n   GROUP BY customer_id, month\n   ORDER BY customer_id, month'\n  -- The output schema MUST be declared — SQL can't infer dynamic columns\n) AS ct (customer_id BIGINT, jan NUMERIC, feb NUMERIC, mar NUMERIC);\n-- Note: doubled single quotes inside the string literal (''2024-01-01'')\n-- crosstab has no way to handle missing months in the middle of a row —\n-- if a customer has Jan and Mar but not Feb, the values SHIFT: jan gets\n-- Jan's total, feb gets Mar's total. Use the 2-parameter crosstab form\n-- with a category source query to avoid this misalignment.\n",[34,510,511,516,521,526,531,551,557,570,575,580,585,590,595,600,605,610,641,646,651,656,662],{"__ignoreMap":32},[37,512,513],{"class":39,"line":40},[37,514,515],{"class":43},"-- crosstab is faster for many fixed categories but requires:\n",[37,517,518],{"class":39,"line":47},[37,519,520],{"class":43},"-- 1. The source query ordered by row_id, then category\n",[37,522,523],{"class":39,"line":53},[37,524,525],{"class":43},"-- 2. A static output schema declaration (columns must be known at parse time)\n",[37,527,528],{"class":39,"line":59},[37,529,530],{"class":43},"-- 3. Missing category values produce NULL (not 0) for that row\n",[37,532,533,536,539,542,545,548],{"class":39,"line":65},[37,534,535],{"class":74},"CREATE",[37,537,538],{"class":81}," EXTENSION ",[37,540,541],{"class":74},"IF",[37,543,544],{"class":74}," NOT",[37,546,547],{"class":74}," EXISTS",[37,549,550],{"class":81}," tablefunc;\n",[37,552,553],{"class":39,"line":71},[37,554,556],{"emptyLinePlaceholder":555},true,"\n",[37,558,559,562,565,567],{"class":39,"line":78},[37,560,561],{"class":74},"SELECT",[37,563,564],{"class":74}," *",[37,566,110],{"class":74},[37,568,569],{"class":81}," crosstab(\n",[37,571,572],{"class":39,"line":85},[37,573,574],{"class":263},"  'SELECT customer_id,\n",[37,576,577],{"class":39,"line":131},[37,578,579],{"class":263},"          EXTRACT(MONTH FROM ordered_on)::int AS month,\n",[37,581,582],{"class":39,"line":164},[37,583,584],{"class":263},"          SUM(amount)::numeric AS total\n",[37,586,587],{"class":39,"line":197},[37,588,589],{"class":263},"   FROM orders\n",[37,591,592],{"class":39,"line":230},[37,593,594],{"class":263},"   WHERE ordered_on >= ''2024-01-01'' AND ordered_on \u003C ''2024-04-01''\n",[37,596,597],{"class":39,"line":243},[37,598,599],{"class":263},"   GROUP BY customer_id, month\n",[37,601,602],{"class":39,"line":252},[37,603,604],{"class":263},"   ORDER BY customer_id, month'\n",[37,606,607],{"class":39,"line":278},[37,608,609],{"class":43},"  -- The output schema MUST be declared — SQL can't infer dynamic columns\n",[37,611,612,614,616,619,622,625,628,631,633,636,638],{"class":39,"line":287},[37,613,122],{"class":81},[37,615,125],{"class":74},[37,617,618],{"class":81}," ct (customer_id ",[37,620,621],{"class":74},"BIGINT",[37,623,624],{"class":81},", jan ",[37,626,627],{"class":74},"NUMERIC",[37,629,630],{"class":81},", feb ",[37,632,627],{"class":74},[37,634,635],{"class":81},", mar ",[37,637,627],{"class":74},[37,639,640],{"class":81},");\n",[37,642,643],{"class":39,"line":296},[37,644,645],{"class":43},"-- Note: doubled single quotes inside the string literal (''2024-01-01'')\n",[37,647,648],{"class":39,"line":302},[37,649,650],{"class":43},"-- crosstab has no way to handle missing months in the middle of a row —\n",[37,652,653],{"class":39,"line":308},[37,654,655],{"class":43},"-- if a customer has Jan and Mar but not Feb, the values SHIFT: jan gets\n",[37,657,659],{"class":39,"line":658},20,[37,660,661],{"class":43},"-- Jan's total, feb gets Mar's total. Use the 2-parameter crosstab form\n",[37,663,665],{"class":39,"line":664},21,[37,666,667],{"class":43},"-- with a category source query to avoid this misalignment.\n",[14,669,671],{"id":670},"gaps-and-islands","Gaps and Islands",[19,673,675],{"id":674},"the-row-number-difference-technique","The Row-Number Difference Technique",[24,677,678],{"language":26},[28,679,681],{"className":30,"code":680,"language":26,"meta":32,"style":32},"-- Problem: find consecutive-day streaks of user activity (\"sessions\")\n-- A run of consecutive days forms an \"island\"; gaps separate islands\n-- The trick: day - ROW_NUMBER() is constant within a run of consecutive days\n--   Day 1: day=Jan 1, rn=1, diff=Dec 31  → group key\n--   Day 2: day=Jan 2, rn=2, diff=Dec 31  → same group key\n--   Day 3: day=Jan 5, rn=3, diff=Jan 2   → DIFFERENT key (gap of 2 days)\n-- When there's a gap, day jumps but rn increments by 1, so diff changes\nWITH day_marks AS (\n  -- DISTINCT first: duplicate activity on the same day would break the\n  -- rn-diff trick (same day, different rn → different diff → splits the run)\n  SELECT DISTINCT user_id, date_trunc('day', activity_at)::date AS day\n  FROM events\n),\nnumbered AS (\n  SELECT user_id, day,\n    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY day) AS rn,\n    -- day - rn: subtracting an integer from a date gives a date in PG\n    -- The exact value doesn't matter — only that it's CONSTANT within a run\n    day - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY day) AS grp\n  FROM day_marks\n)\nSELECT user_id, grp,\n  MIN(day) AS session_start,\n  MAX(day) AS session_end,\n  COUNT(*) AS session_days\nFROM numbered\nGROUP BY user_id, grp\nORDER BY user_id, session_start;\n",[34,682,683,688,693,698,703,708,713,718,731,736,741,764,772,777,786,800,834,839,844,878,885,890,898,915,932,950,958,966],{"__ignoreMap":32},[37,684,685],{"class":39,"line":40},[37,686,687],{"class":43},"-- Problem: find consecutive-day streaks of user activity (\"sessions\")\n",[37,689,690],{"class":39,"line":47},[37,691,692],{"class":43},"-- A run of consecutive days forms an \"island\"; gaps separate islands\n",[37,694,695],{"class":39,"line":53},[37,696,697],{"class":43},"-- The trick: day - ROW_NUMBER() is constant within a run of consecutive days\n",[37,699,700],{"class":39,"line":59},[37,701,702],{"class":43},"--   Day 1: day=Jan 1, rn=1, diff=Dec 31  → group key\n",[37,704,705],{"class":39,"line":65},[37,706,707],{"class":43},"--   Day 2: day=Jan 2, rn=2, diff=Dec 31  → same group key\n",[37,709,710],{"class":39,"line":71},[37,711,712],{"class":43},"--   Day 3: day=Jan 5, rn=3, diff=Jan 2   → DIFFERENT key (gap of 2 days)\n",[37,714,715],{"class":39,"line":78},[37,716,717],{"class":43},"-- When there's a gap, day jumps but rn increments by 1, so diff changes\n",[37,719,720,723,726,728],{"class":39,"line":85},[37,721,722],{"class":74},"WITH",[37,724,725],{"class":81}," day_marks ",[37,727,125],{"class":74},[37,729,730],{"class":81}," (\n",[37,732,733],{"class":39,"line":131},[37,734,735],{"class":43},"  -- DISTINCT first: duplicate activity on the same day would break the\n",[37,737,738],{"class":39,"line":164},[37,739,740],{"class":43},"  -- rn-diff trick (same day, different rn → different diff → splits the run)\n",[37,742,743,746,749,752,755,758,761],{"class":39,"line":197},[37,744,745],{"class":74},"  SELECT DISTINCT",[37,747,748],{"class":81}," user_id, date_trunc(",[37,750,751],{"class":263},"'day'",[37,753,754],{"class":81},", activity_at)::",[37,756,757],{"class":74},"date",[37,759,760],{"class":74}," AS",[37,762,763],{"class":74}," day\n",[37,765,766,769],{"class":39,"line":230},[37,767,768],{"class":74},"  FROM",[37,770,771],{"class":81}," events\n",[37,773,774],{"class":39,"line":243},[37,775,776],{"class":81},"),\n",[37,778,779,782,784],{"class":39,"line":252},[37,780,781],{"class":81},"numbered ",[37,783,125],{"class":74},[37,785,730],{"class":81},[37,787,788,791,794,797],{"class":39,"line":278},[37,789,790],{"class":74},"  SELECT",[37,792,793],{"class":81}," user_id, ",[37,795,796],{"class":74},"day",[37,798,799],{"class":81},",\n",[37,801,802,805,808,811,813,816,819,822,824,827,829,831],{"class":39,"line":287},[37,803,804],{"class":88},"    ROW_NUMBER",[37,806,807],{"class":81},"() ",[37,809,810],{"class":74},"OVER",[37,812,98],{"class":81},[37,814,815],{"class":74},"PARTITION",[37,817,818],{"class":74}," BY",[37,820,821],{"class":81}," user_id ",[37,823,290],{"class":74},[37,825,826],{"class":74}," day",[37,828,122],{"class":81},[37,830,125],{"class":74},[37,832,833],{"class":81}," rn,\n",[37,835,836],{"class":39,"line":296},[37,837,838],{"class":43},"    -- day - rn: subtracting an integer from a date gives a date in PG\n",[37,840,841],{"class":39,"line":302},[37,842,843],{"class":43},"    -- The exact value doesn't matter — only that it's CONSTANT within a run\n",[37,845,846,849,852,855,857,859,861,863,865,867,869,871,873,875],{"class":39,"line":308},[37,847,848],{"class":74},"    day",[37,850,851],{"class":74}," -",[37,853,854],{"class":88}," ROW_NUMBER",[37,856,807],{"class":81},[37,858,810],{"class":74},[37,860,98],{"class":81},[37,862,815],{"class":74},[37,864,818],{"class":74},[37,866,821],{"class":81},[37,868,290],{"class":74},[37,870,826],{"class":74},[37,872,122],{"class":81},[37,874,125],{"class":74},[37,876,877],{"class":81}," grp\n",[37,879,880,882],{"class":39,"line":658},[37,881,768],{"class":74},[37,883,884],{"class":81}," day_marks\n",[37,886,887],{"class":39,"line":664},[37,888,889],{"class":81},")\n",[37,891,893,895],{"class":39,"line":892},22,[37,894,561],{"class":74},[37,896,897],{"class":81}," user_id, grp,\n",[37,899,901,904,906,908,910,912],{"class":39,"line":900},23,[37,902,903],{"class":88},"  MIN",[37,905,357],{"class":81},[37,907,796],{"class":74},[37,909,122],{"class":81},[37,911,125],{"class":74},[37,913,914],{"class":81}," session_start,\n",[37,916,918,921,923,925,927,929],{"class":39,"line":917},24,[37,919,920],{"class":88},"  MAX",[37,922,357],{"class":81},[37,924,796],{"class":74},[37,926,122],{"class":81},[37,928,125],{"class":74},[37,930,931],{"class":81}," session_end,\n",[37,933,935,938,940,943,945,947],{"class":39,"line":934},25,[37,936,937],{"class":88},"  COUNT",[37,939,357],{"class":81},[37,941,942],{"class":74},"*",[37,944,122],{"class":81},[37,946,125],{"class":74},[37,948,949],{"class":81}," session_days\n",[37,951,953,955],{"class":39,"line":952},26,[37,954,246],{"class":74},[37,956,957],{"class":81}," numbered\n",[37,959,961,963],{"class":39,"line":960},27,[37,962,281],{"class":74},[37,964,965],{"class":81}," user_id, grp\n",[37,967,969,971],{"class":39,"line":968},28,[37,970,290],{"class":74},[37,972,973],{"class":81}," user_id, session_start;\n",[19,975,977],{"id":976},"integer-sequence-example","Integer Sequence Example",[24,979,980],{"language":26},[28,981,983],{"className":30,"code":982,"language":26,"meta":32,"style":32},"-- Find runs of consecutive integers in a set\nWITH nums AS (\n  SELECT n,\n    n - ROW_NUMBER() OVER (ORDER BY n) AS grp  -- constant within a run\n  FROM (VALUES (1),(2),(3),(7),(8),(10),(11),(12)) v(n)\n)\nSELECT MIN(n) AS start_val, MAX(n) AS end_val, COUNT(*) AS length\nFROM nums\nGROUP BY grp\nORDER BY start_val;\n--  start_val | end_val | length\n-- -----------+---------+--------\n--          1 |       3 |      3\n--          7 |       8 |      2\n--         10 |      12 |      3\n",[34,984,985,990,1001,1008,1037,1090,1094,1133,1140,1146,1153,1158,1163,1168,1173],{"__ignoreMap":32},[37,986,987],{"class":39,"line":40},[37,988,989],{"class":43},"-- Find runs of consecutive integers in a set\n",[37,991,992,994,997,999],{"class":39,"line":47},[37,993,722],{"class":74},[37,995,996],{"class":81}," nums ",[37,998,125],{"class":74},[37,1000,730],{"class":81},[37,1002,1003,1005],{"class":39,"line":53},[37,1004,790],{"class":74},[37,1006,1007],{"class":81}," n,\n",[37,1009,1010,1013,1016,1018,1020,1022,1024,1026,1029,1031,1034],{"class":39,"line":59},[37,1011,1012],{"class":81},"    n ",[37,1014,1015],{"class":74},"-",[37,1017,854],{"class":88},[37,1019,807],{"class":81},[37,1021,810],{"class":74},[37,1023,98],{"class":81},[37,1025,290],{"class":74},[37,1027,1028],{"class":81}," n) ",[37,1030,125],{"class":74},[37,1032,1033],{"class":81}," grp  ",[37,1035,1036],{"class":43},"-- constant within a run\n",[37,1038,1039,1041,1043,1046,1048,1051,1054,1057,1059,1062,1064,1067,1069,1072,1074,1077,1079,1082,1084,1087],{"class":39,"line":65},[37,1040,768],{"class":74},[37,1042,98],{"class":81},[37,1044,1045],{"class":74},"VALUES",[37,1047,98],{"class":81},[37,1049,1050],{"class":88},"1",[37,1052,1053],{"class":81},"),(",[37,1055,1056],{"class":88},"2",[37,1058,1053],{"class":81},[37,1060,1061],{"class":88},"3",[37,1063,1053],{"class":81},[37,1065,1066],{"class":88},"7",[37,1068,1053],{"class":81},[37,1070,1071],{"class":88},"8",[37,1073,1053],{"class":81},[37,1075,1076],{"class":88},"10",[37,1078,1053],{"class":81},[37,1080,1081],{"class":88},"11",[37,1083,1053],{"class":81},[37,1085,1086],{"class":88},"12",[37,1088,1089],{"class":81},")) v(n)\n",[37,1091,1092],{"class":39,"line":71},[37,1093,889],{"class":81},[37,1095,1096,1098,1101,1104,1106,1109,1112,1114,1116,1119,1122,1124,1126,1128,1130],{"class":39,"line":78},[37,1097,561],{"class":74},[37,1099,1100],{"class":88}," MIN",[37,1102,1103],{"class":81},"(n) ",[37,1105,125],{"class":74},[37,1107,1108],{"class":81}," start_val, ",[37,1110,1111],{"class":88},"MAX",[37,1113,1103],{"class":81},[37,1115,125],{"class":74},[37,1117,1118],{"class":81}," end_val, ",[37,1120,1121],{"class":88},"COUNT",[37,1123,357],{"class":81},[37,1125,942],{"class":74},[37,1127,122],{"class":81},[37,1129,125],{"class":74},[37,1131,1132],{"class":74}," length\n",[37,1134,1135,1137],{"class":39,"line":85},[37,1136,246],{"class":74},[37,1138,1139],{"class":81}," nums\n",[37,1141,1142,1144],{"class":39,"line":131},[37,1143,281],{"class":74},[37,1145,877],{"class":81},[37,1147,1148,1150],{"class":39,"line":164},[37,1149,290],{"class":74},[37,1151,1152],{"class":81}," start_val;\n",[37,1154,1155],{"class":39,"line":197},[37,1156,1157],{"class":43},"--  start_val | end_val | length\n",[37,1159,1160],{"class":39,"line":230},[37,1161,1162],{"class":43},"-- -----------+---------+--------\n",[37,1164,1165],{"class":39,"line":243},[37,1166,1167],{"class":43},"--          1 |       3 |      3\n",[37,1169,1170],{"class":39,"line":252},[37,1171,1172],{"class":43},"--          7 |       8 |      2\n",[37,1174,1175],{"class":39,"line":278},[37,1176,1177],{"class":43},"--         10 |      12 |      3\n",[14,1179,1181],{"id":1180},"histograms","Histograms",[19,1183,1185],{"id":1184},"width_bucket-fixed-width-buckets","width_bucket (Fixed-Width Buckets)",[24,1187,1188],{"language":26},[28,1189,1191],{"className":30,"code":1190,"language":26,"meta":32,"style":32},"-- Histogram of order amounts in 10 equal-width buckets from $0 to $500\n-- width_bucket(value, min, max, num_buckets) returns:\n--   0           if value \u003C min\n--   1..n        if value is in [min, max)\n--   n           if value == max (max goes in the LAST bucket, not overflow)\n--   n+1         if value > max\n-- Bucket boundaries: [0,50), [50,100), ..., [450,500]\n-- The range is half-open: [min, max) except the last bucket which is [max-n*step, max]\nSELECT\n  width_bucket(amount, 0, 500, 10) AS bucket,\n  -- Reconstruct the bucket boundaries for display:\n  (width_bucket(amount, 0, 500, 10) - 1) * 50 AS bucket_min,\n  width_bucket(amount, 0, 500, 10) * 50 AS bucket_max,\n  COUNT(*) AS count,\n  ROUND(AVG(amount), 2) AS avg_in_bucket\nFROM orders\nWHERE amount > 0  -- exclude negative\u002Fzero; they'd go to bucket 0\nGROUP BY bucket\nORDER BY bucket;\n-- ⚠️ Off-by-one: a value exactly at a boundary (e.g., 50.00) goes into\n-- the HIGHER bucket (bucket 2), not bucket 1. width_bucket uses [low, high)\n-- half-open intervals. Only the last bucket [450, 500] is closed on both ends.\n",[34,1192,1193,1198,1203,1208,1213,1218,1223,1228,1233,1237,1262,1267,1300,1325,1340,1362,1368,1382,1389,1396,1401,1406],{"__ignoreMap":32},[37,1194,1195],{"class":39,"line":40},[37,1196,1197],{"class":43},"-- Histogram of order amounts in 10 equal-width buckets from $0 to $500\n",[37,1199,1200],{"class":39,"line":47},[37,1201,1202],{"class":43},"-- width_bucket(value, min, max, num_buckets) returns:\n",[37,1204,1205],{"class":39,"line":53},[37,1206,1207],{"class":43},"--   0           if value \u003C min\n",[37,1209,1210],{"class":39,"line":59},[37,1211,1212],{"class":43},"--   1..n        if value is in [min, max)\n",[37,1214,1215],{"class":39,"line":65},[37,1216,1217],{"class":43},"--   n           if value == max (max goes in the LAST bucket, not overflow)\n",[37,1219,1220],{"class":39,"line":71},[37,1221,1222],{"class":43},"--   n+1         if value > max\n",[37,1224,1225],{"class":39,"line":78},[37,1226,1227],{"class":43},"-- Bucket boundaries: [0,50), [50,100), ..., [450,500]\n",[37,1229,1230],{"class":39,"line":85},[37,1231,1232],{"class":43},"-- The range is half-open: [min, max) except the last bucket which is [max-n*step, max]\n",[37,1234,1235],{"class":39,"line":131},[37,1236,75],{"class":74},[37,1238,1239,1242,1245,1248,1251,1253,1255,1257,1259],{"class":39,"line":164},[37,1240,1241],{"class":81},"  width_bucket(amount, ",[37,1243,1244],{"class":88},"0",[37,1246,1247],{"class":81},", ",[37,1249,1250],{"class":88},"500",[37,1252,1247],{"class":81},[37,1254,1076],{"class":88},[37,1256,122],{"class":81},[37,1258,125],{"class":74},[37,1260,1261],{"class":81}," bucket,\n",[37,1263,1264],{"class":39,"line":197},[37,1265,1266],{"class":43},"  -- Reconstruct the bucket boundaries for display:\n",[37,1268,1269,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1295,1297],{"class":39,"line":230},[37,1270,1271],{"class":81},"  (width_bucket(amount, ",[37,1273,1244],{"class":88},[37,1275,1247],{"class":81},[37,1277,1250],{"class":88},[37,1279,1247],{"class":81},[37,1281,1076],{"class":88},[37,1283,122],{"class":81},[37,1285,1015],{"class":74},[37,1287,119],{"class":88},[37,1289,122],{"class":81},[37,1291,942],{"class":74},[37,1293,1294],{"class":88}," 50",[37,1296,760],{"class":74},[37,1298,1299],{"class":81}," bucket_min,\n",[37,1301,1302,1304,1306,1308,1310,1312,1314,1316,1318,1320,1322],{"class":39,"line":243},[37,1303,1241],{"class":81},[37,1305,1244],{"class":88},[37,1307,1247],{"class":81},[37,1309,1250],{"class":88},[37,1311,1247],{"class":81},[37,1313,1076],{"class":88},[37,1315,122],{"class":81},[37,1317,942],{"class":74},[37,1319,1294],{"class":88},[37,1321,760],{"class":74},[37,1323,1324],{"class":81}," bucket_max,\n",[37,1326,1327,1329,1331,1333,1335,1337],{"class":39,"line":252},[37,1328,937],{"class":88},[37,1330,357],{"class":81},[37,1332,942],{"class":74},[37,1334,122],{"class":81},[37,1336,125],{"class":74},[37,1338,1339],{"class":81}," count,\n",[37,1341,1342,1345,1347,1350,1353,1355,1357,1359],{"class":39,"line":278},[37,1343,1344],{"class":88},"  ROUND",[37,1346,357],{"class":81},[37,1348,1349],{"class":88},"AVG",[37,1351,1352],{"class":81},"(amount), ",[37,1354,1056],{"class":88},[37,1356,122],{"class":81},[37,1358,125],{"class":74},[37,1360,1361],{"class":81}," avg_in_bucket\n",[37,1363,1364,1366],{"class":39,"line":287},[37,1365,246],{"class":74},[37,1367,249],{"class":81},[37,1369,1370,1372,1374,1377,1379],{"class":39,"line":296},[37,1371,101],{"class":74},[37,1373,381],{"class":81},[37,1375,1376],{"class":74},">",[37,1378,387],{"class":88},[37,1380,1381],{"class":43},"  -- exclude negative\u002Fzero; they'd go to bucket 0\n",[37,1383,1384,1386],{"class":39,"line":302},[37,1385,281],{"class":74},[37,1387,1388],{"class":81}," bucket\n",[37,1390,1391,1393],{"class":39,"line":308},[37,1392,290],{"class":74},[37,1394,1395],{"class":81}," bucket;\n",[37,1397,1398],{"class":39,"line":658},[37,1399,1400],{"class":43},"-- ⚠️ Off-by-one: a value exactly at a boundary (e.g., 50.00) goes into\n",[37,1402,1403],{"class":39,"line":664},[37,1404,1405],{"class":43},"-- the HIGHER bucket (bucket 2), not bucket 1. width_bucket uses [low, high)\n",[37,1407,1408],{"class":39,"line":892},[37,1409,1410],{"class":43},"-- half-open intervals. Only the last bucket [450, 500] is closed on both ends.\n",[19,1412,1414],{"id":1413},"manual-buckets-with-case-variable-width","Manual Buckets with CASE (Variable-Width)",[24,1416,1417],{"language":26},[28,1418,1420],{"className":30,"code":1419,"language":26,"meta":32,"style":32},"-- Variable-width buckets — useful when data is log-distributed\n-- The CASE is evaluated top-to-bottom; the first matching WHEN wins\n-- So ranges must be in ascending order with no gaps\nSELECT\n  CASE\n    WHEN amount \u003C 10   THEN '00-10'\n    WHEN amount \u003C 50   THEN '10-50'\n    WHEN amount \u003C 100  THEN '50-100'\n    WHEN amount \u003C 500  THEN '100-500'\n    WHEN amount \u003C 1000 THEN '500-1000'\n    ELSE '1000+'\n  END AS bucket,\n  COUNT(*) AS count,\n  ROUND(AVG(amount), 2) AS avg_amount\nFROM orders\nGROUP BY bucket\n-- ORDER BY MIN(amount) sorts by the natural bucket order, not alphabetically\n-- (alphabetical would put '00-10' after '100-500' — wrong)\nORDER BY MIN(amount);\n",[34,1421,1422,1427,1432,1437,1441,1446,1464,1479,1496,1512,1528,1536,1545,1559,1578,1584,1590,1595,1600],{"__ignoreMap":32},[37,1423,1424],{"class":39,"line":40},[37,1425,1426],{"class":43},"-- Variable-width buckets — useful when data is log-distributed\n",[37,1428,1429],{"class":39,"line":47},[37,1430,1431],{"class":43},"-- The CASE is evaluated top-to-bottom; the first matching WHEN wins\n",[37,1433,1434],{"class":39,"line":53},[37,1435,1436],{"class":43},"-- So ranges must be in ascending order with no gaps\n",[37,1438,1439],{"class":39,"line":59},[37,1440,75],{"class":74},[37,1442,1443],{"class":39,"line":65},[37,1444,1445],{"class":74},"  CASE\n",[37,1447,1448,1451,1453,1455,1458,1461],{"class":39,"line":71},[37,1449,1450],{"class":74},"    WHEN",[37,1452,381],{"class":81},[37,1454,272],{"class":74},[37,1456,1457],{"class":88}," 10",[37,1459,1460],{"class":74},"   THEN",[37,1462,1463],{"class":263}," '00-10'\n",[37,1465,1466,1468,1470,1472,1474,1476],{"class":39,"line":78},[37,1467,1450],{"class":74},[37,1469,381],{"class":81},[37,1471,272],{"class":74},[37,1473,1294],{"class":88},[37,1475,1460],{"class":74},[37,1477,1478],{"class":263}," '10-50'\n",[37,1480,1481,1483,1485,1487,1490,1493],{"class":39,"line":85},[37,1482,1450],{"class":74},[37,1484,381],{"class":81},[37,1486,272],{"class":74},[37,1488,1489],{"class":88}," 100",[37,1491,1492],{"class":74},"  THEN",[37,1494,1495],{"class":263}," '50-100'\n",[37,1497,1498,1500,1502,1504,1507,1509],{"class":39,"line":131},[37,1499,1450],{"class":74},[37,1501,381],{"class":81},[37,1503,272],{"class":74},[37,1505,1506],{"class":88}," 500",[37,1508,1492],{"class":74},[37,1510,1511],{"class":263}," '100-500'\n",[37,1513,1514,1516,1518,1520,1523,1525],{"class":39,"line":164},[37,1515,1450],{"class":74},[37,1517,381],{"class":81},[37,1519,272],{"class":74},[37,1521,1522],{"class":88}," 1000",[37,1524,378],{"class":74},[37,1526,1527],{"class":263}," '500-1000'\n",[37,1529,1530,1533],{"class":39,"line":197},[37,1531,1532],{"class":74},"    ELSE",[37,1534,1535],{"class":263}," '1000+'\n",[37,1537,1538,1541,1543],{"class":39,"line":230},[37,1539,1540],{"class":74},"  END",[37,1542,760],{"class":74},[37,1544,1261],{"class":81},[37,1546,1547,1549,1551,1553,1555,1557],{"class":39,"line":243},[37,1548,937],{"class":88},[37,1550,357],{"class":81},[37,1552,942],{"class":74},[37,1554,122],{"class":81},[37,1556,125],{"class":74},[37,1558,1339],{"class":81},[37,1560,1561,1563,1565,1567,1569,1571,1573,1575],{"class":39,"line":252},[37,1562,1344],{"class":88},[37,1564,357],{"class":81},[37,1566,1349],{"class":88},[37,1568,1352],{"class":81},[37,1570,1056],{"class":88},[37,1572,122],{"class":81},[37,1574,125],{"class":74},[37,1576,1577],{"class":81}," avg_amount\n",[37,1579,1580,1582],{"class":39,"line":278},[37,1581,246],{"class":74},[37,1583,249],{"class":81},[37,1585,1586,1588],{"class":39,"line":287},[37,1587,281],{"class":74},[37,1589,1388],{"class":81},[37,1591,1592],{"class":39,"line":296},[37,1593,1594],{"class":43},"-- ORDER BY MIN(amount) sorts by the natural bucket order, not alphabetically\n",[37,1596,1597],{"class":39,"line":302},[37,1598,1599],{"class":43},"-- (alphabetical would put '00-10' after '100-500' — wrong)\n",[37,1601,1602,1604,1606],{"class":39,"line":308},[37,1603,290],{"class":74},[37,1605,1100],{"class":88},[37,1607,1608],{"class":81},"(amount);\n",[14,1610,1612],{"id":1611},"running-medians","Running Medians",[19,1614,1616],{"id":1615},"overall-and-per-group-median","Overall and Per-Group Median",[24,1618,1619],{"language":26},[28,1620,1622],{"className":30,"code":1621,"language":26,"meta":32,"style":32},"-- PERCENTILE_CONT is an ordered-set aggregate: it sorts the values\n-- internally and interpolates for even-count groups\n-- For 4 values [10, 20, 30, 40]: p=0.5 → interpolate between 20 and 30 → 25\n-- PERCENTILE_DISC returns an actual value (no interpolation): → 20\n\n-- Overall median across all orders\nSELECT\n  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median,\n  PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY amount) AS p25,\n  PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY amount) AS p75,\n  -- IQR (interquartile range) = p75 - p25 — useful for outlier detection\n  PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY amount)\n    - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY amount) AS iqr\nFROM orders;\n\n-- Median per customer\nSELECT customer_id,\n  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median\nFROM orders\nGROUP BY customer_id\nORDER BY median DESC;\n",[34,1623,1624,1629,1634,1639,1644,1648,1653,1657,1692,1722,1752,1757,1782,1815,1822,1826,1831,1838,1867,1873,1879],{"__ignoreMap":32},[37,1625,1626],{"class":39,"line":40},[37,1627,1628],{"class":43},"-- PERCENTILE_CONT is an ordered-set aggregate: it sorts the values\n",[37,1630,1631],{"class":39,"line":47},[37,1632,1633],{"class":43},"-- internally and interpolates for even-count groups\n",[37,1635,1636],{"class":39,"line":53},[37,1637,1638],{"class":43},"-- For 4 values [10, 20, 30, 40]: p=0.5 → interpolate between 20 and 30 → 25\n",[37,1640,1641],{"class":39,"line":59},[37,1642,1643],{"class":43},"-- PERCENTILE_DISC returns an actual value (no interpolation): → 20\n",[37,1645,1646],{"class":39,"line":65},[37,1647,556],{"emptyLinePlaceholder":555},[37,1649,1650],{"class":39,"line":71},[37,1651,1652],{"class":43},"-- Overall median across all orders\n",[37,1654,1655],{"class":39,"line":78},[37,1656,75],{"class":74},[37,1658,1659,1662,1664,1666,1669,1672,1674,1677,1680,1682,1684,1687,1689],{"class":39,"line":85},[37,1660,1661],{"class":88},"  PERCENTILE_CONT",[37,1663,357],{"class":81},[37,1665,1244],{"class":88},[37,1667,1668],{"class":81},".",[37,1670,1671],{"class":88},"5",[37,1673,122],{"class":81},[37,1675,1676],{"class":74},"WITHIN",[37,1678,1679],{"class":88}," GROUP",[37,1681,98],{"class":81},[37,1683,290],{"class":74},[37,1685,1686],{"class":81}," amount) ",[37,1688,125],{"class":74},[37,1690,1691],{"class":81}," median,\n",[37,1693,1694,1696,1698,1700,1702,1705,1707,1709,1711,1713,1715,1717,1719],{"class":39,"line":131},[37,1695,1661],{"class":88},[37,1697,357],{"class":81},[37,1699,1244],{"class":88},[37,1701,1668],{"class":81},[37,1703,1704],{"class":88},"25",[37,1706,122],{"class":81},[37,1708,1676],{"class":74},[37,1710,1679],{"class":88},[37,1712,98],{"class":81},[37,1714,290],{"class":74},[37,1716,1686],{"class":81},[37,1718,125],{"class":74},[37,1720,1721],{"class":81}," p25,\n",[37,1723,1724,1726,1728,1730,1732,1735,1737,1739,1741,1743,1745,1747,1749],{"class":39,"line":164},[37,1725,1661],{"class":88},[37,1727,357],{"class":81},[37,1729,1244],{"class":88},[37,1731,1668],{"class":81},[37,1733,1734],{"class":88},"75",[37,1736,122],{"class":81},[37,1738,1676],{"class":74},[37,1740,1679],{"class":88},[37,1742,98],{"class":81},[37,1744,290],{"class":74},[37,1746,1686],{"class":81},[37,1748,125],{"class":74},[37,1750,1751],{"class":81}," p75,\n",[37,1753,1754],{"class":39,"line":197},[37,1755,1756],{"class":43},"  -- IQR (interquartile range) = p75 - p25 — useful for outlier detection\n",[37,1758,1759,1761,1763,1765,1767,1769,1771,1773,1775,1777,1779],{"class":39,"line":230},[37,1760,1661],{"class":88},[37,1762,357],{"class":81},[37,1764,1244],{"class":88},[37,1766,1668],{"class":81},[37,1768,1734],{"class":88},[37,1770,122],{"class":81},[37,1772,1676],{"class":74},[37,1774,1679],{"class":88},[37,1776,98],{"class":81},[37,1778,290],{"class":74},[37,1780,1781],{"class":81}," amount)\n",[37,1783,1784,1787,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810,1812],{"class":39,"line":243},[37,1785,1786],{"class":74},"    -",[37,1788,1789],{"class":88}," PERCENTILE_CONT",[37,1791,357],{"class":81},[37,1793,1244],{"class":88},[37,1795,1668],{"class":81},[37,1797,1704],{"class":88},[37,1799,122],{"class":81},[37,1801,1676],{"class":74},[37,1803,1679],{"class":88},[37,1805,98],{"class":81},[37,1807,290],{"class":74},[37,1809,1686],{"class":81},[37,1811,125],{"class":74},[37,1813,1814],{"class":81}," iqr\n",[37,1816,1817,1819],{"class":39,"line":252},[37,1818,246],{"class":74},[37,1820,1821],{"class":81}," orders;\n",[37,1823,1824],{"class":39,"line":278},[37,1825,556],{"emptyLinePlaceholder":555},[37,1827,1828],{"class":39,"line":287},[37,1829,1830],{"class":43},"-- Median per customer\n",[37,1832,1833,1835],{"class":39,"line":296},[37,1834,561],{"class":74},[37,1836,1837],{"class":81}," customer_id,\n",[37,1839,1840,1842,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864],{"class":39,"line":302},[37,1841,1661],{"class":88},[37,1843,357],{"class":81},[37,1845,1244],{"class":88},[37,1847,1668],{"class":81},[37,1849,1671],{"class":88},[37,1851,122],{"class":81},[37,1853,1676],{"class":74},[37,1855,1679],{"class":88},[37,1857,98],{"class":81},[37,1859,290],{"class":74},[37,1861,1686],{"class":81},[37,1863,125],{"class":74},[37,1865,1866],{"class":81}," median\n",[37,1868,1869,1871],{"class":39,"line":308},[37,1870,246],{"class":74},[37,1872,249],{"class":81},[37,1874,1875,1877],{"class":39,"line":658},[37,1876,281],{"class":74},[37,1878,284],{"class":81},[37,1880,1881,1883,1886,1889],{"class":39,"line":664},[37,1882,290],{"class":74},[37,1884,1885],{"class":81}," median ",[37,1887,1888],{"class":74},"DESC",[37,1890,1891],{"class":81},";\n",[19,1893,1895],{"id":1894},"running-median-per-row","Running Median (Per Row)",[24,1897,1898],{"language":26},[28,1899,1901],{"className":30,"code":1900,"language":26,"meta":32,"style":32},"-- There's no built-in \"windowed ordered-set aggregate\" in PostgreSQL\n-- PERCENTILE_CONT can't be used as a window function directly\n-- Workaround: self-join — for each row, compute the median of all rows\n-- up to and including it\n-- ⚠️ This is O(N²) — fine for \u003C10K rows, catastrophic for millions\n-- For large data: use t-digest extension for approximate running percentiles\nSELECT o1.ordered_on,\n  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o2.amount) AS running_median\nFROM orders o1\nJOIN orders o2 ON o2.ordered_on \u003C= o1.ordered_on  -- all prior + current\nGROUP BY o1.ordered_on\nORDER BY o1.ordered_on;\n-- For production: install tdigest extension and use:\n--   tdigest(0.5, amount) — approximate, O(N log N) memory, very fast\n",[34,1902,1903,1908,1913,1918,1923,1928,1933,1947,1984,1991,2020,2031,2043,2048],{"__ignoreMap":32},[37,1904,1905],{"class":39,"line":40},[37,1906,1907],{"class":43},"-- There's no built-in \"windowed ordered-set aggregate\" in PostgreSQL\n",[37,1909,1910],{"class":39,"line":47},[37,1911,1912],{"class":43},"-- PERCENTILE_CONT can't be used as a window function directly\n",[37,1914,1915],{"class":39,"line":53},[37,1916,1917],{"class":43},"-- Workaround: self-join — for each row, compute the median of all rows\n",[37,1919,1920],{"class":39,"line":59},[37,1921,1922],{"class":43},"-- up to and including it\n",[37,1924,1925],{"class":39,"line":65},[37,1926,1927],{"class":43},"-- ⚠️ This is O(N²) — fine for \u003C10K rows, catastrophic for millions\n",[37,1929,1930],{"class":39,"line":71},[37,1931,1932],{"class":43},"-- For large data: use t-digest extension for approximate running percentiles\n",[37,1934,1935,1937,1940,1942,1945],{"class":39,"line":78},[37,1936,561],{"class":74},[37,1938,1939],{"class":88}," o1",[37,1941,1668],{"class":81},[37,1943,1944],{"class":88},"ordered_on",[37,1946,799],{"class":81},[37,1948,1949,1951,1953,1955,1957,1959,1961,1963,1965,1967,1969,1972,1974,1977,1979,1981],{"class":39,"line":85},[37,1950,1661],{"class":88},[37,1952,357],{"class":81},[37,1954,1244],{"class":88},[37,1956,1668],{"class":81},[37,1958,1671],{"class":88},[37,1960,122],{"class":81},[37,1962,1676],{"class":74},[37,1964,1679],{"class":88},[37,1966,98],{"class":81},[37,1968,290],{"class":74},[37,1970,1971],{"class":88}," o2",[37,1973,1668],{"class":81},[37,1975,1976],{"class":88},"amount",[37,1978,122],{"class":81},[37,1980,125],{"class":74},[37,1982,1983],{"class":81}," running_median\n",[37,1985,1986,1988],{"class":39,"line":131},[37,1987,246],{"class":74},[37,1989,1990],{"class":81}," orders o1\n",[37,1992,1993,1996,1999,2002,2004,2006,2008,2011,2013,2015,2017],{"class":39,"line":164},[37,1994,1995],{"class":74},"JOIN",[37,1997,1998],{"class":81}," orders o2 ",[37,2000,2001],{"class":74},"ON",[37,2003,1971],{"class":88},[37,2005,1668],{"class":81},[37,2007,1944],{"class":88},[37,2009,2010],{"class":74}," \u003C=",[37,2012,1939],{"class":88},[37,2014,1668],{"class":81},[37,2016,1944],{"class":88},[37,2018,2019],{"class":43},"  -- all prior + current\n",[37,2021,2022,2024,2026,2028],{"class":39,"line":197},[37,2023,281],{"class":74},[37,2025,1939],{"class":88},[37,2027,1668],{"class":81},[37,2029,2030],{"class":88},"ordered_on\n",[37,2032,2033,2035,2037,2039,2041],{"class":39,"line":230},[37,2034,290],{"class":74},[37,2036,1939],{"class":88},[37,2038,1668],{"class":81},[37,2040,1944],{"class":88},[37,2042,1891],{"class":81},[37,2044,2045],{"class":39,"line":243},[37,2046,2047],{"class":43},"-- For production: install tdigest extension and use:\n",[37,2049,2050],{"class":39,"line":252},[37,2051,2052],{"class":43},"--   tdigest(0.5, amount) — approximate, O(N log N) memory, very fast\n",[14,2054,2056],{"id":2055},"temporal-state-machine-queries","Temporal \u002F State-Machine Queries",[19,2058,2060],{"id":2059},"sessionization-with-window-functions","Sessionization with Window Functions",[24,2062,2063],{"language":26},[28,2064,2066],{"className":30,"code":2065,"language":26,"meta":32,"style":32},"-- Group user events into sessions: a new session starts when there's\n-- a gap of > 30 minutes since the previous event for the same user\n-- This is a classic \"gaps-and-islands\" applied to timestamps\nWITH lagged AS (\n  SELECT user_id, event_type, event_at,\n    -- LAG gets the previous event time for the same user\n    LAG(event_at) OVER (PARTITION BY user_id ORDER BY event_at) AS prev_event_at,\n    -- Gap in seconds: if first event, prev is NULL → gap is NULL → new session\n    EXTRACT(EPOCH FROM (\n      event_at - LAG(event_at) OVER (PARTITION BY user_id ORDER BY event_at)\n    )) AS gap_seconds\n  FROM events\n),\nsession_marks AS (\n  SELECT user_id, event_type, event_at, gap_seconds,\n    -- SUM(1) over a window: increments by 1 whenever a new session starts\n    -- Each row where gap > 1800 (30 min) gets a 1; others get 0\n    -- The cumulative sum is the session number\n    SUM(CASE WHEN gap_seconds > 1800 OR gap_seconds IS NULL THEN 1 ELSE 0 END)\n      OVER (PARTITION BY user_id ORDER BY event_at) AS session_num\n  FROM lagged\n)\nSELECT user_id, session_num,\n  MIN(event_at) AS session_start,\n  MAX(event_at) AS session_end,\n  COUNT(*) AS event_count,\n  -- Session duration: from first to last event in the session\n  EXTRACT(EPOCH FROM (MAX(event_at) - MIN(event_at))) AS duration_seconds\nFROM session_marks\nGROUP BY user_id, session_num\nORDER BY user_id, session_start;\n",[34,2067,2068,2073,2078,2083,2094,2101,2106,2134,2139,2148,2175,2185,2191,2195,2204,2211,2216,2221,2226,2269,2291,2298,2302,2309,2319,2329,2344,2349,2374,2382,2390],{"__ignoreMap":32},[37,2069,2070],{"class":39,"line":40},[37,2071,2072],{"class":43},"-- Group user events into sessions: a new session starts when there's\n",[37,2074,2075],{"class":39,"line":47},[37,2076,2077],{"class":43},"-- a gap of > 30 minutes since the previous event for the same user\n",[37,2079,2080],{"class":39,"line":53},[37,2081,2082],{"class":43},"-- This is a classic \"gaps-and-islands\" applied to timestamps\n",[37,2084,2085,2087,2090,2092],{"class":39,"line":59},[37,2086,722],{"class":74},[37,2088,2089],{"class":81}," lagged ",[37,2091,125],{"class":74},[37,2093,730],{"class":81},[37,2095,2096,2098],{"class":39,"line":65},[37,2097,790],{"class":74},[37,2099,2100],{"class":81}," user_id, event_type, event_at,\n",[37,2102,2103],{"class":39,"line":71},[37,2104,2105],{"class":43},"    -- LAG gets the previous event time for the same user\n",[37,2107,2108,2111,2114,2116,2118,2120,2122,2124,2126,2129,2131],{"class":39,"line":78},[37,2109,2110],{"class":88},"    LAG",[37,2112,2113],{"class":81},"(event_at) ",[37,2115,810],{"class":74},[37,2117,98],{"class":81},[37,2119,815],{"class":74},[37,2121,818],{"class":74},[37,2123,821],{"class":81},[37,2125,290],{"class":74},[37,2127,2128],{"class":81}," event_at) ",[37,2130,125],{"class":74},[37,2132,2133],{"class":81}," prev_event_at,\n",[37,2135,2136],{"class":39,"line":85},[37,2137,2138],{"class":43},"    -- Gap in seconds: if first event, prev is NULL → gap is NULL → new session\n",[37,2140,2141,2144,2146],{"class":39,"line":131},[37,2142,2143],{"class":81},"    EXTRACT(EPOCH ",[37,2145,246],{"class":74},[37,2147,730],{"class":81},[37,2149,2150,2153,2155,2158,2160,2162,2164,2166,2168,2170,2172],{"class":39,"line":164},[37,2151,2152],{"class":81},"      event_at ",[37,2154,1015],{"class":74},[37,2156,2157],{"class":88}," LAG",[37,2159,2113],{"class":81},[37,2161,810],{"class":74},[37,2163,98],{"class":81},[37,2165,815],{"class":74},[37,2167,818],{"class":74},[37,2169,821],{"class":81},[37,2171,290],{"class":74},[37,2173,2174],{"class":81}," event_at)\n",[37,2176,2177,2180,2182],{"class":39,"line":197},[37,2178,2179],{"class":81},"    )) ",[37,2181,125],{"class":74},[37,2183,2184],{"class":81}," gap_seconds\n",[37,2186,2187,2189],{"class":39,"line":230},[37,2188,768],{"class":74},[37,2190,771],{"class":81},[37,2192,2193],{"class":39,"line":243},[37,2194,776],{"class":81},[37,2196,2197,2200,2202],{"class":39,"line":252},[37,2198,2199],{"class":81},"session_marks ",[37,2201,125],{"class":74},[37,2203,730],{"class":81},[37,2205,2206,2208],{"class":39,"line":278},[37,2207,790],{"class":74},[37,2209,2210],{"class":81}," user_id, event_type, event_at, gap_seconds,\n",[37,2212,2213],{"class":39,"line":287},[37,2214,2215],{"class":43},"    -- SUM(1) over a window: increments by 1 whenever a new session starts\n",[37,2217,2218],{"class":39,"line":296},[37,2219,2220],{"class":43},"    -- Each row where gap > 1800 (30 min) gets a 1; others get 0\n",[37,2222,2223],{"class":39,"line":302},[37,2224,2225],{"class":43},"    -- The cumulative sum is the session number\n",[37,2227,2228,2231,2233,2235,2237,2240,2242,2245,2248,2250,2253,2256,2258,2260,2263,2265,2267],{"class":39,"line":308},[37,2229,2230],{"class":88},"    SUM",[37,2232,357],{"class":81},[37,2234,360],{"class":74},[37,2236,363],{"class":74},[37,2238,2239],{"class":81}," gap_seconds ",[37,2241,1376],{"class":74},[37,2243,2244],{"class":88}," 1800",[37,2246,2247],{"class":74}," OR",[37,2249,2239],{"class":81},[37,2251,2252],{"class":74},"IS",[37,2254,2255],{"class":74}," NULL",[37,2257,378],{"class":74},[37,2259,119],{"class":88},[37,2261,2262],{"class":74}," ELSE",[37,2264,387],{"class":88},[37,2266,390],{"class":74},[37,2268,889],{"class":81},[37,2270,2271,2274,2276,2278,2280,2282,2284,2286,2288],{"class":39,"line":658},[37,2272,2273],{"class":74},"      OVER",[37,2275,98],{"class":81},[37,2277,815],{"class":74},[37,2279,818],{"class":74},[37,2281,821],{"class":81},[37,2283,290],{"class":74},[37,2285,2128],{"class":81},[37,2287,125],{"class":74},[37,2289,2290],{"class":81}," session_num\n",[37,2292,2293,2295],{"class":39,"line":664},[37,2294,768],{"class":74},[37,2296,2297],{"class":81}," lagged\n",[37,2299,2300],{"class":39,"line":892},[37,2301,889],{"class":81},[37,2303,2304,2306],{"class":39,"line":900},[37,2305,561],{"class":74},[37,2307,2308],{"class":81}," user_id, session_num,\n",[37,2310,2311,2313,2315,2317],{"class":39,"line":917},[37,2312,903],{"class":88},[37,2314,2113],{"class":81},[37,2316,125],{"class":74},[37,2318,914],{"class":81},[37,2320,2321,2323,2325,2327],{"class":39,"line":934},[37,2322,920],{"class":88},[37,2324,2113],{"class":81},[37,2326,125],{"class":74},[37,2328,931],{"class":81},[37,2330,2331,2333,2335,2337,2339,2341],{"class":39,"line":952},[37,2332,937],{"class":88},[37,2334,357],{"class":81},[37,2336,942],{"class":74},[37,2338,122],{"class":81},[37,2340,125],{"class":74},[37,2342,2343],{"class":81}," event_count,\n",[37,2345,2346],{"class":39,"line":960},[37,2347,2348],{"class":43},"  -- Session duration: from first to last event in the session\n",[37,2350,2351,2354,2356,2358,2360,2362,2364,2366,2369,2371],{"class":39,"line":968},[37,2352,2353],{"class":81},"  EXTRACT(EPOCH ",[37,2355,246],{"class":74},[37,2357,98],{"class":81},[37,2359,1111],{"class":88},[37,2361,2113],{"class":81},[37,2363,1015],{"class":74},[37,2365,1100],{"class":88},[37,2367,2368],{"class":81},"(event_at))) ",[37,2370,125],{"class":74},[37,2372,2373],{"class":81}," duration_seconds\n",[37,2375,2377,2379],{"class":39,"line":2376},29,[37,2378,246],{"class":74},[37,2380,2381],{"class":81}," session_marks\n",[37,2383,2385,2387],{"class":39,"line":2384},30,[37,2386,281],{"class":74},[37,2388,2389],{"class":81}," user_id, session_num\n",[37,2391,2393,2395],{"class":39,"line":2392},31,[37,2394,290],{"class":74},[37,2396,973],{"class":81},[19,2398,2400],{"id":2399},"state-change-detection","State Change Detection",[24,2402,2403],{"language":26},[28,2404,2406],{"className":30,"code":2405,"language":26,"meta":32,"style":32},"-- Find points where an entity's status changed (vs the previous row)\n-- IS DISTINCT FROM treats NULL as a distinct value:\n--   NULL IS DISTINCT FROM NULL → FALSE (same)\n--   'active' IS DISTINCT FROM NULL → TRUE (different)\n--   'active' IS DISTINCT FROM 'active' → FALSE (same)\n-- This correctly handles the first row (prev_status = NULL → IS DISTINCT FROM → TRUE)\nWITH lagged AS (\n  SELECT id, entity_id, status, changed_at,\n    LAG(status) OVER (PARTITION BY entity_id ORDER BY changed_at) AS prev_status\n  FROM status_history\n)\nSELECT id, entity_id, status, changed_at\nFROM lagged\nWHERE status IS DISTINCT FROM prev_status  -- only rows where status changed\nORDER BY entity_id, changed_at;\n",[34,2407,2408,2413,2418,2423,2428,2433,2438,2448,2461,2492,2499,2503,2514,2520,2541],{"__ignoreMap":32},[37,2409,2410],{"class":39,"line":40},[37,2411,2412],{"class":43},"-- Find points where an entity's status changed (vs the previous row)\n",[37,2414,2415],{"class":39,"line":47},[37,2416,2417],{"class":43},"-- IS DISTINCT FROM treats NULL as a distinct value:\n",[37,2419,2420],{"class":39,"line":53},[37,2421,2422],{"class":43},"--   NULL IS DISTINCT FROM NULL → FALSE (same)\n",[37,2424,2425],{"class":39,"line":59},[37,2426,2427],{"class":43},"--   'active' IS DISTINCT FROM NULL → TRUE (different)\n",[37,2429,2430],{"class":39,"line":65},[37,2431,2432],{"class":43},"--   'active' IS DISTINCT FROM 'active' → FALSE (same)\n",[37,2434,2435],{"class":39,"line":71},[37,2436,2437],{"class":43},"-- This correctly handles the first row (prev_status = NULL → IS DISTINCT FROM → TRUE)\n",[37,2439,2440,2442,2444,2446],{"class":39,"line":78},[37,2441,722],{"class":74},[37,2443,2089],{"class":81},[37,2445,125],{"class":74},[37,2447,730],{"class":81},[37,2449,2450,2452,2455,2458],{"class":39,"line":85},[37,2451,790],{"class":74},[37,2453,2454],{"class":81}," id, entity_id, ",[37,2456,2457],{"class":74},"status",[37,2459,2460],{"class":81},", changed_at,\n",[37,2462,2463,2465,2467,2469,2471,2473,2475,2477,2479,2482,2484,2487,2489],{"class":39,"line":131},[37,2464,2110],{"class":88},[37,2466,357],{"class":81},[37,2468,2457],{"class":74},[37,2470,122],{"class":81},[37,2472,810],{"class":74},[37,2474,98],{"class":81},[37,2476,815],{"class":74},[37,2478,818],{"class":74},[37,2480,2481],{"class":81}," entity_id ",[37,2483,290],{"class":74},[37,2485,2486],{"class":81}," changed_at) ",[37,2488,125],{"class":74},[37,2490,2491],{"class":81}," prev_status\n",[37,2493,2494,2496],{"class":39,"line":164},[37,2495,768],{"class":74},[37,2497,2498],{"class":81}," status_history\n",[37,2500,2501],{"class":39,"line":197},[37,2502,889],{"class":81},[37,2504,2505,2507,2509,2511],{"class":39,"line":230},[37,2506,561],{"class":74},[37,2508,2454],{"class":81},[37,2510,2457],{"class":74},[37,2512,2513],{"class":81},", changed_at\n",[37,2515,2516,2518],{"class":39,"line":243},[37,2517,246],{"class":74},[37,2519,2297],{"class":81},[37,2521,2522,2524,2527,2530,2533,2535,2538],{"class":39,"line":252},[37,2523,101],{"class":74},[37,2525,2526],{"class":74}," status",[37,2528,2529],{"class":74}," IS",[37,2531,2532],{"class":74}," DISTINCT",[37,2534,110],{"class":74},[37,2536,2537],{"class":81}," prev_status  ",[37,2539,2540],{"class":43},"-- only rows where status changed\n",[37,2542,2543,2545],{"class":39,"line":278},[37,2544,290],{"class":74},[37,2546,2547],{"class":81}," entity_id, changed_at;\n",[19,2549,2551],{"id":2550},"time-in-state-durations","Time-in-State Durations",[24,2553,2554],{"language":26},[28,2555,2557],{"className":30,"code":2556,"language":26,"meta":32,"style":32},"-- How long was each entity in each state?\n-- LEAD gets the NEXT changed_at; the difference is the time in the current state\n-- The last row per entity has LEAD = NULL (still in that state — no next change)\nWITH state_changes AS (\n  SELECT entity_id, status, changed_at,\n    LEAD(changed_at) OVER (PARTITION BY entity_id ORDER BY changed_at) AS next_change\n  FROM status_history\n)\nSELECT entity_id, status,\n  changed_at AS entered_at,\n  next_change AS exited_at,\n  -- COALESCE: if no next change, use now() — entity is still in this state\n  COALESCE(next_change, now()) - changed_at AS duration\nFROM state_changes\nORDER BY entity_id, changed_at;\n",[34,2558,2559,2564,2569,2574,2585,2596,2623,2629,2633,2643,2653,2663,2668,2692,2699],{"__ignoreMap":32},[37,2560,2561],{"class":39,"line":40},[37,2562,2563],{"class":43},"-- How long was each entity in each state?\n",[37,2565,2566],{"class":39,"line":47},[37,2567,2568],{"class":43},"-- LEAD gets the NEXT changed_at; the difference is the time in the current state\n",[37,2570,2571],{"class":39,"line":53},[37,2572,2573],{"class":43},"-- The last row per entity has LEAD = NULL (still in that state — no next change)\n",[37,2575,2576,2578,2581,2583],{"class":39,"line":59},[37,2577,722],{"class":74},[37,2579,2580],{"class":81}," state_changes ",[37,2582,125],{"class":74},[37,2584,730],{"class":81},[37,2586,2587,2589,2592,2594],{"class":39,"line":65},[37,2588,790],{"class":74},[37,2590,2591],{"class":81}," entity_id, ",[37,2593,2457],{"class":74},[37,2595,2460],{"class":81},[37,2597,2598,2601,2604,2606,2608,2610,2612,2614,2616,2618,2620],{"class":39,"line":71},[37,2599,2600],{"class":88},"    LEAD",[37,2602,2603],{"class":81},"(changed_at) ",[37,2605,810],{"class":74},[37,2607,98],{"class":81},[37,2609,815],{"class":74},[37,2611,818],{"class":74},[37,2613,2481],{"class":81},[37,2615,290],{"class":74},[37,2617,2486],{"class":81},[37,2619,125],{"class":74},[37,2621,2622],{"class":81}," next_change\n",[37,2624,2625,2627],{"class":39,"line":78},[37,2626,768],{"class":74},[37,2628,2498],{"class":81},[37,2630,2631],{"class":39,"line":85},[37,2632,889],{"class":81},[37,2634,2635,2637,2639,2641],{"class":39,"line":131},[37,2636,561],{"class":74},[37,2638,2591],{"class":81},[37,2640,2457],{"class":74},[37,2642,799],{"class":81},[37,2644,2645,2648,2650],{"class":39,"line":164},[37,2646,2647],{"class":81},"  changed_at ",[37,2649,125],{"class":74},[37,2651,2652],{"class":81}," entered_at,\n",[37,2654,2655,2658,2660],{"class":39,"line":197},[37,2656,2657],{"class":81},"  next_change ",[37,2659,125],{"class":74},[37,2661,2662],{"class":81}," exited_at,\n",[37,2664,2665],{"class":39,"line":230},[37,2666,2667],{"class":43},"  -- COALESCE: if no next change, use now() — entity is still in this state\n",[37,2669,2670,2673,2676,2679,2682,2684,2687,2689],{"class":39,"line":243},[37,2671,2672],{"class":88},"  COALESCE",[37,2674,2675],{"class":81},"(next_change, ",[37,2677,2678],{"class":74},"now",[37,2680,2681],{"class":81},"()) ",[37,2683,1015],{"class":74},[37,2685,2686],{"class":81}," changed_at ",[37,2688,125],{"class":74},[37,2690,2691],{"class":81}," duration\n",[37,2693,2694,2696],{"class":39,"line":252},[37,2695,246],{"class":74},[37,2697,2698],{"class":81}," state_changes\n",[37,2700,2701,2703],{"class":39,"line":278},[37,2702,290],{"class":74},[37,2704,2547],{"class":81},[14,2706,2708],{"id":2707},"complex-example-sessionization-histogram","Complex Example — Sessionization + Histogram",[24,2710,2711],{"language":26},[28,2712,2714],{"className":30,"code":2713,"language":26,"meta":32,"style":32},"-- Combine sessionization with a histogram of session durations\n-- Goal: for each user, find their sessions (30-min timeout), then\n-- build a histogram of session durations across all users\nWITH lagged AS (\n  SELECT user_id, event_at,\n    EXTRACT(EPOCH FROM (\n      event_at - LAG(event_at) OVER (PARTITION BY user_id ORDER BY event_at)\n    )) AS gap_seconds\n  FROM events\n),\nsessions AS (\n  SELECT user_id, event_at, gap_seconds,\n    SUM(CASE WHEN gap_seconds > 1800 OR gap_seconds IS NULL THEN 1 ELSE 0 END)\n      OVER (PARTITION BY user_id ORDER BY event_at) AS session_num\n  FROM lagged\n),\nsession_stats AS (\n  SELECT user_id, session_num,\n    MIN(event_at) AS session_start,\n    MAX(event_at) AS session_end,\n    EXTRACT(EPOCH FROM (MAX(event_at) - MIN(event_at))) AS duration_seconds\n  FROM sessions\n  GROUP BY user_id, session_num\n)\n-- Histogram: 0-60s, 60-300s, 300-900s, 900-1800s, 1800s+\nSELECT\n  CASE\n    WHEN duration_seconds \u003C 60   THEN '0-1min'\n    WHEN duration_seconds \u003C 300  THEN '1-5min'\n    WHEN duration_seconds \u003C 900  THEN '5-15min'\n    WHEN duration_seconds \u003C 1800 THEN '15-30min'\n    ELSE '30min+'\n  END AS duration_bucket,\n  COUNT(*) AS session_count,\n  ROUND(AVG(duration_seconds)::numeric, 1) AS avg_duration_seconds\nFROM session_stats\nGROUP BY duration_bucket\nORDER BY MIN(duration_seconds);\n",[34,2715,2716,2721,2726,2731,2741,2748,2756,2780,2788,2794,2798,2807,2814,2850,2870,2876,2880,2889,2895,2906,2917,2939,2946,2953,2957,2962,2966,2970,2987,3003,3019,3034,3042,3052,3068,3094,3102,3110],{"__ignoreMap":32},[37,2717,2718],{"class":39,"line":40},[37,2719,2720],{"class":43},"-- Combine sessionization with a histogram of session durations\n",[37,2722,2723],{"class":39,"line":47},[37,2724,2725],{"class":43},"-- Goal: for each user, find their sessions (30-min timeout), then\n",[37,2727,2728],{"class":39,"line":53},[37,2729,2730],{"class":43},"-- build a histogram of session durations across all users\n",[37,2732,2733,2735,2737,2739],{"class":39,"line":59},[37,2734,722],{"class":74},[37,2736,2089],{"class":81},[37,2738,125],{"class":74},[37,2740,730],{"class":81},[37,2742,2743,2745],{"class":39,"line":65},[37,2744,790],{"class":74},[37,2746,2747],{"class":81}," user_id, event_at,\n",[37,2749,2750,2752,2754],{"class":39,"line":71},[37,2751,2143],{"class":81},[37,2753,246],{"class":74},[37,2755,730],{"class":81},[37,2757,2758,2760,2762,2764,2766,2768,2770,2772,2774,2776,2778],{"class":39,"line":78},[37,2759,2152],{"class":81},[37,2761,1015],{"class":74},[37,2763,2157],{"class":88},[37,2765,2113],{"class":81},[37,2767,810],{"class":74},[37,2769,98],{"class":81},[37,2771,815],{"class":74},[37,2773,818],{"class":74},[37,2775,821],{"class":81},[37,2777,290],{"class":74},[37,2779,2174],{"class":81},[37,2781,2782,2784,2786],{"class":39,"line":85},[37,2783,2179],{"class":81},[37,2785,125],{"class":74},[37,2787,2184],{"class":81},[37,2789,2790,2792],{"class":39,"line":131},[37,2791,768],{"class":74},[37,2793,771],{"class":81},[37,2795,2796],{"class":39,"line":164},[37,2797,776],{"class":81},[37,2799,2800,2803,2805],{"class":39,"line":197},[37,2801,2802],{"class":74},"sessions",[37,2804,760],{"class":74},[37,2806,730],{"class":81},[37,2808,2809,2811],{"class":39,"line":230},[37,2810,790],{"class":74},[37,2812,2813],{"class":81}," user_id, event_at, gap_seconds,\n",[37,2815,2816,2818,2820,2822,2824,2826,2828,2830,2832,2834,2836,2838,2840,2842,2844,2846,2848],{"class":39,"line":243},[37,2817,2230],{"class":88},[37,2819,357],{"class":81},[37,2821,360],{"class":74},[37,2823,363],{"class":74},[37,2825,2239],{"class":81},[37,2827,1376],{"class":74},[37,2829,2244],{"class":88},[37,2831,2247],{"class":74},[37,2833,2239],{"class":81},[37,2835,2252],{"class":74},[37,2837,2255],{"class":74},[37,2839,378],{"class":74},[37,2841,119],{"class":88},[37,2843,2262],{"class":74},[37,2845,387],{"class":88},[37,2847,390],{"class":74},[37,2849,889],{"class":81},[37,2851,2852,2854,2856,2858,2860,2862,2864,2866,2868],{"class":39,"line":252},[37,2853,2273],{"class":74},[37,2855,98],{"class":81},[37,2857,815],{"class":74},[37,2859,818],{"class":74},[37,2861,821],{"class":81},[37,2863,290],{"class":74},[37,2865,2128],{"class":81},[37,2867,125],{"class":74},[37,2869,2290],{"class":81},[37,2871,2872,2874],{"class":39,"line":278},[37,2873,768],{"class":74},[37,2875,2297],{"class":81},[37,2877,2878],{"class":39,"line":287},[37,2879,776],{"class":81},[37,2881,2882,2885,2887],{"class":39,"line":296},[37,2883,2884],{"class":81},"session_stats ",[37,2886,125],{"class":74},[37,2888,730],{"class":81},[37,2890,2891,2893],{"class":39,"line":302},[37,2892,790],{"class":74},[37,2894,2308],{"class":81},[37,2896,2897,2900,2902,2904],{"class":39,"line":308},[37,2898,2899],{"class":88},"    MIN",[37,2901,2113],{"class":81},[37,2903,125],{"class":74},[37,2905,914],{"class":81},[37,2907,2908,2911,2913,2915],{"class":39,"line":658},[37,2909,2910],{"class":88},"    MAX",[37,2912,2113],{"class":81},[37,2914,125],{"class":74},[37,2916,931],{"class":81},[37,2918,2919,2921,2923,2925,2927,2929,2931,2933,2935,2937],{"class":39,"line":664},[37,2920,2143],{"class":81},[37,2922,246],{"class":74},[37,2924,98],{"class":81},[37,2926,1111],{"class":88},[37,2928,2113],{"class":81},[37,2930,1015],{"class":74},[37,2932,1100],{"class":88},[37,2934,2368],{"class":81},[37,2936,125],{"class":74},[37,2938,2373],{"class":81},[37,2940,2941,2943],{"class":39,"line":892},[37,2942,768],{"class":74},[37,2944,2945],{"class":74}," sessions\n",[37,2947,2948,2951],{"class":39,"line":900},[37,2949,2950],{"class":74},"  GROUP BY",[37,2952,2389],{"class":81},[37,2954,2955],{"class":39,"line":917},[37,2956,889],{"class":81},[37,2958,2959],{"class":39,"line":934},[37,2960,2961],{"class":43},"-- Histogram: 0-60s, 60-300s, 300-900s, 900-1800s, 1800s+\n",[37,2963,2964],{"class":39,"line":952},[37,2965,75],{"class":74},[37,2967,2968],{"class":39,"line":960},[37,2969,1445],{"class":74},[37,2971,2972,2974,2977,2979,2982,2984],{"class":39,"line":968},[37,2973,1450],{"class":74},[37,2975,2976],{"class":81}," duration_seconds ",[37,2978,272],{"class":74},[37,2980,2981],{"class":88}," 60",[37,2983,1460],{"class":74},[37,2985,2986],{"class":263}," '0-1min'\n",[37,2988,2989,2991,2993,2995,2998,3000],{"class":39,"line":2376},[37,2990,1450],{"class":74},[37,2992,2976],{"class":81},[37,2994,272],{"class":74},[37,2996,2997],{"class":88}," 300",[37,2999,1492],{"class":74},[37,3001,3002],{"class":263}," '1-5min'\n",[37,3004,3005,3007,3009,3011,3014,3016],{"class":39,"line":2384},[37,3006,1450],{"class":74},[37,3008,2976],{"class":81},[37,3010,272],{"class":74},[37,3012,3013],{"class":88}," 900",[37,3015,1492],{"class":74},[37,3017,3018],{"class":263}," '5-15min'\n",[37,3020,3021,3023,3025,3027,3029,3031],{"class":39,"line":2392},[37,3022,1450],{"class":74},[37,3024,2976],{"class":81},[37,3026,272],{"class":74},[37,3028,2244],{"class":88},[37,3030,378],{"class":74},[37,3032,3033],{"class":263}," '15-30min'\n",[37,3035,3037,3039],{"class":39,"line":3036},32,[37,3038,1532],{"class":74},[37,3040,3041],{"class":263}," '30min+'\n",[37,3043,3045,3047,3049],{"class":39,"line":3044},33,[37,3046,1540],{"class":74},[37,3048,760],{"class":74},[37,3050,3051],{"class":81}," duration_bucket,\n",[37,3053,3055,3057,3059,3061,3063,3065],{"class":39,"line":3054},34,[37,3056,937],{"class":88},[37,3058,357],{"class":81},[37,3060,942],{"class":74},[37,3062,122],{"class":81},[37,3064,125],{"class":74},[37,3066,3067],{"class":81}," session_count,\n",[37,3069,3071,3073,3075,3077,3080,3083,3085,3087,3089,3091],{"class":39,"line":3070},35,[37,3072,1344],{"class":88},[37,3074,357],{"class":81},[37,3076,1349],{"class":88},[37,3078,3079],{"class":81},"(duration_seconds)::",[37,3081,3082],{"class":74},"numeric",[37,3084,1247],{"class":81},[37,3086,1050],{"class":88},[37,3088,122],{"class":81},[37,3090,125],{"class":74},[37,3092,3093],{"class":81}," avg_duration_seconds\n",[37,3095,3097,3099],{"class":39,"line":3096},36,[37,3098,246],{"class":74},[37,3100,3101],{"class":81}," session_stats\n",[37,3103,3105,3107],{"class":39,"line":3104},37,[37,3106,281],{"class":74},[37,3108,3109],{"class":81}," duration_bucket\n",[37,3111,3113,3115,3117],{"class":39,"line":3112},38,[37,3114,290],{"class":74},[37,3116,1100],{"class":88},[37,3118,3119],{"class":81},"(duration_seconds);\n",[14,3121,3123],{"id":3122},"batch-insertion-with-returning-cte","Batch Insertion with RETURNING + CTE",[24,3125,3126],{"language":26},[28,3127,3129],{"className":30,"code":3128,"language":26,"meta":32,"style":32},"-- Insert a batch of orders and immediately use their generated IDs\n-- to insert child rows (order_items) — all in one statement\nWITH new_orders AS (\n  INSERT INTO orders (customer_id, amount, ordered_on)\n  SELECT customer_id, amount, now()\n  FROM (VALUES\n    (1, 50.00), (2, 75.50), (3, 120.00), (1, 30.00)\n  ) AS v(customer_id, amount)\n  RETURNING id, customer_id  -- RETURNING captures the generated IDs\n)\nINSERT INTO order_items (order_id, product_id, quantity, price)\nSELECT o.id, p.id, 1, o.amount\nFROM new_orders o\n-- LATERAL: for each new order, look up a product (correlated subquery)\nCROSS JOIN LATERAL (\n  SELECT id FROM products WHERE category_id = o.customer_id % 5 LIMIT 1\n) p;\n-- The CTE + RETURNING pattern lets you chain INSERTs atomically:\n-- if the order_items insert fails, the orders insert is rolled back too\n",[34,3130,3131,3136,3141,3152,3160,3172,3181,3239,3249,3257,3261,3269,3304,3311,3316,3324,3361,3366,3371],{"__ignoreMap":32},[37,3132,3133],{"class":39,"line":40},[37,3134,3135],{"class":43},"-- Insert a batch of orders and immediately use their generated IDs\n",[37,3137,3138],{"class":39,"line":47},[37,3139,3140],{"class":43},"-- to insert child rows (order_items) — all in one statement\n",[37,3142,3143,3145,3148,3150],{"class":39,"line":53},[37,3144,722],{"class":74},[37,3146,3147],{"class":81}," new_orders ",[37,3149,125],{"class":74},[37,3151,730],{"class":81},[37,3153,3154,3157],{"class":39,"line":59},[37,3155,3156],{"class":74},"  INSERT INTO",[37,3158,3159],{"class":81}," orders (customer_id, amount, ordered_on)\n",[37,3161,3162,3164,3167,3169],{"class":39,"line":65},[37,3163,790],{"class":74},[37,3165,3166],{"class":81}," customer_id, amount, ",[37,3168,2678],{"class":74},[37,3170,3171],{"class":81},"()\n",[37,3173,3174,3176,3178],{"class":39,"line":71},[37,3175,768],{"class":74},[37,3177,98],{"class":81},[37,3179,3180],{"class":74},"VALUES\n",[37,3182,3183,3186,3188,3190,3193,3195,3198,3201,3203,3205,3207,3209,3211,3213,3215,3217,3220,3222,3224,3226,3228,3230,3233,3235,3237],{"class":39,"line":78},[37,3184,3185],{"class":81},"    (",[37,3187,1050],{"class":88},[37,3189,1247],{"class":81},[37,3191,3192],{"class":88},"50",[37,3194,1668],{"class":81},[37,3196,3197],{"class":88},"00",[37,3199,3200],{"class":81},"), (",[37,3202,1056],{"class":88},[37,3204,1247],{"class":81},[37,3206,1734],{"class":88},[37,3208,1668],{"class":81},[37,3210,3192],{"class":88},[37,3212,3200],{"class":81},[37,3214,1061],{"class":88},[37,3216,1247],{"class":81},[37,3218,3219],{"class":88},"120",[37,3221,1668],{"class":81},[37,3223,3197],{"class":88},[37,3225,3200],{"class":81},[37,3227,1050],{"class":88},[37,3229,1247],{"class":81},[37,3231,3232],{"class":88},"30",[37,3234,1668],{"class":81},[37,3236,3197],{"class":88},[37,3238,889],{"class":81},[37,3240,3241,3244,3246],{"class":39,"line":85},[37,3242,3243],{"class":81},"  ) ",[37,3245,125],{"class":74},[37,3247,3248],{"class":81}," v(customer_id, amount)\n",[37,3250,3251,3254],{"class":39,"line":131},[37,3252,3253],{"class":81},"  RETURNING id, customer_id  ",[37,3255,3256],{"class":43},"-- RETURNING captures the generated IDs\n",[37,3258,3259],{"class":39,"line":164},[37,3260,889],{"class":81},[37,3262,3263,3266],{"class":39,"line":197},[37,3264,3265],{"class":74},"INSERT INTO",[37,3267,3268],{"class":81}," order_items (order_id, product_id, quantity, price)\n",[37,3270,3271,3273,3276,3278,3281,3283,3286,3288,3290,3292,3294,3296,3299,3301],{"class":39,"line":230},[37,3272,561],{"class":74},[37,3274,3275],{"class":88}," o",[37,3277,1668],{"class":81},[37,3279,3280],{"class":88},"id",[37,3282,1247],{"class":81},[37,3284,3285],{"class":88},"p",[37,3287,1668],{"class":81},[37,3289,3280],{"class":88},[37,3291,1247],{"class":81},[37,3293,1050],{"class":88},[37,3295,1247],{"class":81},[37,3297,3298],{"class":88},"o",[37,3300,1668],{"class":81},[37,3302,3303],{"class":88},"amount\n",[37,3305,3306,3308],{"class":39,"line":243},[37,3307,246],{"class":74},[37,3309,3310],{"class":81}," new_orders o\n",[37,3312,3313],{"class":39,"line":252},[37,3314,3315],{"class":43},"-- LATERAL: for each new order, look up a product (correlated subquery)\n",[37,3317,3318,3321],{"class":39,"line":278},[37,3319,3320],{"class":74},"CROSS JOIN",[37,3322,3323],{"class":81}," LATERAL (\n",[37,3325,3326,3328,3331,3333,3336,3338,3341,3343,3345,3347,3350,3353,3355,3358],{"class":39,"line":287},[37,3327,790],{"class":74},[37,3329,3330],{"class":81}," id ",[37,3332,246],{"class":74},[37,3334,3335],{"class":81}," products ",[37,3337,101],{"class":74},[37,3339,3340],{"class":81}," category_id ",[37,3342,116],{"class":74},[37,3344,3275],{"class":88},[37,3346,1668],{"class":81},[37,3348,3349],{"class":88},"customer_id",[37,3351,3352],{"class":81}," % ",[37,3354,1671],{"class":88},[37,3356,3357],{"class":74}," LIMIT",[37,3359,3360],{"class":88}," 1\n",[37,3362,3363],{"class":39,"line":296},[37,3364,3365],{"class":81},") p;\n",[37,3367,3368],{"class":39,"line":302},[37,3369,3370],{"class":43},"-- The CTE + RETURNING pattern lets you chain INSERTs atomically:\n",[37,3372,3373],{"class":39,"line":308},[37,3374,3375],{"class":43},"-- if the order_items insert fails, the orders insert is rolled back too\n",[14,3377,3379],{"id":3378},"conditional-upsert","Conditional UPSERT",[24,3381,3382],{"language":26},[28,3383,3385],{"className":30,"code":3384,"language":26,"meta":32,"style":32},"-- ON CONFLICT: if the row exists, conditionally update only if the new\n-- data is \"newer\" or \"different\" — avoid unnecessary writes that trigger\n-- WAL + replication traffic + index updates\nINSERT INTO product_prices (product_id, price, effective_at)\nVALUES (42, 19.99, '2024-06-01 10:00:00+00')\nON CONFLICT (product_id) DO UPDATE\nSET price = EXCLUDED.price,\n    effective_at = EXCLUDED.effective_at\nWHERE product_prices.price IS DISTINCT FROM EXCLUDED.price\n  -- IS DISTINCT FROM: only update if the price actually changed\n  -- (handles NULL correctly: NULL IS DISTINCT FROM 19.99 → TRUE)\n  OR product_prices.effective_at \u003C EXCLUDED.effective_at;\n-- Without the WHERE clause, every UPSERT writes a new row version\n-- (even if nothing changed) → MVCC bloat + WAL + replication for nothing\n",[34,3386,3387,3392,3397,3402,3409,3435,3445,3465,3479,3503,3508,3513,3536,3541],{"__ignoreMap":32},[37,3388,3389],{"class":39,"line":40},[37,3390,3391],{"class":43},"-- ON CONFLICT: if the row exists, conditionally update only if the new\n",[37,3393,3394],{"class":39,"line":47},[37,3395,3396],{"class":43},"-- data is \"newer\" or \"different\" — avoid unnecessary writes that trigger\n",[37,3398,3399],{"class":39,"line":53},[37,3400,3401],{"class":43},"-- WAL + replication traffic + index updates\n",[37,3403,3404,3406],{"class":39,"line":59},[37,3405,3265],{"class":74},[37,3407,3408],{"class":81}," product_prices (product_id, price, effective_at)\n",[37,3410,3411,3413,3415,3418,3420,3423,3425,3428,3430,3433],{"class":39,"line":65},[37,3412,1045],{"class":74},[37,3414,98],{"class":81},[37,3416,3417],{"class":88},"42",[37,3419,1247],{"class":81},[37,3421,3422],{"class":88},"19",[37,3424,1668],{"class":81},[37,3426,3427],{"class":88},"99",[37,3429,1247],{"class":81},[37,3431,3432],{"class":263},"'2024-06-01 10:00:00+00'",[37,3434,889],{"class":81},[37,3436,3437,3439,3442],{"class":39,"line":71},[37,3438,2001],{"class":74},[37,3440,3441],{"class":81}," CONFLICT (product_id) DO ",[37,3443,3444],{"class":74},"UPDATE\n",[37,3446,3447,3450,3453,3455,3458,3460,3463],{"class":39,"line":78},[37,3448,3449],{"class":74},"SET",[37,3451,3452],{"class":81}," price ",[37,3454,116],{"class":74},[37,3456,3457],{"class":88}," EXCLUDED",[37,3459,1668],{"class":81},[37,3461,3462],{"class":88},"price",[37,3464,799],{"class":81},[37,3466,3467,3470,3472,3474,3476],{"class":39,"line":85},[37,3468,3469],{"class":81},"    effective_at ",[37,3471,116],{"class":74},[37,3473,3457],{"class":88},[37,3475,1668],{"class":81},[37,3477,3478],{"class":88},"effective_at\n",[37,3480,3481,3483,3486,3488,3490,3492,3494,3496,3498,3500],{"class":39,"line":131},[37,3482,101],{"class":74},[37,3484,3485],{"class":88}," product_prices",[37,3487,1668],{"class":81},[37,3489,3462],{"class":88},[37,3491,2529],{"class":74},[37,3493,2532],{"class":74},[37,3495,110],{"class":74},[37,3497,3457],{"class":88},[37,3499,1668],{"class":81},[37,3501,3502],{"class":88},"price\n",[37,3504,3505],{"class":39,"line":164},[37,3506,3507],{"class":43},"  -- IS DISTINCT FROM: only update if the price actually changed\n",[37,3509,3510],{"class":39,"line":197},[37,3511,3512],{"class":43},"  -- (handles NULL correctly: NULL IS DISTINCT FROM 19.99 → TRUE)\n",[37,3514,3515,3518,3520,3522,3525,3528,3530,3532,3534],{"class":39,"line":230},[37,3516,3517],{"class":74},"  OR",[37,3519,3485],{"class":88},[37,3521,1668],{"class":81},[37,3523,3524],{"class":88},"effective_at",[37,3526,3527],{"class":74}," \u003C",[37,3529,3457],{"class":88},[37,3531,1668],{"class":81},[37,3533,3524],{"class":88},[37,3535,1891],{"class":81},[37,3537,3538],{"class":39,"line":243},[37,3539,3540],{"class":43},"-- Without the WHERE clause, every UPSERT writes a new row version\n",[37,3542,3543],{"class":39,"line":252},[37,3544,3545],{"class":43},"-- (even if nothing changed) → MVCC bloat + WAL + replication for nothing\n",[14,3547,3549],{"id":3548},"dynamic-sql-with-execute","Dynamic SQL with EXECUTE",[24,3551,3552],{"language":26},[28,3553,3555],{"className":30,"code":3554,"language":26,"meta":32,"style":32},"-- Dynamic SQL: build and execute SQL at runtime inside PL\u002FpgSQL\n-- Use EXECUTE FORMAT for safe identifier injection (prevent SQL injection)\nDO $$\nDECLARE\n  tbl text := 'orders_' || to_char(now(), 'YYYY_MM');  -- dynamic table name\n  schema_name text := 'public';\nBEGIN\n  -- %I = identifier (quoted, injection-safe), %s = string literal, %L = literal\n  EXECUTE format(\n    'CREATE TABLE IF NOT EXISTS %I.%I (\n       id BIGSERIAL PRIMARY KEY,\n       amount NUMERIC(10,2) NOT NULL,\n       created_at TIMESTAMPTZ DEFAULT now()\n     ) PARTITION BY RANGE (created_at)',\n    schema_name, tbl\n  );\n  RAISE NOTICE 'Created table %.%', schema_name, tbl;\nEND $$;\n\n-- ⚠️ NEVER use string concatenation for dynamic SQL:\n--   EXECUTE 'SELECT * FROM ' || tbl;  -- SQL injection if tbl is user input\n-- ALWAYS use format('%I', tbl) for identifiers, format('%L', val) for literals\n",[34,3556,3557,3562,3567,3572,3577,3613,3629,3634,3639,3650,3655,3660,3665,3670,3677,3682,3687,3698,3706,3710,3715,3720],{"__ignoreMap":32},[37,3558,3559],{"class":39,"line":40},[37,3560,3561],{"class":43},"-- Dynamic SQL: build and execute SQL at runtime inside PL\u002FpgSQL\n",[37,3563,3564],{"class":39,"line":47},[37,3565,3566],{"class":43},"-- Use EXECUTE FORMAT for safe identifier injection (prevent SQL injection)\n",[37,3568,3569],{"class":39,"line":53},[37,3570,3571],{"class":81},"DO $$\n",[37,3573,3574],{"class":39,"line":59},[37,3575,3576],{"class":74},"DECLARE\n",[37,3578,3579,3582,3585,3588,3590,3593,3596,3599,3601,3604,3607,3610],{"class":39,"line":65},[37,3580,3581],{"class":81},"  tbl ",[37,3583,3584],{"class":74},"text",[37,3586,3587],{"class":81}," :",[37,3589,116],{"class":74},[37,3591,3592],{"class":263}," 'orders_'",[37,3594,3595],{"class":74}," ||",[37,3597,3598],{"class":81}," to_char(",[37,3600,2678],{"class":74},[37,3602,3603],{"class":81},"(), ",[37,3605,3606],{"class":263},"'YYYY_MM'",[37,3608,3609],{"class":81},");  ",[37,3611,3612],{"class":43},"-- dynamic table name\n",[37,3614,3615,3618,3620,3622,3624,3627],{"class":39,"line":71},[37,3616,3617],{"class":81},"  schema_name ",[37,3619,3584],{"class":74},[37,3621,3587],{"class":81},[37,3623,116],{"class":74},[37,3625,3626],{"class":263}," 'public'",[37,3628,1891],{"class":81},[37,3630,3631],{"class":39,"line":78},[37,3632,3633],{"class":74},"BEGIN\n",[37,3635,3636],{"class":39,"line":85},[37,3637,3638],{"class":43},"  -- %I = identifier (quoted, injection-safe), %s = string literal, %L = literal\n",[37,3640,3641,3644,3647],{"class":39,"line":131},[37,3642,3643],{"class":74},"  EXECUTE",[37,3645,3646],{"class":88}," format",[37,3648,3649],{"class":81},"(\n",[37,3651,3652],{"class":39,"line":164},[37,3653,3654],{"class":263},"    'CREATE TABLE IF NOT EXISTS %I.%I (\n",[37,3656,3657],{"class":39,"line":197},[37,3658,3659],{"class":263},"       id BIGSERIAL PRIMARY KEY,\n",[37,3661,3662],{"class":39,"line":230},[37,3663,3664],{"class":263},"       amount NUMERIC(10,2) NOT NULL,\n",[37,3666,3667],{"class":39,"line":243},[37,3668,3669],{"class":263},"       created_at TIMESTAMPTZ DEFAULT now()\n",[37,3671,3672,3675],{"class":39,"line":252},[37,3673,3674],{"class":263},"     ) PARTITION BY RANGE (created_at)'",[37,3676,799],{"class":81},[37,3678,3679],{"class":39,"line":278},[37,3680,3681],{"class":81},"    schema_name, tbl\n",[37,3683,3684],{"class":39,"line":287},[37,3685,3686],{"class":81},"  );\n",[37,3688,3689,3692,3695],{"class":39,"line":296},[37,3690,3691],{"class":81},"  RAISE NOTICE ",[37,3693,3694],{"class":263},"'Created table %.%'",[37,3696,3697],{"class":81},", schema_name, tbl;\n",[37,3699,3700,3703],{"class":39,"line":302},[37,3701,3702],{"class":74},"END",[37,3704,3705],{"class":81}," $$;\n",[37,3707,3708],{"class":39,"line":308},[37,3709,556],{"emptyLinePlaceholder":555},[37,3711,3712],{"class":39,"line":658},[37,3713,3714],{"class":43},"-- ⚠️ NEVER use string concatenation for dynamic SQL:\n",[37,3716,3717],{"class":39,"line":664},[37,3718,3719],{"class":43},"--   EXECUTE 'SELECT * FROM ' || tbl;  -- SQL injection if tbl is user input\n",[37,3721,3722],{"class":39,"line":892},[37,3723,3724],{"class":43},"-- ALWAYS use format('%I', tbl) for identifiers, format('%L', val) for literals\n",[14,3726,3728],{"id":3727},"anti-pattern-procedural-code-where-set-based-sql-works","Anti-Pattern — Procedural Code Where Set-Based SQL Works",[24,3730,3731],{"language":26},[28,3732,3734],{"className":30,"code":3733,"language":26,"meta":32,"style":32},"-- ❌ WRONG: Multiple correlated subqueries (re-executes per row)\nSELECT c.name,\n  (SELECT SUM(amount) FROM orders o WHERE o.customer_id = c.id) AS total_spend,\n  (SELECT COUNT(*)    FROM orders o WHERE o.customer_id = c.id) AS order_count,\n  (SELECT MAX(ordered_on) FROM orders o WHERE o.customer_id = c.id) AS last_order\nFROM customers c;\n-- 3 correlated subqueries → 3 index lookups per customer → O(N × 3 × log M)\n-- Each subquery is planned and executed independently\n\n-- ✅ RIGHT: Single scan with window\u002Faggregate functions\nSELECT c.name, COALESCE(o.total_spend, 0) AS total_spend,\n       COALESCE(o.order_count, 0) AS order_count, o.last_order\nFROM customers c\nLEFT JOIN LATERAL (\n  SELECT SUM(amount) AS total_spend,\n         COUNT(*)    AS order_count,\n         MAX(ordered_on) AS last_order\n  FROM orders o WHERE o.customer_id = c.id\n) o ON true;\n-- LATERAL: one pass over orders per customer, all aggregates computed together\n-- Or even simpler — a plain JOIN + GROUP BY:\nSELECT c.name, COALESCE(SUM(o.amount), 0) AS total_spend,\n       COUNT(o.id) AS order_count, MAX(o.ordered_on) AS last_order\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nGROUP BY c.id, c.name;\n",[34,3735,3736,3741,3755,3796,3839,3878,3885,3890,3895,3899,3904,3938,3970,3977,3984,3996,4011,4022,4045,4055,4060,4065,4103,4138,4144,4167],{"__ignoreMap":32},[37,3737,3738],{"class":39,"line":40},[37,3739,3740],{"class":43},"-- ❌ WRONG: Multiple correlated subqueries (re-executes per row)\n",[37,3742,3743,3745,3748,3750,3753],{"class":39,"line":47},[37,3744,561],{"class":74},[37,3746,3747],{"class":88}," c",[37,3749,1668],{"class":81},[37,3751,3752],{"class":88},"name",[37,3754,799],{"class":81},[37,3756,3757,3760,3762,3765,3767,3769,3772,3774,3776,3778,3780,3783,3785,3787,3789,3791,3793],{"class":39,"line":53},[37,3758,3759],{"class":81},"  (",[37,3761,561],{"class":74},[37,3763,3764],{"class":88}," SUM",[37,3766,92],{"class":81},[37,3768,246],{"class":74},[37,3770,3771],{"class":81}," orders o ",[37,3773,101],{"class":74},[37,3775,3275],{"class":88},[37,3777,1668],{"class":81},[37,3779,3349],{"class":88},[37,3781,3782],{"class":74}," =",[37,3784,3747],{"class":88},[37,3786,1668],{"class":81},[37,3788,3280],{"class":88},[37,3790,122],{"class":81},[37,3792,125],{"class":74},[37,3794,3795],{"class":81}," total_spend,\n",[37,3797,3798,3800,3802,3805,3807,3809,3812,3814,3816,3818,3820,3822,3824,3826,3828,3830,3832,3834,3836],{"class":39,"line":59},[37,3799,3759],{"class":81},[37,3801,561],{"class":74},[37,3803,3804],{"class":88}," COUNT",[37,3806,357],{"class":81},[37,3808,942],{"class":74},[37,3810,3811],{"class":81},")    ",[37,3813,246],{"class":74},[37,3815,3771],{"class":81},[37,3817,101],{"class":74},[37,3819,3275],{"class":88},[37,3821,1668],{"class":81},[37,3823,3349],{"class":88},[37,3825,3782],{"class":74},[37,3827,3747],{"class":88},[37,3829,1668],{"class":81},[37,3831,3280],{"class":88},[37,3833,122],{"class":81},[37,3835,125],{"class":74},[37,3837,3838],{"class":81}," order_count,\n",[37,3840,3841,3843,3845,3848,3851,3853,3855,3857,3859,3861,3863,3865,3867,3869,3871,3873,3875],{"class":39,"line":65},[37,3842,3759],{"class":81},[37,3844,561],{"class":74},[37,3846,3847],{"class":88}," MAX",[37,3849,3850],{"class":81},"(ordered_on) ",[37,3852,246],{"class":74},[37,3854,3771],{"class":81},[37,3856,101],{"class":74},[37,3858,3275],{"class":88},[37,3860,1668],{"class":81},[37,3862,3349],{"class":88},[37,3864,3782],{"class":74},[37,3866,3747],{"class":88},[37,3868,1668],{"class":81},[37,3870,3280],{"class":88},[37,3872,122],{"class":81},[37,3874,125],{"class":74},[37,3876,3877],{"class":81}," last_order\n",[37,3879,3880,3882],{"class":39,"line":71},[37,3881,246],{"class":74},[37,3883,3884],{"class":81}," customers c;\n",[37,3886,3887],{"class":39,"line":78},[37,3888,3889],{"class":43},"-- 3 correlated subqueries → 3 index lookups per customer → O(N × 3 × log M)\n",[37,3891,3892],{"class":39,"line":85},[37,3893,3894],{"class":43},"-- Each subquery is planned and executed independently\n",[37,3896,3897],{"class":39,"line":131},[37,3898,556],{"emptyLinePlaceholder":555},[37,3900,3901],{"class":39,"line":164},[37,3902,3903],{"class":43},"-- ✅ RIGHT: Single scan with window\u002Faggregate functions\n",[37,3905,3906,3908,3910,3912,3914,3916,3919,3921,3923,3925,3928,3930,3932,3934,3936],{"class":39,"line":197},[37,3907,561],{"class":74},[37,3909,3747],{"class":88},[37,3911,1668],{"class":81},[37,3913,3752],{"class":88},[37,3915,1247],{"class":81},[37,3917,3918],{"class":88},"COALESCE",[37,3920,357],{"class":81},[37,3922,3298],{"class":88},[37,3924,1668],{"class":81},[37,3926,3927],{"class":88},"total_spend",[37,3929,1247],{"class":81},[37,3931,1244],{"class":88},[37,3933,122],{"class":81},[37,3935,125],{"class":74},[37,3937,3795],{"class":81},[37,3939,3940,3943,3945,3947,3949,3952,3954,3956,3958,3960,3963,3965,3967],{"class":39,"line":230},[37,3941,3942],{"class":88},"       COALESCE",[37,3944,357],{"class":81},[37,3946,3298],{"class":88},[37,3948,1668],{"class":81},[37,3950,3951],{"class":88},"order_count",[37,3953,1247],{"class":81},[37,3955,1244],{"class":88},[37,3957,122],{"class":81},[37,3959,125],{"class":74},[37,3961,3962],{"class":81}," order_count, ",[37,3964,3298],{"class":88},[37,3966,1668],{"class":81},[37,3968,3969],{"class":88},"last_order\n",[37,3971,3972,3974],{"class":39,"line":243},[37,3973,246],{"class":74},[37,3975,3976],{"class":81}," customers c\n",[37,3978,3979,3982],{"class":39,"line":252},[37,3980,3981],{"class":74},"LEFT JOIN",[37,3983,3323],{"class":81},[37,3985,3986,3988,3990,3992,3994],{"class":39,"line":278},[37,3987,790],{"class":74},[37,3989,3764],{"class":88},[37,3991,92],{"class":81},[37,3993,125],{"class":74},[37,3995,3795],{"class":81},[37,3997,3998,4001,4003,4005,4007,4009],{"class":39,"line":287},[37,3999,4000],{"class":88},"         COUNT",[37,4002,357],{"class":81},[37,4004,942],{"class":74},[37,4006,3811],{"class":81},[37,4008,125],{"class":74},[37,4010,3838],{"class":81},[37,4012,4013,4016,4018,4020],{"class":39,"line":296},[37,4014,4015],{"class":88},"         MAX",[37,4017,3850],{"class":81},[37,4019,125],{"class":74},[37,4021,3877],{"class":81},[37,4023,4024,4026,4028,4030,4032,4034,4036,4038,4040,4042],{"class":39,"line":302},[37,4025,768],{"class":74},[37,4027,3771],{"class":81},[37,4029,101],{"class":74},[37,4031,3275],{"class":88},[37,4033,1668],{"class":81},[37,4035,3349],{"class":88},[37,4037,3782],{"class":74},[37,4039,3747],{"class":88},[37,4041,1668],{"class":81},[37,4043,4044],{"class":88},"id\n",[37,4046,4047,4050,4052],{"class":39,"line":308},[37,4048,4049],{"class":81},") o ",[37,4051,2001],{"class":74},[37,4053,4054],{"class":81}," true;\n",[37,4056,4057],{"class":39,"line":658},[37,4058,4059],{"class":43},"-- LATERAL: one pass over orders per customer, all aggregates computed together\n",[37,4061,4062],{"class":39,"line":664},[37,4063,4064],{"class":43},"-- Or even simpler — a plain JOIN + GROUP BY:\n",[37,4066,4067,4069,4071,4073,4075,4077,4079,4081,4084,4086,4088,4090,4092,4095,4097,4099,4101],{"class":39,"line":892},[37,4068,561],{"class":74},[37,4070,3747],{"class":88},[37,4072,1668],{"class":81},[37,4074,3752],{"class":88},[37,4076,1247],{"class":81},[37,4078,3918],{"class":88},[37,4080,357],{"class":81},[37,4082,4083],{"class":88},"SUM",[37,4085,357],{"class":81},[37,4087,3298],{"class":88},[37,4089,1668],{"class":81},[37,4091,1976],{"class":88},[37,4093,4094],{"class":81},"), ",[37,4096,1244],{"class":88},[37,4098,122],{"class":81},[37,4100,125],{"class":74},[37,4102,3795],{"class":81},[37,4104,4105,4108,4110,4112,4114,4116,4118,4120,4122,4124,4126,4128,4130,4132,4134,4136],{"class":39,"line":900},[37,4106,4107],{"class":88},"       COUNT",[37,4109,357],{"class":81},[37,4111,3298],{"class":88},[37,4113,1668],{"class":81},[37,4115,3280],{"class":88},[37,4117,122],{"class":81},[37,4119,125],{"class":74},[37,4121,3962],{"class":81},[37,4123,1111],{"class":88},[37,4125,357],{"class":81},[37,4127,3298],{"class":88},[37,4129,1668],{"class":81},[37,4131,1944],{"class":88},[37,4133,122],{"class":81},[37,4135,125],{"class":74},[37,4137,3877],{"class":81},[37,4139,4140,4142],{"class":39,"line":917},[37,4141,246],{"class":74},[37,4143,3976],{"class":81},[37,4145,4146,4148,4150,4152,4154,4156,4158,4160,4162,4164],{"class":39,"line":934},[37,4147,3981],{"class":74},[37,4149,3771],{"class":81},[37,4151,2001],{"class":74},[37,4153,3747],{"class":88},[37,4155,1668],{"class":81},[37,4157,3280],{"class":88},[37,4159,3782],{"class":74},[37,4161,3275],{"class":88},[37,4163,1668],{"class":81},[37,4165,4166],{"class":88},"customer_id\n",[37,4168,4169,4171,4173,4175,4177,4179,4182,4184,4186],{"class":39,"line":952},[37,4170,281],{"class":74},[37,4172,3747],{"class":88},[37,4174,1668],{"class":81},[37,4176,3280],{"class":88},[37,4178,1247],{"class":81},[37,4180,4181],{"class":88},"c",[37,4183,1668],{"class":81},[37,4185,3752],{"class":88},[37,4187,1891],{"class":81},[14,4189,4191],{"id":4190},"tips-tricks","💡 Tips & Tricks",[4193,4194,4195,4222,4243,4286,4298,4311,4323],"ul",{},[4196,4197,4198,4202,4203,4206,4207,4209,4210,4212,4213,4216,4217,4219,4220,1668],"li",{},[4199,4200,4201],"strong",{},"Idiom"," — use ",[34,4204,4205],{},"SUM(amount) FILTER (WHERE cond)"," for conditional aggregation. It's SQL standard, one pass over the data, and correctly excludes non-matching rows from ",[34,4208,1349],{},"\u002F",[34,4211,1121],{}," (unlike ",[34,4214,4215],{},"CASE ... ELSE 0"," which includes 0s in the denominator). ",[34,4218,360],{}," is the portable fallback for engines without ",[34,4221,95],{},[4196,4223,4224,4226,4227,4230,4231,4234,4235,4238,4239,4242],{},[4199,4225,4201],{}," — the gaps-and-islands row-number trick: ",[34,4228,4229],{},"value - ROW_NUMBER() OVER (ORDER BY value)"," is constant within a run and jumps at gaps. Works for dates, integers, any ordered sequence. Always ",[34,4232,4233],{},"DISTINCT"," first if duplicates exist — same value with different ",[34,4236,4237],{},"rn"," produces different ",[34,4240,4241],{},"grp",", splitting the run.",[4196,4244,4245,4247,4248,4251,4252,4255,4256,4259,4260,4263,4264,4267,4268,4259,4271,4274,4275,4278,4279,4282,4283,1668],{},[4199,4246,4201],{}," — ",[34,4249,4250],{},"width_bucket(v, min, max, n)"," for histograms: ",[34,4253,4254],{},"v=min"," → bucket 1, ",[34,4257,4258],{},"v=max"," → bucket ",[34,4261,4262],{},"n"," (max goes in last, not overflow), ",[34,4265,4266],{},"v \u003C min"," → bucket 0, ",[34,4269,4270],{},"v > max",[34,4272,4273],{},"n+1",". Half-open intervals ",[34,4276,4277],{},"[low, high)"," except the last bucket. Reconstruct boundaries as ",[34,4280,4281],{},"(bucket-1) × step"," and ",[34,4284,4285],{},"bucket × step",[4196,4287,4288,4247,4290,4293,4294,4297],{},[4199,4289,4201],{},[34,4291,4292],{},"PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)"," for medians. Interpolates for even-count groups (average of two middle values). Use ",[34,4295,4296],{},"PERCENTILE_DISC"," for an actual value (no interpolation). Can't be a window function — use self-join (O(N²)) or t-digest (approximate, O(N log N)) for running medians.",[4196,4299,4300,4302,4303,4306,4307,4310],{},[4199,4301,4201],{}," — sessionization pattern: ",[34,4304,4305],{},"LAG"," to compute gap → ",[34,4308,4309],{},"SUM(CASE WHEN gap > threshold THEN 1 ELSE 0 END) OVER (...)"," as a running session counter. Each gap > threshold increments the counter, creating a new session ID. This is the standard way to group events into sessions in pure SQL.",[4196,4312,4313,4247,4315,4318,4319,4322],{},[4199,4314,4201],{},[34,4316,4317],{},"LATERAL"," for correlated subqueries: ",[34,4320,4321],{},"LEFT JOIN LATERAL (SELECT ... WHERE child.col = parent.col) ON true",". The subquery can reference parent columns (like a correlated subquery) but returns a join row (not a scalar). Useful for per-row aggregation or top-N-per-group without a window function.",[4196,4324,4325,4247,4327,4330,4331,4334,4335,4337,4338,1668],{},[4199,4326,4201],{},[34,4328,4329],{},"array_agg(col ORDER BY sort_col)"," to build ordered lists in a single aggregate. ",[34,4332,4333],{},"array_agg"," doesn't guarantee order without ",[34,4336,290],{}," inside the aggregate — always specify it. Useful for denormalizing child rows into a single column: ",[34,4339,4340],{},"array_agg(product_name ORDER BY item_id)",[14,4342,4344],{"id":4343},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[4193,4346,4347,4371,4396,4423,4443,4461,4478,4498],{},[4196,4348,4349,4357,4358,4360,4361,4363,4364,4366,4367,4209,4369,1668],{},[4199,4350,4351,4353,4354,4356],{},[34,4352,95],{}," vs ",[34,4355,360],{}," performance",": ",[34,4359,95],{}," can be slightly faster because the engine skips non-matching rows entirely (no evaluation of the ELSE branch). In PostgreSQL, the difference is usually negligible — the planner optimizes ",[34,4362,360],{}," well. Choose ",[34,4365,95],{}," for readability and correctness with ",[34,4368,1349],{},[34,4370,1121],{},[4196,4372,4373,4376,4377,4379,4380,4383,4384,4387,4388,4391,4392,4395],{},[4199,4374,4375],{},"Gaps-and-islands with NULL dates",": if ",[34,4378,796],{}," can be NULL, ",[34,4381,4382],{},"day - ROW_NUMBER()"," produces NULL, and ",[34,4385,4386],{},"GROUP BY NULL"," lumps all NULL-day rows into one group. Filter NULLs first: ",[34,4389,4390],{},"WHERE day IS NOT NULL"," in the ",[34,4393,4394],{},"day_marks"," CTE.",[4196,4397,4398,4404,4405,4408,4409,4412,4413,4415,4416,4419,4420,4422],{},[4199,4399,4400,4403],{},[34,4401,4402],{},"width_bucket"," boundary off-by-one",": a value exactly at a bucket boundary (e.g., 50.00 with buckets ",[37,4406,4407],{},"0,500"," in 10) goes into the ",[4199,4410,4411],{},"higher"," bucket (bucket 2, not bucket 1). Intervals are ",[34,4414,4277],{}," half-open. Only the last bucket is ",[34,4417,4418],{},"[max-step, max]"," (closed on both ends). If you need different boundary behavior, use manual ",[34,4421,360],{}," buckets.",[4196,4424,4425,4431,4432,4435,4436,4438,4439,4442],{},[4199,4426,4427,4430],{},[34,4428,4429],{},"PERCENTILE_CONT"," interpolation",": for even N, the median is the linear interpolation of the two middle values. For ",[34,4433,4434],{},"[10, 20, 30, 40]",", p=0.5 → 25.0 (average of 20 and 30). This may surprise users expecting an \"actual\" data point. Use ",[34,4437,4296],{}," for a non-interpolated value (returns 20 for the same input with p=0.5, since it picks the value at position ",[34,4440,4441],{},"ceil(0.5 × N)",").",[4196,4444,4445,4448,4449,4452,4453,4456,4457,4460],{},[4199,4446,4447],{},"Session timeout edge cases",": if the first event has ",[34,4450,4451],{},"prev_event_at = NULL",", the gap is NULL. ",[34,4454,4455],{},"gap_seconds > 1800"," evaluates to NULL (not TRUE), so the first event wouldn't start a new session without the ",[34,4458,4459],{},"OR gap_seconds IS NULL"," guard. Always handle the first-row case explicitly.",[4196,4462,4463,4469,4470,4473,4474,4477],{},[4199,4464,4465,4468],{},[34,4466,4467],{},"crosstab"," output schema declaration",": the output column types and names must be declared statically — ",[34,4471,4472],{},"AS ct (customer_id BIGINT, jan NUMERIC, ...)",". If the source query returns fewer categories than declared columns, extra columns are NULL. If it returns more, they're silently dropped. Use the 2-parameter ",[34,4475,4476],{},"crosstab(source_sql, category_sql)"," form to pin category ordering and avoid value-shifting.",[4196,4479,4480,4357,4485,4488,4489,4491,4492,4495,4496,1668],{},[4199,4481,4482,4484],{},[34,4483,4333],{}," ordering",[34,4486,4487],{},"array_agg(x)"," does NOT guarantee element order without ",[34,4490,290],{}," inside the aggregate: ",[34,4493,4494],{},"array_agg(x ORDER BY y)",". The planner may happen to produce sorted output due to an index scan, but this is not guaranteed and can change between plans. Always specify ",[34,4497,290],{},[4196,4499,4500,4503],{},[4199,4501,4502],{},"Dynamic columns can't be pivoted in pure SQL",": SQL requires all output columns to be known at parse time. To pivot an arbitrary number of categories, either hardcode them (if known), generate SQL dynamically in application code, or return rows (not columns) and pivot in the client.",[14,4505,4507],{"id":4506},"spot-the-bug","🧠 Spot the Bug",[3285,4509,4510],{},"This gaps-and-islands query is supposed to find consecutive-day streaks per user, but it produces wrong groupings — some users have sessions split across multiple groups despite having consecutive days. Why?",[24,4512,4513],{"language":26},[28,4514,4516],{"className":30,"code":4515,"language":26,"meta":32,"style":32},"WITH numbered AS (\n  SELECT user_id, activity_date,\n    activity_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY activity_date) AS grp\n  FROM user_activity\n)\nSELECT user_id, grp, MIN(activity_date) AS start_date, MAX(activity_date) AS end_date\nFROM numbered\nGROUP BY user_id, grp\nORDER BY user_id, start_date;\n",[34,4517,4518,4529,4536,4566,4573,4577,4606,4612,4618],{"__ignoreMap":32},[37,4519,4520,4522,4525,4527],{"class":39,"line":40},[37,4521,722],{"class":74},[37,4523,4524],{"class":81}," numbered ",[37,4526,125],{"class":74},[37,4528,730],{"class":81},[37,4530,4531,4533],{"class":39,"line":47},[37,4532,790],{"class":74},[37,4534,4535],{"class":81}," user_id, activity_date,\n",[37,4537,4538,4541,4543,4545,4547,4549,4551,4553,4555,4557,4559,4562,4564],{"class":39,"line":53},[37,4539,4540],{"class":81},"    activity_date ",[37,4542,1015],{"class":74},[37,4544,854],{"class":88},[37,4546,807],{"class":81},[37,4548,810],{"class":74},[37,4550,98],{"class":81},[37,4552,815],{"class":74},[37,4554,818],{"class":74},[37,4556,821],{"class":81},[37,4558,290],{"class":74},[37,4560,4561],{"class":81}," activity_date) ",[37,4563,125],{"class":74},[37,4565,877],{"class":81},[37,4567,4568,4570],{"class":39,"line":59},[37,4569,768],{"class":74},[37,4571,4572],{"class":81}," user_activity\n",[37,4574,4575],{"class":39,"line":65},[37,4576,889],{"class":81},[37,4578,4579,4581,4584,4587,4590,4592,4595,4597,4599,4601,4603],{"class":39,"line":71},[37,4580,561],{"class":74},[37,4582,4583],{"class":81}," user_id, grp, ",[37,4585,4586],{"class":88},"MIN",[37,4588,4589],{"class":81},"(activity_date) ",[37,4591,125],{"class":74},[37,4593,4594],{"class":74}," start_date",[37,4596,1247],{"class":81},[37,4598,1111],{"class":88},[37,4600,4589],{"class":81},[37,4602,125],{"class":74},[37,4604,4605],{"class":81}," end_date\n",[37,4607,4608,4610],{"class":39,"line":78},[37,4609,246],{"class":74},[37,4611,957],{"class":81},[37,4613,4614,4616],{"class":39,"line":85},[37,4615,281],{"class":74},[37,4617,965],{"class":81},[37,4619,4620,4622,4624,4627],{"class":39,"line":131},[37,4621,290],{"class":74},[37,4623,793],{"class":81},[37,4625,4626],{"class":74},"start_date",[37,4628,1891],{"class":81},[4630,4631,4632,4636,4658,4698,4708,4717,4868,4890],"details",{},[4633,4634,4635],"summary",{},"Answer",[3285,4637,4638,4639,4641,4642,4645,4646,4649,4650,4653,4654,4657],{},"The query doesn't ",[34,4640,4233],{}," the ",[34,4643,4644],{},"(user_id, activity_date)"," pairs first. If a user has ",[4199,4647,4648],{},"multiple activity records on the same day",", each gets a different ",[34,4651,4652],{},"ROW_NUMBER()"," but the same ",[34,4655,4656],{},"activity_date",". This means:",[4193,4659,4660,4672,4687],{},[4196,4661,4662,4663,1247,4666,1247,4669],{},"Day Jan 1, row 1: ",[34,4664,4665],{},"activity_date = Jan 1",[34,4667,4668],{},"rn = 1",[34,4670,4671],{},"grp = Dec 31",[4196,4673,4674,4675,1247,4677,1247,4680,4683,4684],{},"Day Jan 1, row 2: ",[34,4676,4665],{},[34,4678,4679],{},"rn = 2",[34,4681,4682],{},"grp = Dec 30"," ← ",[4199,4685,4686],{},"different grp!",[4196,4688,4689,4690,1247,4693,1247,4696],{},"Day Jan 2, row 3: ",[34,4691,4692],{},"activity_date = Jan 2",[34,4694,4695],{},"rn = 3",[34,4697,4682],{},[3285,4699,4700,4701,4703,4704,4707],{},"The duplicate day splits into two groups, and the next day may join the wrong group. The ",[34,4702,4241],{}," values are inconsistent because ",[34,4705,4706],{},"ROW_NUMBER"," increments per row, not per day.",[3285,4709,4710,4711,4713,4714,4716],{},"Fix: ",[34,4712,4233],{}," the days first, so each day appears once and ",[34,4715,4706],{}," increments per unique day:",[24,4718,4719],{"language":26},[28,4720,4722],{"className":30,"code":4721,"language":26,"meta":32,"style":32},"WITH day_marks AS (\n  SELECT DISTINCT user_id, activity_date  -- ← deduplicate first\n  FROM user_activity\n),\nnumbered AS (\n  SELECT user_id, activity_date,\n    activity_date - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY activity_date) AS grp\n  FROM day_marks\n)\nSELECT user_id, grp, MIN(activity_date) AS start_date, MAX(activity_date) AS end_date,\n       COUNT(*) AS streak_days\nFROM numbered\nGROUP BY user_id, grp\nORDER BY user_id, start_date;\n",[34,4723,4724,4734,4744,4750,4754,4762,4768,4796,4802,4806,4831,4846,4852,4858],{"__ignoreMap":32},[37,4725,4726,4728,4730,4732],{"class":39,"line":40},[37,4727,722],{"class":74},[37,4729,725],{"class":81},[37,4731,125],{"class":74},[37,4733,730],{"class":81},[37,4735,4736,4738,4741],{"class":39,"line":47},[37,4737,745],{"class":74},[37,4739,4740],{"class":81}," user_id, activity_date  ",[37,4742,4743],{"class":43},"-- ← deduplicate first\n",[37,4745,4746,4748],{"class":39,"line":53},[37,4747,768],{"class":74},[37,4749,4572],{"class":81},[37,4751,4752],{"class":39,"line":59},[37,4753,776],{"class":81},[37,4755,4756,4758,4760],{"class":39,"line":65},[37,4757,781],{"class":81},[37,4759,125],{"class":74},[37,4761,730],{"class":81},[37,4763,4764,4766],{"class":39,"line":71},[37,4765,790],{"class":74},[37,4767,4535],{"class":81},[37,4769,4770,4772,4774,4776,4778,4780,4782,4784,4786,4788,4790,4792,4794],{"class":39,"line":78},[37,4771,4540],{"class":81},[37,4773,1015],{"class":74},[37,4775,854],{"class":88},[37,4777,807],{"class":81},[37,4779,810],{"class":74},[37,4781,98],{"class":81},[37,4783,815],{"class":74},[37,4785,818],{"class":74},[37,4787,821],{"class":81},[37,4789,290],{"class":74},[37,4791,4561],{"class":81},[37,4793,125],{"class":74},[37,4795,877],{"class":81},[37,4797,4798,4800],{"class":39,"line":85},[37,4799,768],{"class":74},[37,4801,884],{"class":81},[37,4803,4804],{"class":39,"line":131},[37,4805,889],{"class":81},[37,4807,4808,4810,4812,4814,4816,4818,4820,4822,4824,4826,4828],{"class":39,"line":164},[37,4809,561],{"class":74},[37,4811,4583],{"class":81},[37,4813,4586],{"class":88},[37,4815,4589],{"class":81},[37,4817,125],{"class":74},[37,4819,4594],{"class":74},[37,4821,1247],{"class":81},[37,4823,1111],{"class":88},[37,4825,4589],{"class":81},[37,4827,125],{"class":74},[37,4829,4830],{"class":81}," end_date,\n",[37,4832,4833,4835,4837,4839,4841,4843],{"class":39,"line":197},[37,4834,4107],{"class":88},[37,4836,357],{"class":81},[37,4838,942],{"class":74},[37,4840,122],{"class":81},[37,4842,125],{"class":74},[37,4844,4845],{"class":81}," streak_days\n",[37,4847,4848,4850],{"class":39,"line":230},[37,4849,246],{"class":74},[37,4851,957],{"class":81},[37,4853,4854,4856],{"class":39,"line":243},[37,4855,281],{"class":74},[37,4857,965],{"class":81},[37,4859,4860,4862,4864,4866],{"class":39,"line":252},[37,4861,290],{"class":74},[37,4863,793],{"class":81},[37,4865,4626],{"class":74},[37,4867,1891],{"class":81},[3285,4869,4870,4871,4874,4875,4247,4877,4879,4880,4882,4883,4886,4887,4889],{},"Alternatively, use ",[34,4872,4873],{},"DENSE_RANK"," instead of ",[34,4876,4706],{},[34,4878,4873],{}," gives the same rank to identical values, so duplicates on the same day get the same ",[34,4881,4237],{},", and ",[34,4884,4885],{},"activity_date - DENSE_RANK()"," stays consistent. But ",[34,4888,4233],{}," is cleaner and more efficient (fewer rows to process).",[3285,4891,4892,4895,4896,4899,4900,4902,4903,4906,4907,4909,4910,1668],{},[4199,4893,4894],{},"The lesson",": the row-number-difference trick requires ",[4199,4897,4898],{},"unique, consecutive"," values in the ",[34,4901,290],{}," column. Duplicates break the invariant that ",[34,4904,4905],{},"value - rn"," is constant within a run. Always ",[34,4908,4233],{}," first or use ",[34,4911,4873],{},[4913,4914,4915],"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 .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}",{"title":32,"searchDepth":47,"depth":47,"links":4917},[4918,4924,4928,4932,4936,4941,4942,4943,4944,4945,4946,4947,4948],{"id":16,"depth":47,"text":17,"children":4919},[4920,4921,4922],{"id":21,"depth":53,"text":22},{"id":314,"depth":53,"text":315},{"id":498,"depth":53,"text":4923},"crosstab Function (PostgreSQL tablefunc)",{"id":670,"depth":47,"text":671,"children":4925},[4926,4927],{"id":674,"depth":53,"text":675},{"id":976,"depth":53,"text":977},{"id":1180,"depth":47,"text":1181,"children":4929},[4930,4931],{"id":1184,"depth":53,"text":1185},{"id":1413,"depth":53,"text":1414},{"id":1611,"depth":47,"text":1612,"children":4933},[4934,4935],{"id":1615,"depth":53,"text":1616},{"id":1894,"depth":53,"text":1895},{"id":2055,"depth":47,"text":2056,"children":4937},[4938,4939,4940],{"id":2059,"depth":53,"text":2060},{"id":2399,"depth":53,"text":2400},{"id":2550,"depth":53,"text":2551},{"id":2707,"depth":47,"text":2708},{"id":3122,"depth":47,"text":3123},{"id":3378,"depth":47,"text":3379},{"id":3548,"depth":47,"text":3549},{"id":3727,"depth":47,"text":3728},{"id":4190,"depth":47,"text":4191},{"id":4343,"depth":47,"text":4344},{"id":4506,"depth":47,"text":4507},"md",{},"\u002Fsql\u002F27-advanced-patterns",{"title":5,"description":32},"sql\u002F27-advanced-patterns","z0nFkS19EG0vOgD3spO9VBJfu5-cjMH9SEghygEIIgI",1789924655063]