[{"data":1,"prerenderedAt":3891},["ShallowReactive",2],{"post:\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics\u002F":3},{"post":4,"newer":3841,"older":3851,"related":3861,"series":3889},{"id":5,"title":6,"body":7,"canonical":3823,"categories":3824,"date":3826,"description":3827,"extension":3828,"featured":3829,"hero":3830,"image":3830,"meta":3831,"navigation":725,"path":3832,"readingTime":212,"seo":3833,"series":3830,"seriesOrder":3830,"sites":3834,"source":3830,"stem":3835,"tags":3836,"updated":3830,"url":3839,"__hash__":3840},"blog\u002Fblog\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics.md","Data Warehouse: Reusable SQL Models for IT Operations Metrics",{"type":8,"value":9,"toc":3812},"minimark",[10,19,39,44,47,62,66,69,72,228,236,460,463,763,766,994,1000,1465,1468,1471,1714,1721,2028,2032,2044,2423,2426,2430,2444,2729,2749,2753,2759,2762,2769,2772,2931,2934,3314,3317,3322,3333,3559,3711,3736,3740,3743,3747,3808],[11,12,13,14,18],"p",{},"The first version of most IT operations reporting is a folder of ",[15,16,17],"code",{},".sql"," files, each written for a specific dashboard or a question someone asked once and needed answered by Friday. That works right up until two dashboards report different numbers for the same metric. Mean time to resolution, say, because one query counted a reopened ticket once and the other counted it twice, and nobody remembers which is \"correct\" because both were written under deadline pressure by different people six months apart. Reusable models exist to prevent exactly that drift, and they're worth building well before the pile of one-off queries gets large enough to hurt.",[11,20,21,22,25,26,25,29,32,33,38],{},"Below is the model set I'd start with for a service desk: MTTR, ticket aging, SLA compliance, and patch compliance. It's plain PostgreSQL views, a function, a deploy script, and SQL tests, followed by the same thing in dbt for teams that want it. It builds on the ",[15,23,24],{},"raw"," \u002F ",[15,27,28],{},"staging",[15,30,31],{},"marts"," layout from ",[34,35,37],"a",{"href":36},"\u002F2026\u002F05\u002F20\u002Fdata-warehouse-building-a-reporting-layer-without-a-bi-team\u002F","building a reporting layer without a BI team",". Source field names are placeholders for whatever your PSA and RMM actually return.",[40,41,43],"h2",{"id":42},"a-metric-should-have-exactly-one-definition","A metric should have exactly one definition",[11,45,46],{},"The core idea is unglamorous: every metric that gets reported more than once gets one model that defines it, and every dashboard or downstream query references that model instead of recalculating the metric from raw tables. Mean time to resolution gets defined once, in a single view: what counts as \"resolved,\" how reopens are handled, which ticket types are excluded. Every report pulls from it. When someone (rightly) asks to change the definition, there's one place to change it, and every consumer updates together instead of drifting apart one query at a time.",[11,48,49,50,53,54,57,58,61],{},"Write the definition into the database itself, not only a wiki page. ",[15,51,52],{},"COMMENT ON VIEW"," shows up in ",[15,55,56],{},"psql","'s ",[15,59,60],{},"\\dv+"," listing and in most SQL clients, right where someone is about to use the view.",[40,63,65],{"id":64},"build-in-layers-not-one-wide-query","Build in layers, not one wide query",[11,67,68],{},"The models that have held up best in practice follow a staging-then-mart pattern, even at small scale. A staging layer cleans and normalizes each source (deduplicating records, standardizing status values, converting types) without imposing business logic yet. A mart layer built on clean staging applies the actual metric definitions. Skip the staging layer and write metric logic directly against a raw ticketing export, and every model reimplements the same cleanup. A source schema change then breaks every downstream model separately instead of in one traceable place.",[11,70,71],{},"Here's the chain for resolution time. Staging exposes the ticket and its status history as typed columns:",[73,74,79],"pre",{"className":75,"code":76,"language":77,"meta":78,"style":78},"language-sql shiki shiki-themes github-dark","-- models\u002F010_stg_ticket_status_changes.sql\nCREATE OR REPLACE VIEW staging.stg_ticket_status_changes AS\nSELECT payload ->> 'ticketId'                 AS ticket_id,\n       (payload ->> 'changedAt')::timestamptz AS changed_at,\n       lower(trim(payload ->> 'fromStatus'))  AS from_status,\n       lower(trim(payload ->> 'toStatus'))    AS to_status\nFROM raw.psa_ticket_status_changes;\n","sql","",[15,80,81,90,111,133,156,186,210],{"__ignoreMap":78},[82,83,86],"span",{"class":84,"line":85},"line",1,[82,87,89],{"class":88},"sAwPA","-- models\u002F010_stg_ticket_status_changes.sql\n",[82,91,93,97,100,104,108],{"class":84,"line":92},2,[82,94,96],{"class":95},"snl16","CREATE OR REPLACE",[82,98,99],{"class":95}," VIEW",[82,101,103],{"class":102},"svObZ"," staging",[82,105,107],{"class":106},"s95oV",".stg_ticket_status_changes ",[82,109,110],{"class":95},"AS\n",[82,112,114,117,120,123,127,130],{"class":84,"line":113},3,[82,115,116],{"class":95},"SELECT",[82,118,119],{"class":106}," payload ",[82,121,122],{"class":95},"->>",[82,124,126],{"class":125},"sU2Wk"," 'ticketId'",[82,128,129],{"class":95},"                 AS",[82,131,132],{"class":106}," ticket_id,\n",[82,134,136,139,141,144,147,150,153],{"class":84,"line":135},4,[82,137,138],{"class":106},"       (payload ",[82,140,122],{"class":95},[82,142,143],{"class":125}," 'changedAt'",[82,145,146],{"class":106},")::",[82,148,149],{"class":95},"timestamptz",[82,151,152],{"class":95}," AS",[82,154,155],{"class":106}," changed_at,\n",[82,157,159,163,166,169,172,174,177,180,183],{"class":84,"line":158},5,[82,160,162],{"class":161},"sDLfK","       lower",[82,164,165],{"class":106},"(",[82,167,168],{"class":161},"trim",[82,170,171],{"class":106},"(payload ",[82,173,122],{"class":95},[82,175,176],{"class":125}," 'fromStatus'",[82,178,179],{"class":106},"))  ",[82,181,182],{"class":95},"AS",[82,184,185],{"class":106}," from_status,\n",[82,187,189,191,193,195,197,199,202,205,207],{"class":84,"line":188},6,[82,190,162],{"class":161},[82,192,165],{"class":106},[82,194,168],{"class":161},[82,196,171],{"class":106},[82,198,122],{"class":95},[82,200,201],{"class":125}," 'toStatus'",[82,203,204],{"class":106},"))    ",[82,206,182],{"class":95},[82,208,209],{"class":106}," to_status\n",[82,211,213,216,219,222,225],{"class":84,"line":212},7,[82,214,215],{"class":95},"FROM",[82,217,218],{"class":161}," raw",[82,220,221],{"class":106},".",[82,223,224],{"class":161},"psa_ticket_status_changes",[82,226,227],{"class":106},";\n",[11,229,230,231,235],{},"An intermediate model does the one hard part, reopens, exactly once. A reopen is any transition ",[232,233,234],"em",{},"out of"," a resolved state:",[73,237,239],{"className":75,"code":238,"language":77,"meta":78,"style":78},"-- models\u002F020_int_ticket_lifecycle.sql\nCREATE OR REPLACE VIEW staging.int_ticket_lifecycle AS\nWITH changes AS (\n    SELECT ticket_id,\n           changed_at,\n           to_status   IN ('resolved', 'closed') AS enters_resolved,\n           from_status IN ('resolved', 'closed') AS leaves_resolved\n    FROM staging.stg_ticket_status_changes\n)\nSELECT ticket_id,\n       min(changed_at) FILTER (WHERE enters_resolved)                     AS first_resolved_at,\n       max(changed_at) FILTER (WHERE enters_resolved)                     AS last_resolved_at,\n       count(*)        FILTER (WHERE leaves_resolved AND NOT enters_resolved) AS reopen_count\nFROM changes\nGROUP BY ticket_id;\n",[15,240,241,246,259,272,279,284,312,334,347,353,360,385,406,443,451],{"__ignoreMap":78},[82,242,243],{"class":84,"line":85},[82,244,245],{"class":88},"-- models\u002F020_int_ticket_lifecycle.sql\n",[82,247,248,250,252,254,257],{"class":84,"line":92},[82,249,96],{"class":95},[82,251,99],{"class":95},[82,253,103],{"class":102},[82,255,256],{"class":106},".int_ticket_lifecycle ",[82,258,110],{"class":95},[82,260,261,264,267,269],{"class":84,"line":113},[82,262,263],{"class":95},"WITH",[82,265,266],{"class":95}," changes",[82,268,152],{"class":95},[82,270,271],{"class":106}," (\n",[82,273,274,277],{"class":84,"line":135},[82,275,276],{"class":95},"    SELECT",[82,278,132],{"class":106},[82,280,281],{"class":84,"line":158},[82,282,283],{"class":106},"           changed_at,\n",[82,285,286,289,292,295,298,301,304,307,309],{"class":84,"line":188},[82,287,288],{"class":106},"           to_status   ",[82,290,291],{"class":95},"IN",[82,293,294],{"class":106}," (",[82,296,297],{"class":125},"'resolved'",[82,299,300],{"class":106},", ",[82,302,303],{"class":125},"'closed'",[82,305,306],{"class":106},") ",[82,308,182],{"class":95},[82,310,311],{"class":106}," enters_resolved,\n",[82,313,314,317,319,321,323,325,327,329,331],{"class":84,"line":212},[82,315,316],{"class":106},"           from_status ",[82,318,291],{"class":95},[82,320,294],{"class":106},[82,322,297],{"class":125},[82,324,300],{"class":106},[82,326,303],{"class":125},[82,328,306],{"class":106},[82,330,182],{"class":95},[82,332,333],{"class":106}," leaves_resolved\n",[82,335,337,340,342,344],{"class":84,"line":336},8,[82,338,339],{"class":95},"    FROM",[82,341,103],{"class":161},[82,343,221],{"class":106},[82,345,346],{"class":161},"stg_ticket_status_changes\n",[82,348,350],{"class":84,"line":349},9,[82,351,352],{"class":106},")\n",[82,354,356,358],{"class":84,"line":355},10,[82,357,116],{"class":95},[82,359,132],{"class":106},[82,361,363,366,369,372,374,377,380,382],{"class":84,"line":362},11,[82,364,365],{"class":161},"       min",[82,367,368],{"class":106},"(changed_at) ",[82,370,371],{"class":95},"FILTER",[82,373,294],{"class":106},[82,375,376],{"class":95},"WHERE",[82,378,379],{"class":106}," enters_resolved)                     ",[82,381,182],{"class":95},[82,383,384],{"class":106}," first_resolved_at,\n",[82,386,388,391,393,395,397,399,401,403],{"class":84,"line":387},12,[82,389,390],{"class":161},"       max",[82,392,368],{"class":106},[82,394,371],{"class":95},[82,396,294],{"class":106},[82,398,376],{"class":95},[82,400,379],{"class":106},[82,402,182],{"class":95},[82,404,405],{"class":106}," last_resolved_at,\n",[82,407,409,412,414,417,420,422,424,426,429,432,435,438,440],{"class":84,"line":408},13,[82,410,411],{"class":161},"       count",[82,413,165],{"class":106},[82,415,416],{"class":95},"*",[82,418,419],{"class":106},")        ",[82,421,371],{"class":95},[82,423,294],{"class":106},[82,425,376],{"class":95},[82,427,428],{"class":106}," leaves_resolved ",[82,430,431],{"class":95},"AND",[82,433,434],{"class":95}," NOT",[82,436,437],{"class":106}," enters_resolved) ",[82,439,182],{"class":95},[82,441,442],{"class":106}," reopen_count\n",[82,444,446,448],{"class":84,"line":445},14,[82,447,215],{"class":95},[82,449,450],{"class":95}," changes\n",[82,452,454,457],{"class":84,"line":453},15,[82,455,456],{"class":95},"GROUP BY",[82,458,459],{"class":106}," ticket_id;\n",[11,461,462],{},"The fact model applies the business definition and documents it:",[73,464,466],{"className":75,"code":465,"language":77,"meta":78,"style":78},"-- models\u002F030_fct_ticket_resolution.sql\nCREATE OR REPLACE VIEW marts.fct_ticket_resolution AS\nSELECT t.ticket_id,\n       t.client_id,\n       t.priority,\n       t.queue,\n       t.created_at,\n       l.first_resolved_at,\n       l.last_resolved_at,\n       l.reopen_count,\n       l.last_resolved_at - t.created_at   AS time_to_resolve,\n       t.first_response_at - t.created_at  AS time_to_first_response\nFROM staging.stg_tickets t\nJOIN staging.int_ticket_lifecycle l USING (ticket_id)\nWHERE t.status IN ('resolved', 'closed')\n  AND t.queue NOT IN ('spam', 'merged', 'internal-project');\n\nCOMMENT ON VIEW marts.fct_ticket_resolution IS\n  'One row per resolved ticket. Resolution time runs from creation to the FINAL resolution, '\n  'so a reopened ticket counts once with its full elapsed time. Excludes spam, merged, and '\n  'internal project queues. Wall-clock time, not business hours. Owner: service desk lead.';\n",[15,467,468,473,487,502,514,525,536,547,559,570,581,604,627,641,662,686,720,727,743,749,755],{"__ignoreMap":78},[82,469,470],{"class":84,"line":85},[82,471,472],{"class":88},"-- models\u002F030_fct_ticket_resolution.sql\n",[82,474,475,477,479,482,485],{"class":84,"line":92},[82,476,96],{"class":95},[82,478,99],{"class":95},[82,480,481],{"class":102}," marts",[82,483,484],{"class":106},".fct_ticket_resolution ",[82,486,110],{"class":95},[82,488,489,491,494,496,499],{"class":84,"line":113},[82,490,116],{"class":95},[82,492,493],{"class":161}," t",[82,495,221],{"class":106},[82,497,498],{"class":161},"ticket_id",[82,500,501],{"class":106},",\n",[82,503,504,507,509,512],{"class":84,"line":135},[82,505,506],{"class":161},"       t",[82,508,221],{"class":106},[82,510,511],{"class":161},"client_id",[82,513,501],{"class":106},[82,515,516,518,520,523],{"class":84,"line":158},[82,517,506],{"class":161},[82,519,221],{"class":106},[82,521,522],{"class":161},"priority",[82,524,501],{"class":106},[82,526,527,529,531,534],{"class":84,"line":188},[82,528,506],{"class":161},[82,530,221],{"class":106},[82,532,533],{"class":161},"queue",[82,535,501],{"class":106},[82,537,538,540,542,545],{"class":84,"line":212},[82,539,506],{"class":161},[82,541,221],{"class":106},[82,543,544],{"class":161},"created_at",[82,546,501],{"class":106},[82,548,549,552,554,557],{"class":84,"line":336},[82,550,551],{"class":161},"       l",[82,553,221],{"class":106},[82,555,556],{"class":161},"first_resolved_at",[82,558,501],{"class":106},[82,560,561,563,565,568],{"class":84,"line":349},[82,562,551],{"class":161},[82,564,221],{"class":106},[82,566,567],{"class":161},"last_resolved_at",[82,569,501],{"class":106},[82,571,572,574,576,579],{"class":84,"line":355},[82,573,551],{"class":161},[82,575,221],{"class":106},[82,577,578],{"class":161},"reopen_count",[82,580,501],{"class":106},[82,582,583,585,587,589,592,594,596,598,601],{"class":84,"line":362},[82,584,551],{"class":161},[82,586,221],{"class":106},[82,588,567],{"class":161},[82,590,591],{"class":95}," -",[82,593,493],{"class":161},[82,595,221],{"class":106},[82,597,544],{"class":161},[82,599,600],{"class":95},"   AS",[82,602,603],{"class":106}," time_to_resolve,\n",[82,605,606,608,610,613,615,617,619,621,624],{"class":84,"line":387},[82,607,506],{"class":161},[82,609,221],{"class":106},[82,611,612],{"class":161},"first_response_at",[82,614,591],{"class":95},[82,616,493],{"class":161},[82,618,221],{"class":106},[82,620,544],{"class":161},[82,622,623],{"class":95},"  AS",[82,625,626],{"class":106}," time_to_first_response\n",[82,628,629,631,633,635,638],{"class":84,"line":408},[82,630,215],{"class":95},[82,632,103],{"class":161},[82,634,221],{"class":106},[82,636,637],{"class":161},"stg_tickets",[82,639,640],{"class":106}," t\n",[82,642,643,646,648,650,653,656,659],{"class":84,"line":445},[82,644,645],{"class":95},"JOIN",[82,647,103],{"class":161},[82,649,221],{"class":106},[82,651,652],{"class":161},"int_ticket_lifecycle",[82,654,655],{"class":106}," l ",[82,657,658],{"class":95},"USING",[82,660,661],{"class":106}," (ticket_id)\n",[82,663,664,666,668,670,673,676,678,680,682,684],{"class":84,"line":453},[82,665,376],{"class":95},[82,667,493],{"class":161},[82,669,221],{"class":106},[82,671,672],{"class":161},"status",[82,674,675],{"class":95}," IN",[82,677,294],{"class":106},[82,679,297],{"class":125},[82,681,300],{"class":106},[82,683,303],{"class":125},[82,685,352],{"class":106},[82,687,689,692,694,696,698,700,702,704,707,709,712,714,717],{"class":84,"line":688},16,[82,690,691],{"class":95},"  AND",[82,693,493],{"class":161},[82,695,221],{"class":106},[82,697,533],{"class":161},[82,699,434],{"class":95},[82,701,675],{"class":95},[82,703,294],{"class":106},[82,705,706],{"class":125},"'spam'",[82,708,300],{"class":106},[82,710,711],{"class":125},"'merged'",[82,713,300],{"class":106},[82,715,716],{"class":125},"'internal-project'",[82,718,719],{"class":106},");\n",[82,721,723],{"class":84,"line":722},17,[82,724,726],{"emptyLinePlaceholder":725},true,"\n",[82,728,730,733,735,737,740],{"class":84,"line":729},18,[82,731,732],{"class":95},"COMMENT ON VIEW ",[82,734,31],{"class":161},[82,736,221],{"class":106},[82,738,739],{"class":161},"fct_ticket_resolution",[82,741,742],{"class":95}," IS\n",[82,744,746],{"class":84,"line":745},19,[82,747,748],{"class":125},"  'One row per resolved ticket. Resolution time runs from creation to the FINAL resolution, '\n",[82,750,752],{"class":84,"line":751},20,[82,753,754],{"class":125},"  'so a reopened ticket counts once with its full elapsed time. Excludes spam, merged, and '\n",[82,756,758,761],{"class":84,"line":757},21,[82,759,760],{"class":125},"  'internal project queues. Wall-clock time, not business hours. Owner: service desk lead.'",[82,762,227],{"class":106},[11,764,765],{},"Everything else reads from that one model. MTTR by month and priority, with the median next to the mean because a few tickets left open for weeks drag the average far from what a typical customer sees:",[73,767,769],{"className":75,"code":768,"language":77,"meta":78,"style":78},"-- models\u002F040_mttr_monthly.sql\nCREATE OR REPLACE VIEW marts.mttr_monthly AS\nSELECT date_trunc('month', last_resolved_at)::date AS month,\n       priority,\n       count(*)                                      AS tickets_resolved,\n       round((avg(extract(epoch FROM time_to_resolve)) \u002F 3600)::numeric, 1) AS mttr_hours,\n       round((percentile_cont(0.5) WITHIN GROUP (\n                 ORDER BY extract(epoch FROM time_to_resolve)::float8) \u002F 3600)::numeric, 1) AS median_hours,\n       count(*) FILTER (WHERE reopen_count > 0)      AS reopened_tickets\nFROM marts.fct_ticket_resolution\nGROUP BY 1, 2;\n",[15,770,771,776,789,812,817,833,875,904,936,969,980],{"__ignoreMap":78},[82,772,773],{"class":84,"line":85},[82,774,775],{"class":88},"-- models\u002F040_mttr_monthly.sql\n",[82,777,778,780,782,784,787],{"class":84,"line":92},[82,779,96],{"class":95},[82,781,99],{"class":95},[82,783,481],{"class":102},[82,785,786],{"class":106},".mttr_monthly ",[82,788,110],{"class":95},[82,790,791,793,796,799,802,805,807,810],{"class":84,"line":113},[82,792,116],{"class":95},[82,794,795],{"class":106}," date_trunc(",[82,797,798],{"class":125},"'month'",[82,800,801],{"class":106},", last_resolved_at)::",[82,803,804],{"class":95},"date",[82,806,152],{"class":95},[82,808,809],{"class":95}," month",[82,811,501],{"class":106},[82,813,814],{"class":84,"line":135},[82,815,816],{"class":106},"       priority,\n",[82,818,819,821,823,825,828,830],{"class":84,"line":158},[82,820,411],{"class":161},[82,822,165],{"class":106},[82,824,416],{"class":95},[82,826,827],{"class":106},")                                      ",[82,829,182],{"class":95},[82,831,832],{"class":106}," tickets_resolved,\n",[82,834,835,838,841,844,847,849,852,855,858,860,863,865,868,870,872],{"class":84,"line":188},[82,836,837],{"class":161},"       round",[82,839,840],{"class":106},"((",[82,842,843],{"class":161},"avg",[82,845,846],{"class":106},"(extract(epoch ",[82,848,215],{"class":95},[82,850,851],{"class":106}," time_to_resolve)) ",[82,853,854],{"class":95},"\u002F",[82,856,857],{"class":161}," 3600",[82,859,146],{"class":106},[82,861,862],{"class":95},"numeric",[82,864,300],{"class":106},[82,866,867],{"class":161},"1",[82,869,306],{"class":106},[82,871,182],{"class":95},[82,873,874],{"class":106}," mttr_hours,\n",[82,876,877,879,881,884,886,889,891,894,896,899,902],{"class":84,"line":212},[82,878,837],{"class":161},[82,880,840],{"class":106},[82,882,883],{"class":161},"percentile_cont",[82,885,165],{"class":106},[82,887,888],{"class":161},"0",[82,890,221],{"class":106},[82,892,893],{"class":161},"5",[82,895,306],{"class":106},[82,897,898],{"class":95},"WITHIN",[82,900,901],{"class":161}," GROUP",[82,903,271],{"class":106},[82,905,906,909,912,914,917,919,921,923,925,927,929,931,933],{"class":84,"line":336},[82,907,908],{"class":95},"                 ORDER BY",[82,910,911],{"class":106}," extract(epoch ",[82,913,215],{"class":95},[82,915,916],{"class":106}," time_to_resolve)::float8) ",[82,918,854],{"class":95},[82,920,857],{"class":161},[82,922,146],{"class":106},[82,924,862],{"class":95},[82,926,300],{"class":106},[82,928,867],{"class":161},[82,930,306],{"class":106},[82,932,182],{"class":95},[82,934,935],{"class":106}," median_hours,\n",[82,937,938,940,942,944,946,948,950,952,955,958,961,964,966],{"class":84,"line":349},[82,939,411],{"class":161},[82,941,165],{"class":106},[82,943,416],{"class":95},[82,945,306],{"class":106},[82,947,371],{"class":95},[82,949,294],{"class":106},[82,951,376],{"class":95},[82,953,954],{"class":106}," reopen_count ",[82,956,957],{"class":95},">",[82,959,960],{"class":161}," 0",[82,962,963],{"class":106},")      ",[82,965,182],{"class":95},[82,967,968],{"class":106}," reopened_tickets\n",[82,970,971,973,975,977],{"class":84,"line":355},[82,972,215],{"class":95},[82,974,481],{"class":161},[82,976,221],{"class":106},[82,978,979],{"class":161},"fct_ticket_resolution\n",[82,981,982,984,987,989,992],{"class":84,"line":362},[82,983,456],{"class":95},[82,985,986],{"class":161}," 1",[82,988,300],{"class":106},[82,990,991],{"class":161},"2",[82,993,227],{"class":106},[11,995,996,997,999],{},"SLA compliance joins against a small, version-controlled targets table instead of hardcoding hours in the query. A ticket with no first response yet counts as a miss, because a null comparison never satisfies the ",[15,998,371],{},":",[73,1001,1003],{"className":75,"code":1002,"language":77,"meta":78,"style":78},"-- models\u002F050_sla_compliance_monthly.sql\nCREATE TABLE IF NOT EXISTS staging.sla_targets (\n    priority        text PRIMARY KEY,\n    first_response  interval NOT NULL,\n    resolution      interval NOT NULL\n);\n\nINSERT INTO staging.sla_targets VALUES\n    ('p1', interval '15 minutes', interval '4 hours'),\n    ('p2', interval '1 hour',     interval '8 hours'),\n    ('p3', interval '4 hours',    interval '3 days'),\n    ('p4', interval '1 day',      interval '10 days')\nON CONFLICT (priority) DO UPDATE\n    SET first_response = EXCLUDED.first_response, resolution = EXCLUDED.resolution;\n\nCREATE OR REPLACE VIEW marts.sla_compliance_monthly AS\nSELECT date_trunc('month', f.created_at)::date AS month,\n       f.priority,\n       count(*) AS tickets,\n       round(100.0 * count(*) FILTER (WHERE f.time_to_first_response \u003C= s.first_response) \u002F count(*), 1) AS first_response_pct,\n       round(100.0 * count(*) FILTER (WHERE f.time_to_resolve \u003C= s.resolution) \u002F count(*), 1)            AS resolution_pct\nFROM marts.fct_ticket_resolution f\nJOIN staging.sla_targets s USING (priority)\nGROUP BY 1, 2;\n",[15,1004,1005,1010,1035,1048,1058,1066,1070,1074,1088,1110,1130,1149,1169,1180,1213,1217,1230,1257,1268,1283,1354,1419,1433,1452],{"__ignoreMap":78},[82,1006,1007],{"class":84,"line":85},[82,1008,1009],{"class":88},"-- models\u002F050_sla_compliance_monthly.sql\n",[82,1011,1012,1015,1018,1021,1023,1026,1028,1030,1033],{"class":84,"line":92},[82,1013,1014],{"class":95},"CREATE",[82,1016,1017],{"class":95}," TABLE",[82,1019,1020],{"class":102}," IF",[82,1022,434],{"class":95},[82,1024,1025],{"class":95}," EXISTS",[82,1027,103],{"class":161},[82,1029,221],{"class":106},[82,1031,1032],{"class":161},"sla_targets",[82,1034,271],{"class":106},[82,1036,1037,1040,1043,1046],{"class":84,"line":113},[82,1038,1039],{"class":106},"    priority        ",[82,1041,1042],{"class":95},"text",[82,1044,1045],{"class":95}," PRIMARY KEY",[82,1047,501],{"class":106},[82,1049,1050,1053,1056],{"class":84,"line":135},[82,1051,1052],{"class":106},"    first_response  interval ",[82,1054,1055],{"class":95},"NOT NULL",[82,1057,501],{"class":106},[82,1059,1060,1063],{"class":84,"line":158},[82,1061,1062],{"class":106},"    resolution      interval ",[82,1064,1065],{"class":95},"NOT NULL\n",[82,1067,1068],{"class":84,"line":188},[82,1069,719],{"class":106},[82,1071,1072],{"class":84,"line":212},[82,1073,726],{"emptyLinePlaceholder":725},[82,1075,1076,1079,1081,1083,1085],{"class":84,"line":336},[82,1077,1078],{"class":95},"INSERT INTO",[82,1080,103],{"class":161},[82,1082,221],{"class":106},[82,1084,1032],{"class":161},[82,1086,1087],{"class":95}," VALUES\n",[82,1089,1090,1093,1096,1099,1102,1104,1107],{"class":84,"line":349},[82,1091,1092],{"class":106},"    (",[82,1094,1095],{"class":125},"'p1'",[82,1097,1098],{"class":106},", interval ",[82,1100,1101],{"class":125},"'15 minutes'",[82,1103,1098],{"class":106},[82,1105,1106],{"class":125},"'4 hours'",[82,1108,1109],{"class":106},"),\n",[82,1111,1112,1114,1117,1119,1122,1125,1128],{"class":84,"line":355},[82,1113,1092],{"class":106},[82,1115,1116],{"class":125},"'p2'",[82,1118,1098],{"class":106},[82,1120,1121],{"class":125},"'1 hour'",[82,1123,1124],{"class":106},",     interval ",[82,1126,1127],{"class":125},"'8 hours'",[82,1129,1109],{"class":106},[82,1131,1132,1134,1137,1139,1141,1144,1147],{"class":84,"line":362},[82,1133,1092],{"class":106},[82,1135,1136],{"class":125},"'p3'",[82,1138,1098],{"class":106},[82,1140,1106],{"class":125},[82,1142,1143],{"class":106},",    interval ",[82,1145,1146],{"class":125},"'3 days'",[82,1148,1109],{"class":106},[82,1150,1151,1153,1156,1158,1161,1164,1167],{"class":84,"line":387},[82,1152,1092],{"class":106},[82,1154,1155],{"class":125},"'p4'",[82,1157,1098],{"class":106},[82,1159,1160],{"class":125},"'1 day'",[82,1162,1163],{"class":106},",      interval ",[82,1165,1166],{"class":125},"'10 days'",[82,1168,352],{"class":106},[82,1170,1171,1174,1177],{"class":84,"line":408},[82,1172,1173],{"class":95},"ON",[82,1175,1176],{"class":106}," CONFLICT (priority) DO ",[82,1178,1179],{"class":95},"UPDATE\n",[82,1181,1182,1185,1188,1191,1194,1196,1199,1202,1204,1206,1208,1211],{"class":84,"line":445},[82,1183,1184],{"class":95},"    SET",[82,1186,1187],{"class":106}," first_response ",[82,1189,1190],{"class":95},"=",[82,1192,1193],{"class":161}," EXCLUDED",[82,1195,221],{"class":106},[82,1197,1198],{"class":161},"first_response",[82,1200,1201],{"class":106},", resolution ",[82,1203,1190],{"class":95},[82,1205,1193],{"class":161},[82,1207,221],{"class":106},[82,1209,1210],{"class":161},"resolution",[82,1212,227],{"class":106},[82,1214,1215],{"class":84,"line":453},[82,1216,726],{"emptyLinePlaceholder":725},[82,1218,1219,1221,1223,1225,1228],{"class":84,"line":688},[82,1220,96],{"class":95},[82,1222,99],{"class":95},[82,1224,481],{"class":102},[82,1226,1227],{"class":106},".sla_compliance_monthly ",[82,1229,110],{"class":95},[82,1231,1232,1234,1236,1238,1240,1243,1245,1247,1249,1251,1253,1255],{"class":84,"line":722},[82,1233,116],{"class":95},[82,1235,795],{"class":106},[82,1237,798],{"class":125},[82,1239,300],{"class":106},[82,1241,1242],{"class":161},"f",[82,1244,221],{"class":106},[82,1246,544],{"class":161},[82,1248,146],{"class":106},[82,1250,804],{"class":95},[82,1252,152],{"class":95},[82,1254,809],{"class":95},[82,1256,501],{"class":106},[82,1258,1259,1262,1264,1266],{"class":84,"line":729},[82,1260,1261],{"class":161},"       f",[82,1263,221],{"class":106},[82,1265,522],{"class":161},[82,1267,501],{"class":106},[82,1269,1270,1272,1274,1276,1278,1280],{"class":84,"line":745},[82,1271,411],{"class":161},[82,1273,165],{"class":106},[82,1275,416],{"class":95},[82,1277,306],{"class":106},[82,1279,182],{"class":95},[82,1281,1282],{"class":106}," tickets,\n",[82,1284,1285,1287,1289,1292,1294,1296,1299,1302,1304,1306,1308,1310,1312,1314,1317,1319,1322,1325,1328,1330,1332,1334,1336,1338,1340,1342,1345,1347,1349,1351],{"class":84,"line":751},[82,1286,837],{"class":161},[82,1288,165],{"class":106},[82,1290,1291],{"class":161},"100",[82,1293,221],{"class":106},[82,1295,888],{"class":161},[82,1297,1298],{"class":95}," *",[82,1300,1301],{"class":161}," count",[82,1303,165],{"class":106},[82,1305,416],{"class":95},[82,1307,306],{"class":106},[82,1309,371],{"class":95},[82,1311,294],{"class":106},[82,1313,376],{"class":95},[82,1315,1316],{"class":161}," f",[82,1318,221],{"class":106},[82,1320,1321],{"class":161},"time_to_first_response",[82,1323,1324],{"class":95}," \u003C=",[82,1326,1327],{"class":161}," s",[82,1329,221],{"class":106},[82,1331,1198],{"class":161},[82,1333,306],{"class":106},[82,1335,854],{"class":95},[82,1337,1301],{"class":161},[82,1339,165],{"class":106},[82,1341,416],{"class":95},[82,1343,1344],{"class":106},"), ",[82,1346,867],{"class":161},[82,1348,306],{"class":106},[82,1350,182],{"class":95},[82,1352,1353],{"class":106}," first_response_pct,\n",[82,1355,1356,1358,1360,1362,1364,1366,1368,1370,1372,1374,1376,1378,1380,1382,1384,1386,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407,1409,1411,1414,1416],{"class":84,"line":757},[82,1357,837],{"class":161},[82,1359,165],{"class":106},[82,1361,1291],{"class":161},[82,1363,221],{"class":106},[82,1365,888],{"class":161},[82,1367,1298],{"class":95},[82,1369,1301],{"class":161},[82,1371,165],{"class":106},[82,1373,416],{"class":95},[82,1375,306],{"class":106},[82,1377,371],{"class":95},[82,1379,294],{"class":106},[82,1381,376],{"class":95},[82,1383,1316],{"class":161},[82,1385,221],{"class":106},[82,1387,1388],{"class":161},"time_to_resolve",[82,1390,1324],{"class":95},[82,1392,1327],{"class":161},[82,1394,221],{"class":106},[82,1396,1210],{"class":161},[82,1398,306],{"class":106},[82,1400,854],{"class":95},[82,1402,1301],{"class":161},[82,1404,165],{"class":106},[82,1406,416],{"class":95},[82,1408,1344],{"class":106},[82,1410,867],{"class":161},[82,1412,1413],{"class":106},")            ",[82,1415,182],{"class":95},[82,1417,1418],{"class":106}," resolution_pct\n",[82,1420,1422,1424,1426,1428,1430],{"class":84,"line":1421},22,[82,1423,215],{"class":95},[82,1425,481],{"class":161},[82,1427,221],{"class":106},[82,1429,739],{"class":161},[82,1431,1432],{"class":106}," f\n",[82,1434,1436,1438,1440,1442,1444,1447,1449],{"class":84,"line":1435},23,[82,1437,645],{"class":95},[82,1439,103],{"class":161},[82,1441,221],{"class":106},[82,1443,1032],{"class":161},[82,1445,1446],{"class":106}," s ",[82,1448,658],{"class":95},[82,1450,1451],{"class":106}," (priority)\n",[82,1453,1455,1457,1459,1461,1463],{"class":84,"line":1454},24,[82,1456,456],{"class":95},[82,1458,986],{"class":161},[82,1460,300],{"class":106},[82,1462,991],{"class":161},[82,1464,227],{"class":106},[11,1466,1467],{},"The targets above are example values. Use the ones in your actual contracts.",[11,1469,1470],{},"Ticket aging answers \"how bad is the backlog right now,\" so it reads open tickets from staging rather than the resolved-ticket fact:",[73,1472,1474],{"className":75,"code":1473,"language":77,"meta":78,"style":78},"-- models\u002F060_ticket_aging.sql\nCREATE OR REPLACE VIEW marts.ticket_aging AS\nSELECT client_id,\n       priority,\n       CASE\n           WHEN age \u003C interval '1 day'   THEN '1: under 1 day'\n           WHEN age \u003C interval '3 days'  THEN '2: 1-3 days'\n           WHEN age \u003C interval '7 days'  THEN '3: 3-7 days'\n           WHEN age \u003C interval '30 days' THEN '4: 7-30 days'\n           ELSE '5: over 30 days'\n       END      AS age_bucket,\n       count(*) AS open_tickets\nFROM (\n    SELECT client_id, priority, now() - created_at AS age\n    FROM staging.stg_tickets\n    WHERE status NOT IN ('resolved', 'closed', 'cancelled')\n) AS open_tickets\nGROUP BY 1, 2, 3;\n",[15,1475,1476,1481,1494,1501,1505,1510,1532,1550,1568,1587,1595,1606,1621,1627,1651,1662,1689,1697],{"__ignoreMap":78},[82,1477,1478],{"class":84,"line":85},[82,1479,1480],{"class":88},"-- models\u002F060_ticket_aging.sql\n",[82,1482,1483,1485,1487,1489,1492],{"class":84,"line":92},[82,1484,96],{"class":95},[82,1486,99],{"class":95},[82,1488,481],{"class":102},[82,1490,1491],{"class":106},".ticket_aging ",[82,1493,110],{"class":95},[82,1495,1496,1498],{"class":84,"line":113},[82,1497,116],{"class":95},[82,1499,1500],{"class":106}," client_id,\n",[82,1502,1503],{"class":84,"line":135},[82,1504,816],{"class":106},[82,1506,1507],{"class":84,"line":158},[82,1508,1509],{"class":95},"       CASE\n",[82,1511,1512,1515,1518,1521,1524,1526,1529],{"class":84,"line":188},[82,1513,1514],{"class":95},"           WHEN",[82,1516,1517],{"class":106}," age ",[82,1519,1520],{"class":95},"\u003C",[82,1522,1523],{"class":106}," interval ",[82,1525,1160],{"class":125},[82,1527,1528],{"class":95},"   THEN",[82,1530,1531],{"class":125}," '1: under 1 day'\n",[82,1533,1534,1536,1538,1540,1542,1544,1547],{"class":84,"line":212},[82,1535,1514],{"class":95},[82,1537,1517],{"class":106},[82,1539,1520],{"class":95},[82,1541,1523],{"class":106},[82,1543,1146],{"class":125},[82,1545,1546],{"class":95},"  THEN",[82,1548,1549],{"class":125}," '2: 1-3 days'\n",[82,1551,1552,1554,1556,1558,1560,1563,1565],{"class":84,"line":336},[82,1553,1514],{"class":95},[82,1555,1517],{"class":106},[82,1557,1520],{"class":95},[82,1559,1523],{"class":106},[82,1561,1562],{"class":125},"'7 days'",[82,1564,1546],{"class":95},[82,1566,1567],{"class":125}," '3: 3-7 days'\n",[82,1569,1570,1572,1574,1576,1578,1581,1584],{"class":84,"line":349},[82,1571,1514],{"class":95},[82,1573,1517],{"class":106},[82,1575,1520],{"class":95},[82,1577,1523],{"class":106},[82,1579,1580],{"class":125},"'30 days'",[82,1582,1583],{"class":95}," THEN",[82,1585,1586],{"class":125}," '4: 7-30 days'\n",[82,1588,1589,1592],{"class":84,"line":355},[82,1590,1591],{"class":95},"           ELSE",[82,1593,1594],{"class":125}," '5: over 30 days'\n",[82,1596,1597,1600,1603],{"class":84,"line":362},[82,1598,1599],{"class":95},"       END",[82,1601,1602],{"class":95},"      AS",[82,1604,1605],{"class":106}," age_bucket,\n",[82,1607,1608,1610,1612,1614,1616,1618],{"class":84,"line":387},[82,1609,411],{"class":161},[82,1611,165],{"class":106},[82,1613,416],{"class":95},[82,1615,306],{"class":106},[82,1617,182],{"class":95},[82,1619,1620],{"class":106}," open_tickets\n",[82,1622,1623,1625],{"class":84,"line":408},[82,1624,215],{"class":95},[82,1626,271],{"class":106},[82,1628,1629,1631,1634,1637,1640,1643,1646,1648],{"class":84,"line":445},[82,1630,276],{"class":95},[82,1632,1633],{"class":106}," client_id, priority, ",[82,1635,1636],{"class":95},"now",[82,1638,1639],{"class":106},"() ",[82,1641,1642],{"class":95},"-",[82,1644,1645],{"class":106}," created_at ",[82,1647,182],{"class":95},[82,1649,1650],{"class":106}," age\n",[82,1652,1653,1655,1657,1659],{"class":84,"line":453},[82,1654,339],{"class":95},[82,1656,103],{"class":161},[82,1658,221],{"class":106},[82,1660,1661],{"class":161},"stg_tickets\n",[82,1663,1664,1667,1670,1672,1674,1676,1678,1680,1682,1684,1687],{"class":84,"line":688},[82,1665,1666],{"class":95},"    WHERE",[82,1668,1669],{"class":95}," status",[82,1671,434],{"class":95},[82,1673,675],{"class":95},[82,1675,294],{"class":106},[82,1677,297],{"class":125},[82,1679,300],{"class":106},[82,1681,303],{"class":125},[82,1683,300],{"class":106},[82,1685,1686],{"class":125},"'cancelled'",[82,1688,352],{"class":106},[82,1690,1691,1693,1695],{"class":84,"line":722},[82,1692,306],{"class":106},[82,1694,182],{"class":95},[82,1696,1620],{"class":106},[82,1698,1699,1701,1703,1705,1707,1709,1712],{"class":84,"line":729},[82,1700,456],{"class":95},[82,1702,986],{"class":161},[82,1704,300],{"class":106},[82,1706,991],{"class":161},[82,1708,300],{"class":106},[82,1710,1711],{"class":161},"3",[82,1713,227],{"class":106},[11,1715,1716,1717,1720],{},"Patch compliance follows the same shape against RMM data. The definition, written in one place, is that a device is compliant when it has no missing critical updates ",[232,1718,1719],{},"and"," has checked in recently. That second condition matters: a laptop that hasn't reported in a month shows zero missing updates only because nobody has asked it.",[73,1722,1724],{"className":75,"code":1723,"language":77,"meta":78,"style":78},"-- models\u002F070_patch_compliance.sql\nCREATE OR REPLACE VIEW marts.patch_compliance AS\nSELECT client_id,\n       os_family,\n       count(*)                                    AS active_devices,\n       count(*) FILTER (WHERE is_compliant)        AS compliant_devices,\n       round(100.0 * count(*) FILTER (WHERE is_compliant) \u002F nullif(count(*), 0), 1) AS compliance_pct,\n       count(*) FILTER (WHERE NOT reported_recently) AS not_reporting_devices\nFROM (\n    SELECT client_id,\n           os_family,\n           last_seen_at >= now() - interval '14 days' AS reported_recently,\n           missing_critical_updates = 0\n             AND last_seen_at >= now() - interval '14 days' AS is_compliant\n    FROM staging.stg_device_patch_status\n    WHERE NOT is_retired\n) AS devices\nGROUP BY 1, 2;\n\nCOMMENT ON VIEW marts.patch_compliance IS\n  'Compliant = zero missing critical updates AND checked in within 14 days. '\n  'Retired devices excluded. Not-reporting devices count as non-compliant.';\n",[15,1725,1726,1731,1744,1750,1755,1771,1795,1855,1881,1887,1893,1898,1923,1933,1958,1969,1978,1987,1999,2003,2016,2021],{"__ignoreMap":78},[82,1727,1728],{"class":84,"line":85},[82,1729,1730],{"class":88},"-- models\u002F070_patch_compliance.sql\n",[82,1732,1733,1735,1737,1739,1742],{"class":84,"line":92},[82,1734,96],{"class":95},[82,1736,99],{"class":95},[82,1738,481],{"class":102},[82,1740,1741],{"class":106},".patch_compliance ",[82,1743,110],{"class":95},[82,1745,1746,1748],{"class":84,"line":113},[82,1747,116],{"class":95},[82,1749,1500],{"class":106},[82,1751,1752],{"class":84,"line":135},[82,1753,1754],{"class":106},"       os_family,\n",[82,1756,1757,1759,1761,1763,1766,1768],{"class":84,"line":158},[82,1758,411],{"class":161},[82,1760,165],{"class":106},[82,1762,416],{"class":95},[82,1764,1765],{"class":106},")                                    ",[82,1767,182],{"class":95},[82,1769,1770],{"class":106}," active_devices,\n",[82,1772,1773,1775,1777,1779,1781,1783,1785,1787,1790,1792],{"class":84,"line":188},[82,1774,411],{"class":161},[82,1776,165],{"class":106},[82,1778,416],{"class":95},[82,1780,306],{"class":106},[82,1782,371],{"class":95},[82,1784,294],{"class":106},[82,1786,376],{"class":95},[82,1788,1789],{"class":106}," is_compliant)        ",[82,1791,182],{"class":95},[82,1793,1794],{"class":106}," compliant_devices,\n",[82,1796,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823,1826,1828,1831,1833,1836,1838,1840,1842,1844,1846,1848,1850,1852],{"class":84,"line":212},[82,1798,837],{"class":161},[82,1800,165],{"class":106},[82,1802,1291],{"class":161},[82,1804,221],{"class":106},[82,1806,888],{"class":161},[82,1808,1298],{"class":95},[82,1810,1301],{"class":161},[82,1812,165],{"class":106},[82,1814,416],{"class":95},[82,1816,306],{"class":106},[82,1818,371],{"class":95},[82,1820,294],{"class":106},[82,1822,376],{"class":95},[82,1824,1825],{"class":106}," is_compliant) ",[82,1827,854],{"class":95},[82,1829,1830],{"class":161}," nullif",[82,1832,165],{"class":106},[82,1834,1835],{"class":161},"count",[82,1837,165],{"class":106},[82,1839,416],{"class":95},[82,1841,1344],{"class":106},[82,1843,888],{"class":161},[82,1845,1344],{"class":106},[82,1847,867],{"class":161},[82,1849,306],{"class":106},[82,1851,182],{"class":95},[82,1853,1854],{"class":106}," compliance_pct,\n",[82,1856,1857,1859,1861,1863,1865,1867,1869,1871,1873,1876,1878],{"class":84,"line":336},[82,1858,411],{"class":161},[82,1860,165],{"class":106},[82,1862,416],{"class":95},[82,1864,306],{"class":106},[82,1866,371],{"class":95},[82,1868,294],{"class":106},[82,1870,376],{"class":95},[82,1872,434],{"class":95},[82,1874,1875],{"class":106}," reported_recently) ",[82,1877,182],{"class":95},[82,1879,1880],{"class":106}," not_reporting_devices\n",[82,1882,1883,1885],{"class":84,"line":349},[82,1884,215],{"class":95},[82,1886,271],{"class":106},[82,1888,1889,1891],{"class":84,"line":355},[82,1890,276],{"class":95},[82,1892,1500],{"class":106},[82,1894,1895],{"class":84,"line":362},[82,1896,1897],{"class":106},"           os_family,\n",[82,1899,1900,1903,1906,1909,1911,1913,1915,1918,1920],{"class":84,"line":387},[82,1901,1902],{"class":106},"           last_seen_at ",[82,1904,1905],{"class":95},">=",[82,1907,1908],{"class":95}," now",[82,1910,1639],{"class":106},[82,1912,1642],{"class":95},[82,1914,1523],{"class":106},[82,1916,1917],{"class":125},"'14 days'",[82,1919,152],{"class":95},[82,1921,1922],{"class":106}," reported_recently,\n",[82,1924,1925,1928,1930],{"class":84,"line":408},[82,1926,1927],{"class":106},"           missing_critical_updates ",[82,1929,1190],{"class":95},[82,1931,1932],{"class":161}," 0\n",[82,1934,1935,1938,1941,1943,1945,1947,1949,1951,1953,1955],{"class":84,"line":445},[82,1936,1937],{"class":95},"             AND",[82,1939,1940],{"class":106}," last_seen_at ",[82,1942,1905],{"class":95},[82,1944,1908],{"class":95},[82,1946,1639],{"class":106},[82,1948,1642],{"class":95},[82,1950,1523],{"class":106},[82,1952,1917],{"class":125},[82,1954,152],{"class":95},[82,1956,1957],{"class":106}," is_compliant\n",[82,1959,1960,1962,1964,1966],{"class":84,"line":453},[82,1961,339],{"class":95},[82,1963,103],{"class":161},[82,1965,221],{"class":106},[82,1967,1968],{"class":161},"stg_device_patch_status\n",[82,1970,1971,1973,1975],{"class":84,"line":688},[82,1972,1666],{"class":95},[82,1974,434],{"class":95},[82,1976,1977],{"class":106}," is_retired\n",[82,1979,1980,1982,1984],{"class":84,"line":722},[82,1981,306],{"class":106},[82,1983,182],{"class":95},[82,1985,1986],{"class":106}," devices\n",[82,1988,1989,1991,1993,1995,1997],{"class":84,"line":729},[82,1990,456],{"class":95},[82,1992,986],{"class":161},[82,1994,300],{"class":106},[82,1996,991],{"class":161},[82,1998,227],{"class":106},[82,2000,2001],{"class":84,"line":745},[82,2002,726],{"emptyLinePlaceholder":725},[82,2004,2005,2007,2009,2011,2014],{"class":84,"line":751},[82,2006,732],{"class":95},[82,2008,31],{"class":161},[82,2010,221],{"class":106},[82,2012,2013],{"class":161},"patch_compliance",[82,2015,742],{"class":95},[82,2017,2018],{"class":84,"line":757},[82,2019,2020],{"class":125},"  'Compliant = zero missing critical updates AND checked in within 14 days. '\n",[82,2022,2023,2026],{"class":84,"line":1421},[82,2024,2025],{"class":125},"  'Retired devices excluded. Not-reporting devices count as non-compliant.'",[82,2027,227],{"class":106},[40,2029,2031],{"id":2030},"parameterize-the-model-not-the-copy","Parameterize the model, not the copy",[11,2033,2034,2035,300,2038,2040,2041,2043],{},"The second most common failure after duplicated logic is a model that only serves one dashboard's filters (one date range, one client, one team) hardcoded into the query. The fix isn't a new copy of the query for every variant. It's a model that exposes the varying pieces as columns or parameters and keeps the metric logic itself in one place. Every view above already does the first half: ",[15,2036,2037],{},"month",[15,2039,511],{},", and ",[15,2042,522],{}," are columns, so a dashboard filters them instead of editing SQL. When a caller needs an arbitrary date range, a set-returning SQL function wraps the same fact model:",[73,2045,2047],{"className":75,"code":2046,"language":77,"meta":78,"style":78},"-- models\u002F080_fn_mttr.sql\nCREATE OR REPLACE FUNCTION marts.mttr(p_from date, p_to date, p_client_id bigint DEFAULT NULL)\nRETURNS TABLE (priority text, tickets_resolved bigint, mttr_hours numeric, median_hours numeric)\nLANGUAGE sql\nSTABLE\nAS $$\n    SELECT f.priority,\n           count(*),\n           round((avg(extract(epoch FROM f.time_to_resolve)) \u002F 3600)::numeric, 1),\n           round((percentile_cont(0.5) WITHIN GROUP (\n                     ORDER BY extract(epoch FROM f.time_to_resolve)::float8) \u002F 3600)::numeric, 1)\n    FROM marts.fct_ticket_resolution f\n    WHERE f.last_resolved_at >= p_from\n      AND f.last_resolved_at \u003C  p_to + 1\n      AND (p_client_id IS NULL OR f.client_id = p_client_id)\n    GROUP BY f.priority\n    ORDER BY f.priority\n$$;\n\n-- Q3 for every client, then Q3 for one client\nSELECT * FROM marts.mttr('2026-07-01', '2026-09-30');\nSELECT * FROM marts.mttr('2026-07-01', '2026-09-30', 1042);\n",[15,2048,2049,2054,2087,2116,2124,2129,2136,2148,2159,2195,2219,2251,2263,2279,2302,2329,2341,2352,2357,2361,2366,2394],{"__ignoreMap":78},[82,2050,2051],{"class":84,"line":85},[82,2052,2053],{"class":88},"-- models\u002F080_fn_mttr.sql\n",[82,2055,2056,2058,2061,2063,2066,2068,2071,2073,2076,2079,2082,2085],{"class":84,"line":92},[82,2057,96],{"class":95},[82,2059,2060],{"class":95}," FUNCTION",[82,2062,481],{"class":102},[82,2064,2065],{"class":106},".mttr(p_from ",[82,2067,804],{"class":95},[82,2069,2070],{"class":106},", p_to ",[82,2072,804],{"class":95},[82,2074,2075],{"class":106},", p_client_id ",[82,2077,2078],{"class":95},"bigint",[82,2080,2081],{"class":95}," DEFAULT",[82,2083,2084],{"class":95}," NULL",[82,2086,352],{"class":106},[82,2088,2089,2092,2094,2096,2099,2102,2104,2107,2109,2112,2114],{"class":84,"line":113},[82,2090,2091],{"class":95},"RETURNS",[82,2093,1017],{"class":95},[82,2095,294],{"class":106},[82,2097,2098],{"class":95},"priority text",[82,2100,2101],{"class":106},", tickets_resolved ",[82,2103,2078],{"class":95},[82,2105,2106],{"class":106},", mttr_hours ",[82,2108,862],{"class":95},[82,2110,2111],{"class":106},", median_hours ",[82,2113,862],{"class":95},[82,2115,352],{"class":106},[82,2117,2118,2121],{"class":84,"line":135},[82,2119,2120],{"class":95},"LANGUAGE",[82,2122,2123],{"class":95}," sql\n",[82,2125,2126],{"class":84,"line":158},[82,2127,2128],{"class":106},"STABLE\n",[82,2130,2131,2133],{"class":84,"line":188},[82,2132,182],{"class":95},[82,2134,2135],{"class":106}," $$\n",[82,2137,2138,2140,2142,2144,2146],{"class":84,"line":212},[82,2139,276],{"class":95},[82,2141,1316],{"class":161},[82,2143,221],{"class":106},[82,2145,522],{"class":161},[82,2147,501],{"class":106},[82,2149,2150,2153,2155,2157],{"class":84,"line":336},[82,2151,2152],{"class":161},"           count",[82,2154,165],{"class":106},[82,2156,416],{"class":95},[82,2158,1109],{"class":106},[82,2160,2161,2164,2166,2168,2170,2172,2174,2176,2178,2181,2183,2185,2187,2189,2191,2193],{"class":84,"line":349},[82,2162,2163],{"class":161},"           round",[82,2165,840],{"class":106},[82,2167,843],{"class":161},[82,2169,846],{"class":106},[82,2171,215],{"class":95},[82,2173,1316],{"class":161},[82,2175,221],{"class":106},[82,2177,1388],{"class":161},[82,2179,2180],{"class":106},")) ",[82,2182,854],{"class":95},[82,2184,857],{"class":161},[82,2186,146],{"class":106},[82,2188,862],{"class":95},[82,2190,300],{"class":106},[82,2192,867],{"class":161},[82,2194,1109],{"class":106},[82,2196,2197,2199,2201,2203,2205,2207,2209,2211,2213,2215,2217],{"class":84,"line":355},[82,2198,2163],{"class":161},[82,2200,840],{"class":106},[82,2202,883],{"class":161},[82,2204,165],{"class":106},[82,2206,888],{"class":161},[82,2208,221],{"class":106},[82,2210,893],{"class":161},[82,2212,306],{"class":106},[82,2214,898],{"class":95},[82,2216,901],{"class":161},[82,2218,271],{"class":106},[82,2220,2221,2224,2226,2228,2230,2232,2234,2237,2239,2241,2243,2245,2247,2249],{"class":84,"line":362},[82,2222,2223],{"class":95},"                     ORDER BY",[82,2225,911],{"class":106},[82,2227,215],{"class":95},[82,2229,1316],{"class":161},[82,2231,221],{"class":106},[82,2233,1388],{"class":161},[82,2235,2236],{"class":106},")::float8) ",[82,2238,854],{"class":95},[82,2240,857],{"class":161},[82,2242,146],{"class":106},[82,2244,862],{"class":95},[82,2246,300],{"class":106},[82,2248,867],{"class":161},[82,2250,352],{"class":106},[82,2252,2253,2255,2257,2259,2261],{"class":84,"line":387},[82,2254,339],{"class":95},[82,2256,481],{"class":161},[82,2258,221],{"class":106},[82,2260,739],{"class":161},[82,2262,1432],{"class":106},[82,2264,2265,2267,2269,2271,2273,2276],{"class":84,"line":408},[82,2266,1666],{"class":95},[82,2268,1316],{"class":161},[82,2270,221],{"class":106},[82,2272,567],{"class":161},[82,2274,2275],{"class":95}," >=",[82,2277,2278],{"class":106}," p_from\n",[82,2280,2281,2284,2286,2288,2290,2293,2296,2299],{"class":84,"line":445},[82,2282,2283],{"class":95},"      AND",[82,2285,1316],{"class":161},[82,2287,221],{"class":106},[82,2289,567],{"class":161},[82,2291,2292],{"class":95}," \u003C",[82,2294,2295],{"class":106},"  p_to ",[82,2297,2298],{"class":95},"+",[82,2300,2301],{"class":161}," 1\n",[82,2303,2304,2306,2309,2312,2314,2317,2319,2321,2323,2326],{"class":84,"line":453},[82,2305,2283],{"class":95},[82,2307,2308],{"class":106}," (p_client_id ",[82,2310,2311],{"class":95},"IS",[82,2313,2084],{"class":95},[82,2315,2316],{"class":95}," OR",[82,2318,1316],{"class":161},[82,2320,221],{"class":106},[82,2322,511],{"class":161},[82,2324,2325],{"class":95}," =",[82,2327,2328],{"class":106}," p_client_id)\n",[82,2330,2331,2334,2336,2338],{"class":84,"line":688},[82,2332,2333],{"class":95},"    GROUP BY",[82,2335,1316],{"class":161},[82,2337,221],{"class":106},[82,2339,2340],{"class":161},"priority\n",[82,2342,2343,2346,2348,2350],{"class":84,"line":722},[82,2344,2345],{"class":95},"    ORDER BY",[82,2347,1316],{"class":161},[82,2349,221],{"class":106},[82,2351,2340],{"class":161},[82,2353,2354],{"class":84,"line":729},[82,2355,2356],{"class":106},"$$;\n",[82,2358,2359],{"class":84,"line":745},[82,2360,726],{"emptyLinePlaceholder":725},[82,2362,2363],{"class":84,"line":751},[82,2364,2365],{"class":88},"-- Q3 for every client, then Q3 for one client\n",[82,2367,2368,2370,2372,2375,2377,2379,2382,2384,2387,2389,2392],{"class":84,"line":757},[82,2369,116],{"class":95},[82,2371,1298],{"class":95},[82,2373,2374],{"class":95}," FROM",[82,2376,481],{"class":161},[82,2378,221],{"class":106},[82,2380,2381],{"class":161},"mttr",[82,2383,165],{"class":106},[82,2385,2386],{"class":125},"'2026-07-01'",[82,2388,300],{"class":106},[82,2390,2391],{"class":125},"'2026-09-30'",[82,2393,719],{"class":106},[82,2395,2396,2398,2400,2402,2404,2406,2408,2410,2412,2414,2416,2418,2421],{"class":84,"line":1421},[82,2397,116],{"class":95},[82,2399,1298],{"class":95},[82,2401,2374],{"class":95},[82,2403,481],{"class":161},[82,2405,221],{"class":106},[82,2407,2381],{"class":161},[82,2409,165],{"class":106},[82,2411,2386],{"class":125},[82,2413,300],{"class":106},[82,2415,2391],{"class":125},[82,2417,300],{"class":106},[82,2419,2420],{"class":161},"1042",[82,2422,719],{"class":106},[11,2424,2425],{},"A tool like dbt makes this natural with its templating, but plain SQL views with a consistent set of filter columns get most of the same benefit without adopting a new toolchain. That matters when there's no dedicated team to own that toolchain.",[40,2427,2429],{"id":2428},"materialize-only-when-a-view-gets-slow","Materialize only when a view gets slow",[11,2431,2432,2433,2435,2436,2439,2440,2443],{},"Every model above is a plain view, which means every dashboard load re-runs the whole chain down to ",[15,2434,24],{},". That's fine as long as the whole chain re-runs faster than people will wait for a dashboard tile, which at service desk volumes it usually does, and views have one big advantage: they're never stale. It stops being fine when a tile's query takes longer than that and ",[15,2437,2438],{},"EXPLAIN ANALYZE"," shows the time going into re-deriving the chain, typically once the raw status-change history has grown or several tiles hit the same model at once. When one does get slow, materialize that model alone and keep its name, so nothing downstream has to change. Replace the contents of ",[15,2441,2442],{},"030"," with:",[73,2445,2447],{"className":75,"code":2446,"language":77,"meta":78,"style":78},"-- models\u002F030_fct_ticket_resolution.sql, materialized version\nDROP MATERIALIZED VIEW IF EXISTS marts.fct_ticket_resolution CASCADE;\nCREATE MATERIALIZED VIEW marts.fct_ticket_resolution AS\nSELECT t.ticket_id, t.client_id, t.priority, t.queue, t.created_at,\n       l.first_resolved_at, l.last_resolved_at, l.reopen_count,\n       l.last_resolved_at - t.created_at  AS time_to_resolve,\n       t.first_response_at - t.created_at AS time_to_first_response\nFROM staging.stg_tickets t\nJOIN staging.int_ticket_lifecycle l USING (ticket_id)\nWHERE t.status IN ('resolved', 'closed')\n  AND t.queue NOT IN ('spam', 'merged', 'internal-project');\n\nCREATE UNIQUE INDEX fct_ticket_resolution_ticket_id ON marts.fct_ticket_resolution (ticket_id);\n\n-- run after each load, outside deploy.sh\nREFRESH MATERIALIZED VIEW CONCURRENTLY marts.fct_ticket_resolution;\n",[15,2448,2449,2454,2476,2491,2536,2563,2583,2603,2615,2631,2653,2681,2685,2707,2711,2716],{"__ignoreMap":78},[82,2450,2451],{"class":84,"line":85},[82,2452,2453],{"class":88},"-- models\u002F030_fct_ticket_resolution.sql, materialized version\n",[82,2455,2456,2459,2462,2465,2467,2469,2471,2473],{"class":84,"line":92},[82,2457,2458],{"class":95},"DROP",[82,2460,2461],{"class":106}," MATERIALIZED VIEW ",[82,2463,2464],{"class":95},"IF",[82,2466,1025],{"class":95},[82,2468,481],{"class":161},[82,2470,221],{"class":106},[82,2472,739],{"class":161},[82,2474,2475],{"class":106}," CASCADE;\n",[82,2477,2478,2480,2482,2484,2486,2488],{"class":84,"line":113},[82,2479,1014],{"class":95},[82,2481,2461],{"class":106},[82,2483,31],{"class":161},[82,2485,221],{"class":106},[82,2487,739],{"class":161},[82,2489,2490],{"class":95}," AS\n",[82,2492,2493,2495,2497,2499,2501,2503,2506,2508,2510,2512,2514,2516,2518,2520,2522,2524,2526,2528,2530,2532,2534],{"class":84,"line":135},[82,2494,116],{"class":95},[82,2496,493],{"class":161},[82,2498,221],{"class":106},[82,2500,498],{"class":161},[82,2502,300],{"class":106},[82,2504,2505],{"class":161},"t",[82,2507,221],{"class":106},[82,2509,511],{"class":161},[82,2511,300],{"class":106},[82,2513,2505],{"class":161},[82,2515,221],{"class":106},[82,2517,522],{"class":161},[82,2519,300],{"class":106},[82,2521,2505],{"class":161},[82,2523,221],{"class":106},[82,2525,533],{"class":161},[82,2527,300],{"class":106},[82,2529,2505],{"class":161},[82,2531,221],{"class":106},[82,2533,544],{"class":161},[82,2535,501],{"class":106},[82,2537,2538,2540,2542,2544,2546,2549,2551,2553,2555,2557,2559,2561],{"class":84,"line":158},[82,2539,551],{"class":161},[82,2541,221],{"class":106},[82,2543,556],{"class":161},[82,2545,300],{"class":106},[82,2547,2548],{"class":161},"l",[82,2550,221],{"class":106},[82,2552,567],{"class":161},[82,2554,300],{"class":106},[82,2556,2548],{"class":161},[82,2558,221],{"class":106},[82,2560,578],{"class":161},[82,2562,501],{"class":106},[82,2564,2565,2567,2569,2571,2573,2575,2577,2579,2581],{"class":84,"line":188},[82,2566,551],{"class":161},[82,2568,221],{"class":106},[82,2570,567],{"class":161},[82,2572,591],{"class":95},[82,2574,493],{"class":161},[82,2576,221],{"class":106},[82,2578,544],{"class":161},[82,2580,623],{"class":95},[82,2582,603],{"class":106},[82,2584,2585,2587,2589,2591,2593,2595,2597,2599,2601],{"class":84,"line":212},[82,2586,506],{"class":161},[82,2588,221],{"class":106},[82,2590,612],{"class":161},[82,2592,591],{"class":95},[82,2594,493],{"class":161},[82,2596,221],{"class":106},[82,2598,544],{"class":161},[82,2600,152],{"class":95},[82,2602,626],{"class":106},[82,2604,2605,2607,2609,2611,2613],{"class":84,"line":336},[82,2606,215],{"class":95},[82,2608,103],{"class":161},[82,2610,221],{"class":106},[82,2612,637],{"class":161},[82,2614,640],{"class":106},[82,2616,2617,2619,2621,2623,2625,2627,2629],{"class":84,"line":349},[82,2618,645],{"class":95},[82,2620,103],{"class":161},[82,2622,221],{"class":106},[82,2624,652],{"class":161},[82,2626,655],{"class":106},[82,2628,658],{"class":95},[82,2630,661],{"class":106},[82,2632,2633,2635,2637,2639,2641,2643,2645,2647,2649,2651],{"class":84,"line":355},[82,2634,376],{"class":95},[82,2636,493],{"class":161},[82,2638,221],{"class":106},[82,2640,672],{"class":161},[82,2642,675],{"class":95},[82,2644,294],{"class":106},[82,2646,297],{"class":125},[82,2648,300],{"class":106},[82,2650,303],{"class":125},[82,2652,352],{"class":106},[82,2654,2655,2657,2659,2661,2663,2665,2667,2669,2671,2673,2675,2677,2679],{"class":84,"line":362},[82,2656,691],{"class":95},[82,2658,493],{"class":161},[82,2660,221],{"class":106},[82,2662,533],{"class":161},[82,2664,434],{"class":95},[82,2666,675],{"class":95},[82,2668,294],{"class":106},[82,2670,706],{"class":125},[82,2672,300],{"class":106},[82,2674,711],{"class":125},[82,2676,300],{"class":106},[82,2678,716],{"class":125},[82,2680,719],{"class":106},[82,2682,2683],{"class":84,"line":387},[82,2684,726],{"emptyLinePlaceholder":725},[82,2686,2687,2689,2692,2695,2698,2700,2702,2704],{"class":84,"line":408},[82,2688,1014],{"class":95},[82,2690,2691],{"class":95}," UNIQUE INDEX",[82,2693,2694],{"class":102}," fct_ticket_resolution_ticket_id",[82,2696,2697],{"class":95}," ON",[82,2699,481],{"class":161},[82,2701,221],{"class":106},[82,2703,739],{"class":161},[82,2705,2706],{"class":106}," (ticket_id);\n",[82,2708,2709],{"class":84,"line":445},[82,2710,726],{"emptyLinePlaceholder":725},[82,2712,2713],{"class":84,"line":453},[82,2714,2715],{"class":88},"-- run after each load, outside deploy.sh\n",[82,2717,2718,2721,2723,2725,2727],{"class":84,"line":688},[82,2719,2720],{"class":106},"REFRESH MATERIALIZED VIEW CONCURRENTLY ",[82,2722,31],{"class":161},[82,2724,221],{"class":106},[82,2726,739],{"class":161},[82,2728,227],{"class":106},[11,2730,2731,2734,2735,2737,2738,2740,2741,2744,2745,2748],{},[15,2732,2733],{},"CONCURRENTLY"," lets dashboards keep reading during the refresh. PostgreSQL only allows it when the materialized view has at least one unique index on plain column names, with no expression and no ",[15,2736,376],{}," clause, which is what the ",[15,2739,498],{}," index is for. The first time, drop the old plain view by hand (",[15,2742,2743],{},"DROP VIEW marts.fct_ticket_resolution CASCADE;","), since the materialized drop won't touch a plain view. After that, every deploy rebuilds the fact, and ",[15,2746,2747],{},"CASCADE"," drops the views built on it, which files 040 onward recreate in the same transaction. The numbered layout makes that a normal deploy, not a special procedure.",[40,2750,2752],{"id":2751},"version-control-the-models-like-application-code","Version control the models like application code",[11,2754,2755,2756,2758],{},"SQL models for metrics that leadership makes decisions from deserve the same discipline as production code: checked into source control, reviewed before a definition changes, with a history someone can point to when a number moves and a stakeholder asks why. I've seen too many \"why did our SLA compliance number jump overnight\" conversations resolve to an undocumented ",[15,2757,376],{}," clause edit made directly against a production view, with no record of who changed it or when. A pull request with a one-line description would have made that a five-minute answer instead of a half-day investigation.",[11,2760,2761],{},"The repository needs nothing more than numbered model files, test files, and a deploy script:",[73,2763,2767],{"className":2764,"code":2766,"language":1042,"meta":78},[2765],"language-text","reporting-models\u002F\n  models\u002F010_stg_ticket_status_changes.sql\n  models\u002F020_int_ticket_lifecycle.sql\n  models\u002F030_fct_ticket_resolution.sql\n  ...\n  tests\u002Fassert_fct_ticket_unique.sql\n  tests\u002Fassert_resolution_not_negative.sql\n  tests\u002Fassert_priorities_have_sla.sql\n  deploy.sh\n",[15,2768,2766],{"__ignoreMap":78},[11,2770,2771],{},"Tests follow the convention dbt uses for singular data tests: each file is a query that returns the rows that break an assertion, and zero rows means pass.",[73,2773,2775],{"className":75,"code":2774,"language":77,"meta":78,"style":78},"-- tests\u002Fassert_fct_ticket_unique.sql\nSELECT ticket_id, count(*) FROM marts.fct_ticket_resolution GROUP BY ticket_id HAVING count(*) > 1;\n\n-- tests\u002Fassert_resolution_not_negative.sql\nSELECT ticket_id, time_to_resolve FROM marts.fct_ticket_resolution WHERE time_to_resolve \u003C interval '0';\n\n-- tests\u002Fassert_priorities_have_sla.sql\nSELECT DISTINCT t.priority FROM staging.stg_tickets t\nLEFT JOIN staging.sla_targets s USING (priority)\nWHERE s.priority IS NULL;\n",[15,2776,2777,2782,2828,2832,2837,2867,2871,2876,2897,2914],{"__ignoreMap":78},[82,2778,2779],{"class":84,"line":85},[82,2780,2781],{"class":88},"-- tests\u002Fassert_fct_ticket_unique.sql\n",[82,2783,2784,2786,2789,2791,2793,2795,2797,2799,2801,2803,2805,2808,2811,2814,2816,2818,2820,2822,2824,2826],{"class":84,"line":92},[82,2785,116],{"class":95},[82,2787,2788],{"class":106}," ticket_id, ",[82,2790,1835],{"class":161},[82,2792,165],{"class":106},[82,2794,416],{"class":95},[82,2796,306],{"class":106},[82,2798,215],{"class":95},[82,2800,481],{"class":161},[82,2802,221],{"class":106},[82,2804,739],{"class":161},[82,2806,2807],{"class":95}," GROUP BY",[82,2809,2810],{"class":106}," ticket_id ",[82,2812,2813],{"class":95},"HAVING",[82,2815,1301],{"class":161},[82,2817,165],{"class":106},[82,2819,416],{"class":95},[82,2821,306],{"class":106},[82,2823,957],{"class":95},[82,2825,986],{"class":161},[82,2827,227],{"class":106},[82,2829,2830],{"class":84,"line":113},[82,2831,726],{"emptyLinePlaceholder":725},[82,2833,2834],{"class":84,"line":135},[82,2835,2836],{"class":88},"-- tests\u002Fassert_resolution_not_negative.sql\n",[82,2838,2839,2841,2844,2846,2848,2850,2852,2855,2858,2860,2862,2865],{"class":84,"line":158},[82,2840,116],{"class":95},[82,2842,2843],{"class":106}," ticket_id, time_to_resolve ",[82,2845,215],{"class":95},[82,2847,481],{"class":161},[82,2849,221],{"class":106},[82,2851,739],{"class":161},[82,2853,2854],{"class":95}," WHERE",[82,2856,2857],{"class":106}," time_to_resolve ",[82,2859,1520],{"class":95},[82,2861,1523],{"class":106},[82,2863,2864],{"class":125},"'0'",[82,2866,227],{"class":106},[82,2868,2869],{"class":84,"line":188},[82,2870,726],{"emptyLinePlaceholder":725},[82,2872,2873],{"class":84,"line":212},[82,2874,2875],{"class":88},"-- tests\u002Fassert_priorities_have_sla.sql\n",[82,2877,2878,2881,2883,2885,2887,2889,2891,2893,2895],{"class":84,"line":336},[82,2879,2880],{"class":95},"SELECT DISTINCT",[82,2882,493],{"class":161},[82,2884,221],{"class":106},[82,2886,522],{"class":161},[82,2888,2374],{"class":95},[82,2890,103],{"class":161},[82,2892,221],{"class":106},[82,2894,637],{"class":161},[82,2896,640],{"class":106},[82,2898,2899,2902,2904,2906,2908,2910,2912],{"class":84,"line":349},[82,2900,2901],{"class":95},"LEFT JOIN",[82,2903,103],{"class":161},[82,2905,221],{"class":106},[82,2907,1032],{"class":161},[82,2909,1446],{"class":106},[82,2911,658],{"class":95},[82,2913,1451],{"class":106},[82,2915,2916,2918,2920,2922,2924,2927,2929],{"class":84,"line":355},[82,2917,376],{"class":95},[82,2919,1327],{"class":161},[82,2921,221],{"class":106},[82,2923,522],{"class":161},[82,2925,2926],{"class":95}," IS",[82,2928,2084],{"class":95},[82,2930,227],{"class":106},[11,2932,2933],{},"The deploy script applies every model in one transaction, so a broken model (one that fails to apply) leaves the previous versions in place, then runs the tests. The tests run after that transaction has committed, so a failing test doesn't roll anything back: the new views are already live, and the non-zero exit is a signal to fix forward or revert the commit and redeploy:",[73,2935,2939],{"className":2936,"code":2937,"language":2938,"meta":78,"style":78},"language-bash shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# deploy.sh: apply models in order in one transaction, then run SQL tests.\n# Needs psql and MODELS_DSN for the transformer role. Exits non-zero on any failure.\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nargs=()\nfor model in models\u002F*.sql; do\n  args+=(--file \"$model\")\ndone\npsql \"$MODELS_DSN\" --set ON_ERROR_STOP=1 --single-transaction --quiet \"${args[@]}\"\necho \"Applied ${#args[@]} model file(s).\"\n\nfailed=0\nfor test in tests\u002F*.sql; do\n  # A psql error stops the script here (set -e); only grep's no-match status is tolerated.\n  output=$(psql \"$MODELS_DSN\" --set ON_ERROR_STOP=1 --no-align --tuples-only --file \"$test\")\n  rows=$(grep -c . \u003C\u003C\u003C\"$output\" || true)\n  if [[ \"$rows\" -gt 0 ]]; then\n    echo \"FAIL $test ($rows row(s))\"\n    failed=1\n  else\n    echo \"pass $test\"\n  fi\ndone\nexit \"$failed\"\n","bash",[15,2940,2941,2946,2951,2956,2967,2987,2991,3001,3021,3044,3049,3088,3108,3112,3122,3138,3143,3185,3221,3247,3264,3274,3279,3291,3296,3301],{"__ignoreMap":78},[82,2942,2943],{"class":84,"line":85},[82,2944,2945],{"class":88},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[82,2947,2948],{"class":84,"line":92},[82,2949,2950],{"class":88},"# deploy.sh: apply models in order in one transaction, then run SQL tests.\n",[82,2952,2953],{"class":84,"line":113},[82,2954,2955],{"class":88},"# Needs psql and MODELS_DSN for the transformer role. Exits non-zero on any failure.\n",[82,2957,2958,2961,2964],{"class":84,"line":135},[82,2959,2960],{"class":161},"set",[82,2962,2963],{"class":161}," -euo",[82,2965,2966],{"class":125}," pipefail\n",[82,2968,2969,2972,2975,2978,2981,2984],{"class":84,"line":158},[82,2970,2971],{"class":161},"cd",[82,2973,2974],{"class":125}," \"$(",[82,2976,2977],{"class":102},"dirname",[82,2979,2980],{"class":125}," \"",[82,2982,2983],{"class":161},"$0",[82,2985,2986],{"class":125},"\")\"\n",[82,2988,2989],{"class":84,"line":188},[82,2990,726],{"emptyLinePlaceholder":725},[82,2992,2993,2996,2998],{"class":84,"line":212},[82,2994,2995],{"class":106},"args",[82,2997,1190],{"class":95},[82,2999,3000],{"class":106},"()\n",[82,3002,3003,3006,3009,3012,3015,3018],{"class":84,"line":336},[82,3004,3005],{"class":95},"for",[82,3007,3008],{"class":106}," model ",[82,3010,3011],{"class":95},"in",[82,3013,3014],{"class":125}," models\u002F*.sql",[82,3016,3017],{"class":106},"; ",[82,3019,3020],{"class":95},"do\n",[82,3022,3023,3026,3029,3031,3034,3036,3039,3042],{"class":84,"line":349},[82,3024,3025],{"class":106},"  args",[82,3027,3028],{"class":95},"+=",[82,3030,165],{"class":106},[82,3032,3033],{"class":125},"--file",[82,3035,2980],{"class":125},[82,3037,3038],{"class":106},"$model",[82,3040,3041],{"class":125},"\"",[82,3043,352],{"class":106},[82,3045,3046],{"class":84,"line":355},[82,3047,3048],{"class":95},"done\n",[82,3050,3051,3053,3055,3058,3060,3063,3066,3068,3071,3074,3077,3079,3082,3085],{"class":84,"line":362},[82,3052,56],{"class":102},[82,3054,2980],{"class":125},[82,3056,3057],{"class":106},"$MODELS_DSN",[82,3059,3041],{"class":125},[82,3061,3062],{"class":161}," --set",[82,3064,3065],{"class":125}," ON_ERROR_STOP=",[82,3067,867],{"class":161},[82,3069,3070],{"class":161}," --single-transaction",[82,3072,3073],{"class":161}," --quiet",[82,3075,3076],{"class":125}," \"${",[82,3078,2995],{"class":106},[82,3080,3081],{"class":125},"[",[82,3083,3084],{"class":95},"@",[82,3086,3087],{"class":125},"]}\"\n",[82,3089,3090,3093,3096,3099,3101,3103,3105],{"class":84,"line":387},[82,3091,3092],{"class":161},"echo",[82,3094,3095],{"class":125}," \"Applied ${",[82,3097,3098],{"class":95},"#",[82,3100,2995],{"class":106},[82,3102,3081],{"class":125},[82,3104,3084],{"class":95},[82,3106,3107],{"class":125},"]} model file(s).\"\n",[82,3109,3110],{"class":84,"line":408},[82,3111,726],{"emptyLinePlaceholder":725},[82,3113,3114,3117,3119],{"class":84,"line":445},[82,3115,3116],{"class":106},"failed",[82,3118,1190],{"class":95},[82,3120,3121],{"class":125},"0\n",[82,3123,3124,3126,3129,3131,3134,3136],{"class":84,"line":453},[82,3125,3005],{"class":95},[82,3127,3128],{"class":106}," test ",[82,3130,3011],{"class":95},[82,3132,3133],{"class":125}," tests\u002F*.sql",[82,3135,3017],{"class":106},[82,3137,3020],{"class":95},[82,3139,3140],{"class":84,"line":688},[82,3141,3142],{"class":88},"  # A psql error stops the script here (set -e); only grep's no-match status is tolerated.\n",[82,3144,3145,3148,3150,3153,3155,3157,3159,3161,3163,3165,3167,3170,3173,3176,3178,3181,3183],{"class":84,"line":722},[82,3146,3147],{"class":106},"  output",[82,3149,1190],{"class":95},[82,3151,3152],{"class":106},"$(",[82,3154,56],{"class":102},[82,3156,2980],{"class":125},[82,3158,3057],{"class":106},[82,3160,3041],{"class":125},[82,3162,3062],{"class":161},[82,3164,3065],{"class":125},[82,3166,867],{"class":161},[82,3168,3169],{"class":161}," --no-align",[82,3171,3172],{"class":161}," --tuples-only",[82,3174,3175],{"class":161}," --file",[82,3177,2980],{"class":125},[82,3179,3180],{"class":106},"$test",[82,3182,3041],{"class":125},[82,3184,352],{"class":106},[82,3186,3187,3190,3192,3194,3197,3200,3203,3206,3208,3211,3213,3216,3219],{"class":84,"line":729},[82,3188,3189],{"class":106},"  rows",[82,3191,1190],{"class":95},[82,3193,3152],{"class":106},[82,3195,3196],{"class":102},"grep",[82,3198,3199],{"class":161}," -c",[82,3201,3202],{"class":125}," .",[82,3204,3205],{"class":95}," \u003C\u003C\u003C",[82,3207,3041],{"class":125},[82,3209,3210],{"class":106},"$output",[82,3212,3041],{"class":125},[82,3214,3215],{"class":95}," ||",[82,3217,3218],{"class":161}," true",[82,3220,352],{"class":106},[82,3222,3223,3226,3229,3231,3234,3236,3239,3241,3244],{"class":84,"line":745},[82,3224,3225],{"class":95},"  if",[82,3227,3228],{"class":106}," [[ ",[82,3230,3041],{"class":125},[82,3232,3233],{"class":106},"$rows",[82,3235,3041],{"class":125},[82,3237,3238],{"class":95}," -gt",[82,3240,960],{"class":161},[82,3242,3243],{"class":106}," ]]; ",[82,3245,3246],{"class":95},"then\n",[82,3248,3249,3252,3255,3257,3259,3261],{"class":84,"line":751},[82,3250,3251],{"class":161},"    echo",[82,3253,3254],{"class":125}," \"FAIL ",[82,3256,3180],{"class":106},[82,3258,294],{"class":125},[82,3260,3233],{"class":106},[82,3262,3263],{"class":125}," row(s))\"\n",[82,3265,3266,3269,3271],{"class":84,"line":757},[82,3267,3268],{"class":106},"    failed",[82,3270,1190],{"class":95},[82,3272,3273],{"class":125},"1\n",[82,3275,3276],{"class":84,"line":1421},[82,3277,3278],{"class":95},"  else\n",[82,3280,3281,3283,3286,3288],{"class":84,"line":1435},[82,3282,3251],{"class":161},[82,3284,3285],{"class":125}," \"pass ",[82,3287,3180],{"class":106},[82,3289,3290],{"class":125},"\"\n",[82,3292,3293],{"class":84,"line":1454},[82,3294,3295],{"class":95},"  fi\n",[82,3297,3299],{"class":84,"line":3298},25,[82,3300,3048],{"class":95},[82,3302,3304,3307,3309,3312],{"class":84,"line":3303},26,[82,3305,3306],{"class":161},"exit",[82,3308,2980],{"class":125},[82,3310,3311],{"class":106},"$failed",[82,3313,3290],{"class":125},[11,3315,3316],{},"Run it from CI on every merge to the main branch. The pull request becomes the changelog.",[3318,3319,3321],"h3",{"id":3320},"the-same-models-in-dbt","The same models in dbt",[11,3323,3324,3325,3328,3329,3332],{},"If the model count grows, or someone on the team already knows dbt, the move is mostly mechanical. Each view becomes a ",[15,3326,3327],{},"select"," in its own file, table names become ",[15,3330,3331],{},"ref()"," calls so dbt can work out the build order, and the test files become YAML:",[73,3334,3336],{"className":75,"code":3335,"language":77,"meta":78,"style":78},"-- models\u002Fmarts\u002Ffct_ticket_resolution.sql\n{{ config(materialized='view') }}\n\nselect t.ticket_id,\n       t.client_id,\n       t.priority,\n       t.queue,\n       t.created_at,\n       l.last_resolved_at,\n       l.reopen_count,\n       l.last_resolved_at - t.created_at as time_to_resolve,\n       t.first_response_at - t.created_at as time_to_first_response\nfrom {{ ref('stg_tickets') }} t\njoin {{ ref('int_ticket_lifecycle') }} l using (ticket_id)\nwhere t.status in ('resolved', 'closed')\n  and t.queue not in ('spam', 'merged', 'internal-project')\n",[15,3337,3338,3343,3356,3360,3372,3382,3392,3402,3412,3422,3432,3453,3473,3487,3505,3529],{"__ignoreMap":78},[82,3339,3340],{"class":84,"line":85},[82,3341,3342],{"class":88},"-- models\u002Fmarts\u002Ffct_ticket_resolution.sql\n",[82,3344,3345,3348,3350,3353],{"class":84,"line":92},[82,3346,3347],{"class":106},"{{ config(materialized",[82,3349,1190],{"class":95},[82,3351,3352],{"class":125},"'view'",[82,3354,3355],{"class":106},") }}\n",[82,3357,3358],{"class":84,"line":113},[82,3359,726],{"emptyLinePlaceholder":725},[82,3361,3362,3364,3366,3368,3370],{"class":84,"line":135},[82,3363,3327],{"class":95},[82,3365,493],{"class":161},[82,3367,221],{"class":106},[82,3369,498],{"class":161},[82,3371,501],{"class":106},[82,3373,3374,3376,3378,3380],{"class":84,"line":158},[82,3375,506],{"class":161},[82,3377,221],{"class":106},[82,3379,511],{"class":161},[82,3381,501],{"class":106},[82,3383,3384,3386,3388,3390],{"class":84,"line":188},[82,3385,506],{"class":161},[82,3387,221],{"class":106},[82,3389,522],{"class":161},[82,3391,501],{"class":106},[82,3393,3394,3396,3398,3400],{"class":84,"line":212},[82,3395,506],{"class":161},[82,3397,221],{"class":106},[82,3399,533],{"class":161},[82,3401,501],{"class":106},[82,3403,3404,3406,3408,3410],{"class":84,"line":336},[82,3405,506],{"class":161},[82,3407,221],{"class":106},[82,3409,544],{"class":161},[82,3411,501],{"class":106},[82,3413,3414,3416,3418,3420],{"class":84,"line":349},[82,3415,551],{"class":161},[82,3417,221],{"class":106},[82,3419,567],{"class":161},[82,3421,501],{"class":106},[82,3423,3424,3426,3428,3430],{"class":84,"line":355},[82,3425,551],{"class":161},[82,3427,221],{"class":106},[82,3429,578],{"class":161},[82,3431,501],{"class":106},[82,3433,3434,3436,3438,3440,3442,3444,3446,3448,3451],{"class":84,"line":362},[82,3435,551],{"class":161},[82,3437,221],{"class":106},[82,3439,567],{"class":161},[82,3441,591],{"class":95},[82,3443,493],{"class":161},[82,3445,221],{"class":106},[82,3447,544],{"class":161},[82,3449,3450],{"class":95}," as",[82,3452,603],{"class":106},[82,3454,3455,3457,3459,3461,3463,3465,3467,3469,3471],{"class":84,"line":387},[82,3456,506],{"class":161},[82,3458,221],{"class":106},[82,3460,612],{"class":161},[82,3462,591],{"class":95},[82,3464,493],{"class":161},[82,3466,221],{"class":106},[82,3468,544],{"class":161},[82,3470,3450],{"class":95},[82,3472,626],{"class":106},[82,3474,3475,3478,3481,3484],{"class":84,"line":408},[82,3476,3477],{"class":95},"from",[82,3479,3480],{"class":106}," {{ ref(",[82,3482,3483],{"class":125},"'stg_tickets'",[82,3485,3486],{"class":106},") }} t\n",[82,3488,3489,3492,3494,3497,3500,3503],{"class":84,"line":445},[82,3490,3491],{"class":95},"join",[82,3493,3480],{"class":106},[82,3495,3496],{"class":125},"'int_ticket_lifecycle'",[82,3498,3499],{"class":106},") }} l ",[82,3501,3502],{"class":95},"using",[82,3504,661],{"class":106},[82,3506,3507,3510,3512,3514,3516,3519,3521,3523,3525,3527],{"class":84,"line":453},[82,3508,3509],{"class":95},"where",[82,3511,493],{"class":161},[82,3513,221],{"class":106},[82,3515,672],{"class":161},[82,3517,3518],{"class":95}," in",[82,3520,294],{"class":106},[82,3522,297],{"class":125},[82,3524,300],{"class":106},[82,3526,303],{"class":125},[82,3528,352],{"class":106},[82,3530,3531,3534,3536,3538,3540,3543,3545,3547,3549,3551,3553,3555,3557],{"class":84,"line":688},[82,3532,3533],{"class":95},"  and",[82,3535,493],{"class":161},[82,3537,221],{"class":106},[82,3539,533],{"class":161},[82,3541,3542],{"class":95}," not",[82,3544,3518],{"class":95},[82,3546,294],{"class":106},[82,3548,706],{"class":125},[82,3550,300],{"class":106},[82,3552,711],{"class":125},[82,3554,300],{"class":106},[82,3556,716],{"class":125},[82,3558,352],{"class":106},[73,3560,3564],{"className":3561,"code":3562,"language":3563,"meta":78,"style":78},"language-yaml shiki shiki-themes github-dark","# models\u002Fmarts\u002Fschema.yml\nmodels:\n  - name: fct_ticket_resolution\n    description: >\n      One row per resolved ticket. Resolution time runs to the final resolution,\n      so reopened tickets count once. Excludes spam, merged, and internal project queues.\n    columns:\n      - name: ticket_id\n        data_tests:\n          - unique\n          - not_null\n      - name: priority\n        data_tests:\n          - accepted_values:\n              arguments:\n                values: ['p1', 'p2', 'p3', 'p4']\n","yaml",[15,3565,3566,3571,3580,3593,3603,3608,3613,3620,3632,3639,3647,3654,3664,3670,3679,3686],{"__ignoreMap":78},[82,3567,3568],{"class":84,"line":85},[82,3569,3570],{"class":88},"# models\u002Fmarts\u002Fschema.yml\n",[82,3572,3573,3577],{"class":84,"line":92},[82,3574,3576],{"class":3575},"s4JwU","models",[82,3578,3579],{"class":106},":\n",[82,3581,3582,3585,3588,3591],{"class":84,"line":113},[82,3583,3584],{"class":106},"  - ",[82,3586,3587],{"class":3575},"name",[82,3589,3590],{"class":106},": ",[82,3592,979],{"class":125},[82,3594,3595,3598,3600],{"class":84,"line":135},[82,3596,3597],{"class":3575},"    description",[82,3599,3590],{"class":106},[82,3601,3602],{"class":95},">\n",[82,3604,3605],{"class":84,"line":158},[82,3606,3607],{"class":125},"      One row per resolved ticket. Resolution time runs to the final resolution,\n",[82,3609,3610],{"class":84,"line":188},[82,3611,3612],{"class":125},"      so reopened tickets count once. Excludes spam, merged, and internal project queues.\n",[82,3614,3615,3618],{"class":84,"line":212},[82,3616,3617],{"class":3575},"    columns",[82,3619,3579],{"class":106},[82,3621,3622,3625,3627,3629],{"class":84,"line":336},[82,3623,3624],{"class":106},"      - ",[82,3626,3587],{"class":3575},[82,3628,3590],{"class":106},[82,3630,3631],{"class":125},"ticket_id\n",[82,3633,3634,3637],{"class":84,"line":349},[82,3635,3636],{"class":3575},"        data_tests",[82,3638,3579],{"class":106},[82,3640,3641,3644],{"class":84,"line":355},[82,3642,3643],{"class":106},"          - ",[82,3645,3646],{"class":125},"unique\n",[82,3648,3649,3651],{"class":84,"line":362},[82,3650,3643],{"class":106},[82,3652,3653],{"class":125},"not_null\n",[82,3655,3656,3658,3660,3662],{"class":84,"line":387},[82,3657,3624],{"class":106},[82,3659,3587],{"class":3575},[82,3661,3590],{"class":106},[82,3663,2340],{"class":125},[82,3665,3666,3668],{"class":84,"line":408},[82,3667,3636],{"class":3575},[82,3669,3579],{"class":106},[82,3671,3672,3674,3677],{"class":84,"line":445},[82,3673,3643],{"class":106},[82,3675,3676],{"class":3575},"accepted_values",[82,3678,3579],{"class":106},[82,3680,3681,3684],{"class":84,"line":453},[82,3682,3683],{"class":3575},"              arguments",[82,3685,3579],{"class":106},[82,3687,3688,3691,3694,3696,3698,3700,3702,3704,3706,3708],{"class":84,"line":688},[82,3689,3690],{"class":3575},"                values",[82,3692,3693],{"class":106},": [",[82,3695,1095],{"class":125},[82,3697,300],{"class":106},[82,3699,1116],{"class":125},[82,3701,300],{"class":106},[82,3703,1136],{"class":125},[82,3705,300],{"class":106},[82,3707,1155],{"class":125},[82,3709,3710],{"class":106},"]\n",[11,3712,3713,3716,3717,3720,3721,3724,3725,3728,3729,3732,3733,221],{},[15,3714,3715],{},"data_tests:"," is the current key (",[15,3718,3719],{},"tests:"," still works as an alias), and nesting inputs under ",[15,3722,3723],{},"arguments:"," applies from dbt 1.10.5. On older versions, put ",[15,3726,3727],{},"values"," directly under the test name. ",[15,3730,3731],{},"dbt build"," then runs models and tests in dependency order, which replaces ",[15,3734,3735],{},"deploy.sh",[40,3737,3739],{"id":3738},"reusable-doesnt-mean-over-engineered","Reusable doesn't mean over-engineered",[11,3741,3742],{},"The temptation once this pattern clicks is to model everything, including metrics nobody has asked for twice. My rule of thumb: a query becomes a model the second time it's needed, not before. The first time, a one-off is fine. That keeps the model library focused on metrics that have proven they'll be asked about again, instead of piling up unused abstraction that's harder to maintain than the duplicated queries it was meant to replace.",[40,3744,3746],{"id":3745},"references","References",[3748,3749,3750,3759,3766,3773,3780,3787,3794,3801],"ul",{},[3751,3752,3753],"li",{},[34,3754,3758],{"href":3755,"rel":3756},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ffunctions-aggregate.html",[3757],"nofollow","PostgreSQL: aggregate functions, including percentile_cont",[3751,3760,3761],{},[34,3762,3765],{"href":3763,"rel":3764},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-expressions.html",[3757],"PostgreSQL: aggregate expressions and the FILTER clause",[3751,3767,3768],{},[34,3769,3772],{"href":3770,"rel":3771},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fxfunc-sql.html",[3757],"PostgreSQL: SQL functions returning sets (RETURNS TABLE)",[3751,3774,3775],{},[34,3776,3779],{"href":3777,"rel":3778},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-refreshmaterializedview.html",[3757],"PostgreSQL: REFRESH MATERIALIZED VIEW",[3751,3781,3782],{},[34,3783,3786],{"href":3784,"rel":3785},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-comment.html",[3757],"PostgreSQL: COMMENT",[3751,3788,3789],{},[34,3790,3793],{"href":3791,"rel":3792},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fapp-psql.html",[3757],"PostgreSQL: psql (--single-transaction, ON_ERROR_STOP)",[3751,3795,3796],{},[34,3797,3800],{"href":3798,"rel":3799},"https:\u002F\u002Fdocs.getdbt.com\u002Fdocs\u002Fbuild\u002Fdata-tests",[3757],"dbt: data tests",[3751,3802,3803],{},[34,3804,3807],{"href":3805,"rel":3806},"https:\u002F\u002Fdocs.getdbt.com\u002Fdocs\u002Fbuild\u002Fmaterializations",[3757],"dbt: materializations",[3809,3810,3811],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":78,"searchDepth":92,"depth":92,"links":3813},[3814,3815,3816,3817,3818,3821,3822],{"id":42,"depth":92,"text":43},{"id":64,"depth":92,"text":65},{"id":2030,"depth":92,"text":2031},{"id":2428,"depth":92,"text":2429},{"id":2751,"depth":92,"text":2752,"children":3819},[3820],{"id":3320,"depth":113,"text":3321},{"id":3738,"depth":92,"text":3739},{"id":3745,"depth":92,"text":3746},"techcolumnist",[3825],"engineering","2026-08-19T14:00:00Z","Layered, version-controlled SQL models for MTTR, ticket aging, SLA compliance, and patch compliance, with plain-SQL tests and the dbt equivalent.","md",false,null,{},"\u002Fblog\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics",{"title":6,"description":3827},[3823],"blog\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics",[3837,3838,77],"data-warehouse","reporting","\u002F2026\u002F08\u002F19\u002Fdata-warehouse-reusable-sql-models-for-it-operations-metrics\u002F","xIs3AgJeASvVi1QICXFUlXOyn1ZwTsuZOynzFyGCwy8",{"title":3842,"description":3843,"date":3844,"url":3845,"categories":3846,"tags":3847,"image":3830,"readingTime":212,"canonical":3823,"sites":3850,"series":3830,"seriesOrder":3830},"Talos Linux: Upgrading a Cluster Without a Maintenance Window","Rolling Talos and Kubernetes upgrades through a cluster one node at a time: preflight checks, etcd snapshots, Image Factory installers, and scripts.","2026-08-26T14:00:00Z","\u002F2026\u002F08\u002F26\u002Ftalos-linux-upgrading-a-cluster-without-a-maintenance-window\u002F",[3825],[3848,3849],"talos","kubernetes",[3823],{"title":3852,"description":3853,"date":3854,"url":3855,"categories":3856,"tags":3858,"image":3830,"readingTime":212,"canonical":3823,"sites":3860,"series":3830,"seriesOrder":3830},"Azure: Cost Governance Without Killing Developer Velocity","Azure cost governance that catches waste without approval gates: built-in policy guardrails, budget alerts as code, and scheduled cleanup, with the scripts.","2026-08-12T14:00:00Z","\u002F2026\u002F08\u002F12\u002Fazure-cost-governance-without-killing-developer-velocity\u002F",[3825,3857],"strategy",[3859],"azure",[3823],[3862,3870,3878],{"title":3863,"description":3864,"date":3865,"url":36,"categories":3866,"tags":3867,"image":3830,"readingTime":188,"canonical":3823,"sites":3869,"series":3830,"seriesOrder":3830},"Data Warehouse: Building a Reporting Layer Without a BI Team","A practical build for an IT operations reporting layer with no BI team: one Postgres database, raw\u002Fstaging\u002Fmarts schemas, an API loader script, and freshness checks.","2026-05-20T14:00:00Z",[3825,3857],[3837,3838,77,3868],"python",[3823],{"title":3871,"description":3872,"date":3873,"url":3874,"categories":3875,"tags":3876,"image":3830,"readingTime":336,"canonical":3823,"sites":3877,"series":3830,"seriesOrder":3830},"Data Warehouse: When to Stop Querying Production and Build a Warehouse","How to measure reporting load on a production Postgres database, the cheap fixes to try first, and the signals that mean a separate warehouse is finally justified.","2026-05-27T14:00:00Z","\u002F2026\u002F05\u002F27\u002Fdata-warehouse-when-to-stop-querying-production-and-build-a-warehouse\u002F",[3825,3857],[3837,77,3868],[3823],{"title":3879,"description":3880,"date":3881,"url":3882,"categories":3883,"tags":3885,"image":3830,"readingTime":158,"canonical":3823,"sites":3888,"series":3830,"seriesOrder":3830},"Node.js: Data Warehouse – Streaming ETL Rows Without Buffering Them","A Node.js script that streams Postgres rows through a cursor, a transform and COPY FROM STDIN with backpressure, so memory tracks batch size, not table size.","2026-02-25T14:00:00Z","\u002F2026\u002F02\u002F25\u002Fnode-js-data-warehouse-streaming-etl-rows-without-loading-them-all-in-memory\u002F",[3884,3825],"scripts",[3886,3837,3887,77],"nodejs","etl",[3823],{"doc":3830,"posts":3890},[],1790052514093]