[{"data":1,"prerenderedAt":4087},["ShallowReactive",2],{"page-\u002Fsql\u002F16-views-and-materialized-views":3},{"id":4,"title":5,"body":6,"description":16,"extension":4081,"meta":4082,"navigation":120,"path":4083,"seo":4084,"stem":4085,"__hash__":4086},"content\u002Fsql\u002F16-views-and-materialized-views.md","16 — Views & Materialized Views",{"type":7,"value":8,"toc":4058},"minimark",[9,13,17,20,25,33,217,224,229,257,411,414,416,420,438,601,615,619,627,764,768,883,890,894,1009,1020,1265,1267,1271,1281,1477,1481,1586,1642,1646,1659,1719,1728,1730,1734,1737,2787,2793,2795,2799,3062,3068,3070,3074,3243,3245,3249,3342,3344,3348,3425,3604,3606,3610,3697,3699,3703,3853,3855,3859,3865,3927,4040,4042,4045,4054],[10,11,5],"h1",{"id":12},"_16-views-materialized-views",[14,15,16],"p",{},"Views are saved queries that act as virtual tables — they encapsulate complexity, enforce consistency, and control access. Materialized views persist the query result, trading freshness for speed. Both are essential tools in a production schema, but they have fundamentally different storage, refresh, and concurrency semantics.",[18,19],"hr",{},[21,22,24],"h2",{"id":23},"regular-views-stored-query-no-storage","Regular Views: Stored Query, No Storage",[14,26,27,28,32],{},"A view is a ",[29,30,31],"strong",{},"named, stored query"," — it holds no data of its own. When you query a view, the planner macro-expands it into its defining query, then optimizes the whole thing. Predicates from the outer query are pushed into the view's scan:",[34,35,37],"code-wrapper",{"language":36},"sql",[38,39,43],"pre",{"className":40,"code":41,"language":36,"meta":42,"style":42},"language-sql shiki shiki-themes github-light github-dark","-- Define a view: a named, reusable query definition\nCREATE VIEW active_customers AS\n  SELECT id, name, city, active\n  FROM customers\n  WHERE active = true;          -- WHERE clause becomes part of every query through the view\n\n-- Querying the view is transparent — the planner expands it:\n--   SELECT id, name, city, active FROM customers WHERE active = true AND city = 'NYC'\nSELECT * FROM active_customers WHERE city = 'NYC';\n\n-- EXPLAIN shows the base table scan with both predicates pushed down\nEXPLAIN SELECT * FROM active_customers WHERE city = 'NYC';\n--  Index Scan on customers_city_idx\n--    Index Cond: (city = 'NYC'::text)\n--    Filter: (active = true)\n","",[44,45,46,55,72,88,97,115,122,128,134,164,169,175,199,205,211],"code",{"__ignoreMap":42},[47,48,51],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sdCPZ","-- Define a view: a named, reusable query definition\n",[47,56,58,62,65,69],{"class":49,"line":57},2,[47,59,61],{"class":60},"svdQ7","CREATE",[47,63,64],{"class":60}," VIEW",[47,66,68],{"class":67},"sIsaT"," active_customers",[47,70,71],{"class":60}," AS\n",[47,73,75,78,82,85],{"class":49,"line":74},3,[47,76,77],{"class":60},"  SELECT",[47,79,81],{"class":80},"ssxIu"," id, ",[47,83,84],{"class":60},"name",[47,86,87],{"class":80},", city, active\n",[47,89,91,94],{"class":49,"line":90},4,[47,92,93],{"class":60},"  FROM",[47,95,96],{"class":80}," customers\n",[47,98,100,103,106,109,112],{"class":49,"line":99},5,[47,101,102],{"class":60},"  WHERE",[47,104,105],{"class":80}," active ",[47,107,108],{"class":60},"=",[47,110,111],{"class":80}," true;          ",[47,113,114],{"class":53},"-- WHERE clause becomes part of every query through the view\n",[47,116,118],{"class":49,"line":117},6,[47,119,121],{"emptyLinePlaceholder":120},true,"\n",[47,123,125],{"class":49,"line":124},7,[47,126,127],{"class":53},"-- Querying the view is transparent — the planner expands it:\n",[47,129,131],{"class":49,"line":130},8,[47,132,133],{"class":53},"--   SELECT id, name, city, active FROM customers WHERE active = true AND city = 'NYC'\n",[47,135,137,140,143,146,149,152,155,157,161],{"class":49,"line":136},9,[47,138,139],{"class":60},"SELECT",[47,141,142],{"class":60}," *",[47,144,145],{"class":60}," FROM",[47,147,148],{"class":80}," active_customers ",[47,150,151],{"class":60},"WHERE",[47,153,154],{"class":80}," city ",[47,156,108],{"class":60},[47,158,160],{"class":159},"sJ6F3"," 'NYC'",[47,162,163],{"class":80},";\n",[47,165,167],{"class":49,"line":166},10,[47,168,121],{"emptyLinePlaceholder":120},[47,170,172],{"class":49,"line":171},11,[47,173,174],{"class":53},"-- EXPLAIN shows the base table scan with both predicates pushed down\n",[47,176,178,181,183,185,187,189,191,193,195,197],{"class":49,"line":177},12,[47,179,180],{"class":80},"EXPLAIN ",[47,182,139],{"class":60},[47,184,142],{"class":60},[47,186,145],{"class":60},[47,188,148],{"class":80},[47,190,151],{"class":60},[47,192,154],{"class":80},[47,194,108],{"class":60},[47,196,160],{"class":159},[47,198,163],{"class":80},[47,200,202],{"class":49,"line":201},13,[47,203,204],{"class":53},"--  Index Scan on customers_city_idx\n",[47,206,208],{"class":49,"line":207},14,[47,209,210],{"class":53},"--    Index Cond: (city = 'NYC'::text)\n",[47,212,214],{"class":49,"line":213},15,[47,215,216],{"class":53},"--    Filter: (active = true)\n",[14,218,219,220,223],{},"The view definition is stored in ",[44,221,222],{},"pg_views"," — the query text, not data. The planner re-optimizes every time, so a view's performance depends on the underlying tables' indexes and the query's complexity.",[225,226,228],"h3",{"id":227},"why-use-views","Why Use Views?",[230,231,232,239,245,251],"ol",{},[233,234,235,238],"li",{},[29,236,237],{},"Encapsulation"," — hide a 20-line join behind a single name.",[233,240,241,244],{},[29,242,243],{},"Consistency"," — one definition of \"active customer\" used everywhere, updated in one place.",[233,246,247,250],{},[29,248,249],{},"Security"," — grant access to a view (subset of columns\u002Frows) without granting access to the base table.",[233,252,253,256],{},[29,254,255],{},"API stability"," — present a stable schema even as underlying tables evolve (add a column, keep the view unchanged).",[34,258,259],{"language":36},[38,260,262],{"className":40,"code":261,"language":36,"meta":42,"style":42},"-- Security view: hide the salary column from non-HR users\nCREATE VIEW employee_directory AS\n  SELECT id, name, department       -- salary intentionally excluded\n  FROM employees;\n\nGRANT SELECT ON employee_directory TO public;   -- public sees the view\nREVOKE SELECT ON employees FROM public;          -- but not the base table\n\n-- Row-level view: per-tenant isolation via a security function\nCREATE VIEW my_orders AS\n  SELECT * FROM orders\n  WHERE customer_id = current_user_id();         -- custom function returns the caller's tenant ID\nGRANT SELECT ON my_orders TO public;\n",[44,263,264,269,280,294,301,305,328,349,353,358,369,380,395],{"__ignoreMap":42},[47,265,266],{"class":49,"line":50},[47,267,268],{"class":53},"-- Security view: hide the salary column from non-HR users\n",[47,270,271,273,275,278],{"class":49,"line":57},[47,272,61],{"class":60},[47,274,64],{"class":60},[47,276,277],{"class":67}," employee_directory",[47,279,71],{"class":60},[47,281,282,284,286,288,291],{"class":49,"line":74},[47,283,77],{"class":60},[47,285,81],{"class":80},[47,287,84],{"class":60},[47,289,290],{"class":80},", department       ",[47,292,293],{"class":53},"-- salary intentionally excluded\n",[47,295,296,298],{"class":49,"line":90},[47,297,93],{"class":60},[47,299,300],{"class":80}," employees;\n",[47,302,303],{"class":49,"line":99},[47,304,121],{"emptyLinePlaceholder":120},[47,306,307,310,313,316,319,322,325],{"class":49,"line":117},[47,308,309],{"class":60},"GRANT",[47,311,312],{"class":60}," SELECT",[47,314,315],{"class":60}," ON",[47,317,318],{"class":80}," employee_directory ",[47,320,321],{"class":60},"TO",[47,323,324],{"class":80}," public;   ",[47,326,327],{"class":53},"-- public sees the view\n",[47,329,330,333,335,337,340,343,346],{"class":49,"line":124},[47,331,332],{"class":60},"REVOKE",[47,334,312],{"class":60},[47,336,315],{"class":60},[47,338,339],{"class":80}," employees ",[47,341,342],{"class":60},"FROM",[47,344,345],{"class":80}," public;          ",[47,347,348],{"class":53},"-- but not the base table\n",[47,350,351],{"class":49,"line":130},[47,352,121],{"emptyLinePlaceholder":120},[47,354,355],{"class":49,"line":136},[47,356,357],{"class":53},"-- Row-level view: per-tenant isolation via a security function\n",[47,359,360,362,364,367],{"class":49,"line":166},[47,361,61],{"class":60},[47,363,64],{"class":60},[47,365,366],{"class":67}," my_orders",[47,368,71],{"class":60},[47,370,371,373,375,377],{"class":49,"line":171},[47,372,77],{"class":60},[47,374,142],{"class":60},[47,376,145],{"class":60},[47,378,379],{"class":80}," orders\n",[47,381,382,384,387,389,392],{"class":49,"line":177},[47,383,102],{"class":60},[47,385,386],{"class":80}," customer_id ",[47,388,108],{"class":60},[47,390,391],{"class":80}," current_user_id();         ",[47,393,394],{"class":53},"-- custom function returns the caller's tenant ID\n",[47,396,397,399,401,403,406,408],{"class":49,"line":201},[47,398,309],{"class":60},[47,400,312],{"class":60},[47,402,315],{"class":60},[47,404,405],{"class":80}," my_orders ",[47,407,321],{"class":60},[47,409,410],{"class":80}," public;\n",[14,412,413],{},"Views are the classic way to expose a limited projection of a table to less-privileged roles — simpler to reason about than per-column grants, and they survive base-table column renames better.",[18,415],{},[21,417,419],{"id":418},"updatable-views","Updatable Views",[14,421,422,423,426,427,430,431,430,434,437],{},"Simple views (single base table, no aggregation\u002Fdistinct\u002Fgrouping\u002Fjoin\u002Fwindow) are ",[29,424,425],{},"automatically updatable"," in PostgreSQL — ",[44,428,429],{},"INSERT","\u002F",[44,432,433],{},"UPDATE",[44,435,436],{},"DELETE"," on the view propagate to the base table:",[34,439,440],{"language":36},[38,441,443],{"className":40,"code":442,"language":36,"meta":42,"style":42},"CREATE VIEW active_customers AS\n  SELECT id, name, city, active\n  FROM customers\n  WHERE active = true;          -- filter: only active rows are visible\n\n-- INSERT through the view → inserts into the base table\nINSERT INTO active_customers (name, city) VALUES ('Eve', 'LA');\n-- INSERT INTO customers (name, city, active) VALUES ('Eve', 'LA', true)\n-- Note: the view's WHERE active = true is NOT enforced on insert — only on reads\n\n-- UPDATE through the view → updates the base table, only visible rows\nUPDATE active_customers SET city = 'SF' WHERE id = 2;\n\n-- DELETE through the view → deletes from the base table, only visible rows\nDELETE FROM active_customers WHERE id = 3;\n",[44,444,445,455,465,471,484,488,493,524,529,534,538,543,573,577,582],{"__ignoreMap":42},[47,446,447,449,451,453],{"class":49,"line":50},[47,448,61],{"class":60},[47,450,64],{"class":60},[47,452,68],{"class":67},[47,454,71],{"class":60},[47,456,457,459,461,463],{"class":49,"line":57},[47,458,77],{"class":60},[47,460,81],{"class":80},[47,462,84],{"class":60},[47,464,87],{"class":80},[47,466,467,469],{"class":49,"line":74},[47,468,93],{"class":60},[47,470,96],{"class":80},[47,472,473,475,477,479,481],{"class":49,"line":90},[47,474,102],{"class":60},[47,476,105],{"class":80},[47,478,108],{"class":60},[47,480,111],{"class":80},[47,482,483],{"class":53},"-- filter: only active rows are visible\n",[47,485,486],{"class":49,"line":99},[47,487,121],{"emptyLinePlaceholder":120},[47,489,490],{"class":49,"line":117},[47,491,492],{"class":53},"-- INSERT through the view → inserts into the base table\n",[47,494,495,498,501,503,506,509,512,515,518,521],{"class":49,"line":124},[47,496,497],{"class":60},"INSERT INTO",[47,499,500],{"class":80}," active_customers (",[47,502,84],{"class":60},[47,504,505],{"class":80},", city) ",[47,507,508],{"class":60},"VALUES",[47,510,511],{"class":80}," (",[47,513,514],{"class":159},"'Eve'",[47,516,517],{"class":80},", ",[47,519,520],{"class":159},"'LA'",[47,522,523],{"class":80},");\n",[47,525,526],{"class":49,"line":130},[47,527,528],{"class":53},"-- INSERT INTO customers (name, city, active) VALUES ('Eve', 'LA', true)\n",[47,530,531],{"class":49,"line":136},[47,532,533],{"class":53},"-- Note: the view's WHERE active = true is NOT enforced on insert — only on reads\n",[47,535,536],{"class":49,"line":166},[47,537,121],{"emptyLinePlaceholder":120},[47,539,540],{"class":49,"line":171},[47,541,542],{"class":53},"-- UPDATE through the view → updates the base table, only visible rows\n",[47,544,545,547,549,552,554,556,559,562,565,567,571],{"class":49,"line":177},[47,546,433],{"class":60},[47,548,148],{"class":80},[47,550,551],{"class":60},"SET",[47,553,154],{"class":80},[47,555,108],{"class":60},[47,557,558],{"class":159}," 'SF'",[47,560,561],{"class":60}," WHERE",[47,563,564],{"class":80}," id ",[47,566,108],{"class":60},[47,568,570],{"class":569},"snvgF"," 2",[47,572,163],{"class":80},[47,574,575],{"class":49,"line":201},[47,576,121],{"emptyLinePlaceholder":120},[47,578,579],{"class":49,"line":207},[47,580,581],{"class":53},"-- DELETE through the view → deletes from the base table, only visible rows\n",[47,583,584,586,588,590,592,594,596,599],{"class":49,"line":213},[47,585,436],{"class":60},[47,587,145],{"class":60},[47,589,148],{"class":80},[47,591,151],{"class":60},[47,593,564],{"class":80},[47,595,108],{"class":60},[47,597,598],{"class":569}," 3",[47,600,163],{"class":80},[14,602,603,606,607,610,611,614],{},[29,604,605],{},"The gap",": the view's ",[44,608,609],{},"WHERE active = true"," is a read filter. Inserting a row with ",[44,612,613],{},"active = false"," through the view succeeds — the row lands in the base table, but it's invisible to the view. This is a subtle authorization hole: a user inserts through a view, the row is in the base table but not the view, the user can't see it, but it exists.",[225,616,618],{"id":617},"with-check-option","WITH CHECK OPTION",[14,620,621,623,624,626],{},[44,622,618],{}," closes that gap — it enforces that inserts\u002Fupdates must satisfy the view's ",[44,625,151],{},":",[34,628,629],{"language":36},[38,630,632],{"className":40,"code":631,"language":36,"meta":42,"style":42},"CREATE VIEW active_customers AS\n  SELECT id, name, city, active\n  FROM customers\n  WHERE active = true\n  WITH CHECK OPTION;            -- inserts\u002Fupdates must satisfy WHERE active = true\n\n-- This now fails — the row wouldn't be visible in the view\nINSERT INTO active_customers (name, city, active) VALUES ('Eve', 'LA', false);\n-- ERROR: new row violates check option for view \"active_customers\"\n\n-- This also fails — updating a visible row to become invisible\nUPDATE active_customers SET active = false WHERE id = 2;\n-- ERROR: new row violates check option for view \"active_customers\"\n",[44,633,634,644,654,660,671,688,692,697,721,726,730,735,760],{"__ignoreMap":42},[47,635,636,638,640,642],{"class":49,"line":50},[47,637,61],{"class":60},[47,639,64],{"class":60},[47,641,68],{"class":67},[47,643,71],{"class":60},[47,645,646,648,650,652],{"class":49,"line":57},[47,647,77],{"class":60},[47,649,81],{"class":80},[47,651,84],{"class":60},[47,653,87],{"class":80},[47,655,656,658],{"class":49,"line":74},[47,657,93],{"class":60},[47,659,96],{"class":80},[47,661,662,664,666,668],{"class":49,"line":90},[47,663,102],{"class":60},[47,665,105],{"class":80},[47,667,108],{"class":60},[47,669,670],{"class":80}," true\n",[47,672,673,676,679,682,685],{"class":49,"line":99},[47,674,675],{"class":60},"  WITH",[47,677,678],{"class":60}," CHECK",[47,680,681],{"class":60}," OPTION",[47,683,684],{"class":80},";            ",[47,686,687],{"class":53},"-- inserts\u002Fupdates must satisfy WHERE active = true\n",[47,689,690],{"class":49,"line":117},[47,691,121],{"emptyLinePlaceholder":120},[47,693,694],{"class":49,"line":124},[47,695,696],{"class":53},"-- This now fails — the row wouldn't be visible in the view\n",[47,698,699,701,703,705,708,710,712,714,716,718],{"class":49,"line":130},[47,700,497],{"class":60},[47,702,500],{"class":80},[47,704,84],{"class":60},[47,706,707],{"class":80},", city, active) ",[47,709,508],{"class":60},[47,711,511],{"class":80},[47,713,514],{"class":159},[47,715,517],{"class":80},[47,717,520],{"class":159},[47,719,720],{"class":80},", false);\n",[47,722,723],{"class":49,"line":136},[47,724,725],{"class":53},"-- ERROR: new row violates check option for view \"active_customers\"\n",[47,727,728],{"class":49,"line":166},[47,729,121],{"emptyLinePlaceholder":120},[47,731,732],{"class":49,"line":171},[47,733,734],{"class":53},"-- This also fails — updating a visible row to become invisible\n",[47,736,737,739,741,743,745,747,750,752,754,756,758],{"class":49,"line":177},[47,738,433],{"class":60},[47,740,148],{"class":80},[47,742,551],{"class":60},[47,744,105],{"class":80},[47,746,108],{"class":60},[47,748,749],{"class":80}," false ",[47,751,151],{"class":60},[47,753,564],{"class":80},[47,755,108],{"class":60},[47,757,570],{"class":569},[47,759,163],{"class":80},[47,761,762],{"class":49,"line":201},[47,763,725],{"class":53},[225,765,767],{"id":766},"local-vs-cascaded-check-scope","LOCAL vs CASCADED check scope",[34,769,770],{"language":36},[38,771,773],{"className":40,"code":772,"language":36,"meta":42,"style":42},"-- CASCADED (default): checks this view's WHERE AND all underlying views' WHERE\nCREATE VIEW vip_active_customers AS\n  SELECT * FROM active_customers WHERE city = 'NYC'\n  WITH CASCADED CHECK OPTION;   -- must satisfy active = true AND city = 'NYC'\n\n-- LOCAL: checks only this view's WHERE, not underlying views\nCREATE VIEW vip_nyc_local AS\n  SELECT * FROM active_customers WHERE city = 'NYC'\n  WITH LOCAL CHECK OPTION;      -- must satisfy city = 'NYC' (but not active = true)\n",[44,774,775,780,791,810,828,832,837,848,866],{"__ignoreMap":42},[47,776,777],{"class":49,"line":50},[47,778,779],{"class":53},"-- CASCADED (default): checks this view's WHERE AND all underlying views' WHERE\n",[47,781,782,784,786,789],{"class":49,"line":57},[47,783,61],{"class":60},[47,785,64],{"class":60},[47,787,788],{"class":67}," vip_active_customers",[47,790,71],{"class":60},[47,792,793,795,797,799,801,803,805,807],{"class":49,"line":74},[47,794,77],{"class":60},[47,796,142],{"class":60},[47,798,145],{"class":60},[47,800,148],{"class":80},[47,802,151],{"class":60},[47,804,154],{"class":80},[47,806,108],{"class":60},[47,808,809],{"class":159}," 'NYC'\n",[47,811,812,814,817,820,822,825],{"class":49,"line":90},[47,813,675],{"class":60},[47,815,816],{"class":80}," CASCADED ",[47,818,819],{"class":60},"CHECK",[47,821,681],{"class":60},[47,823,824],{"class":80},";   ",[47,826,827],{"class":53},"-- must satisfy active = true AND city = 'NYC'\n",[47,829,830],{"class":49,"line":99},[47,831,121],{"emptyLinePlaceholder":120},[47,833,834],{"class":49,"line":117},[47,835,836],{"class":53},"-- LOCAL: checks only this view's WHERE, not underlying views\n",[47,838,839,841,843,846],{"class":49,"line":124},[47,840,61],{"class":60},[47,842,64],{"class":60},[47,844,845],{"class":67}," vip_nyc_local",[47,847,71],{"class":60},[47,849,850,852,854,856,858,860,862,864],{"class":49,"line":130},[47,851,77],{"class":60},[47,853,142],{"class":60},[47,855,145],{"class":60},[47,857,148],{"class":80},[47,859,151],{"class":60},[47,861,154],{"class":80},[47,863,108],{"class":60},[47,865,809],{"class":159},[47,867,868,870,873,875,877,880],{"class":49,"line":136},[47,869,675],{"class":60},[47,871,872],{"class":60}," LOCAL",[47,874,678],{"class":60},[47,876,681],{"class":60},[47,878,879],{"class":80},";      ",[47,881,882],{"class":53},"-- must satisfy city = 'NYC' (but not active = true)\n",[14,884,885,886,889],{},"Use ",[44,887,888],{},"CASCADED"," for layered security views — it ensures all upstream filters are enforced.",[225,891,893],{"id":892},"what-makes-a-view-non-updatable","What Makes a View Non-Updatable?",[895,896,897,913],"table",{},[898,899,900],"thead",{},[901,902,903,907,910],"tr",{},[904,905,906],"th",{},"Construct",[904,908,909],{},"Updatable?",[904,911,912],{},"Why",[914,915,916,928,941,954,964,980,994],"tbody",{},[901,917,918,922,925],{},[919,920,921],"td",{},"Single base table, simple SELECT",[919,923,924],{},"✅",[919,926,927],{},"Direct 1:1 mapping to base rows",[901,929,930,935,938],{},[919,931,932],{},[44,933,934],{},"DISTINCT",[919,936,937],{},"❌",[919,939,940],{},"Can't map result rows to unique base rows",[901,942,943,949,951],{},[919,944,945,948],{},[44,946,947],{},"GROUP BY"," \u002F aggregates",[919,950,937],{},[919,952,953],{},"Multiple base rows → one result row",[901,955,956,959,961],{},[919,957,958],{},"Window functions",[919,960,937],{},[919,962,963],{},"No 1:1 row mapping",[901,965,966,975,977],{},[919,967,968,971,972],{},[44,969,970],{},"UNION"," \u002F ",[44,973,974],{},"UNION ALL",[919,976,937],{},[919,978,979],{},"Ambiguous which base table to update",[901,981,982,988,991],{},[919,983,984,987],{},[44,985,986],{},"JOIN"," (multi-table)",[919,989,990],{},"❌ (mostly)",[919,992,993],{},"Can't determine which table to write to",[901,995,996,1004,1006],{},[919,997,998,971,1001],{},[44,999,1000],{},"LIMIT",[44,1002,1003],{},"OFFSET",[919,1005,937],{},[919,1007,1008],{},"Row identity lost",[14,1010,1011,1012,1015,1016,1019],{},"For non-updatable views, create ",[44,1013,1014],{},"INSTEAD OF"," triggers (on views) or ",[44,1017,1018],{},"INSTEAD"," rules to handle writes manually.",[34,1021,1022],{"language":36},[38,1023,1025],{"className":40,"code":1024,"language":36,"meta":42,"style":42},"-- Make a join view updatable with an INSTEAD OF trigger\nCREATE VIEW order_summary AS\n  SELECT o.id, o.amount, c.name AS customer_name\n  FROM orders o JOIN customers c ON o.customer_id = c.id;\n\nCREATE FUNCTION update_order_summary() RETURNS trigger AS $$\nBEGIN\n  -- Only the orders table is updated — customer_name is read-only through the view\n  UPDATE orders SET amount = NEW.amount WHERE id = NEW.id;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER update_order_summary\n  INSTEAD OF UPDATE ON order_summary\n  FOR EACH ROW EXECUTE FUNCTION update_order_summary();\n",[44,1026,1027,1032,1043,1081,1115,1119,1144,1149,1154,1190,1198,1205,1216,1220,1230,1245],{"__ignoreMap":42},[47,1028,1029],{"class":49,"line":50},[47,1030,1031],{"class":53},"-- Make a join view updatable with an INSTEAD OF trigger\n",[47,1033,1034,1036,1038,1041],{"class":49,"line":57},[47,1035,61],{"class":60},[47,1037,64],{"class":60},[47,1039,1040],{"class":67}," order_summary",[47,1042,71],{"class":60},[47,1044,1045,1047,1050,1053,1056,1058,1061,1063,1066,1068,1071,1073,1075,1078],{"class":49,"line":74},[47,1046,77],{"class":60},[47,1048,1049],{"class":569}," o",[47,1051,1052],{"class":80},".",[47,1054,1055],{"class":569},"id",[47,1057,517],{"class":80},[47,1059,1060],{"class":569},"o",[47,1062,1052],{"class":80},[47,1064,1065],{"class":569},"amount",[47,1067,517],{"class":80},[47,1069,1070],{"class":569},"c",[47,1072,1052],{"class":80},[47,1074,84],{"class":569},[47,1076,1077],{"class":60}," AS",[47,1079,1080],{"class":80}," customer_name\n",[47,1082,1083,1085,1088,1090,1093,1096,1098,1100,1103,1106,1109,1111,1113],{"class":49,"line":90},[47,1084,93],{"class":60},[47,1086,1087],{"class":80}," orders o ",[47,1089,986],{"class":60},[47,1091,1092],{"class":80}," customers c ",[47,1094,1095],{"class":60},"ON",[47,1097,1049],{"class":569},[47,1099,1052],{"class":80},[47,1101,1102],{"class":569},"customer_id",[47,1104,1105],{"class":60}," =",[47,1107,1108],{"class":569}," c",[47,1110,1052],{"class":80},[47,1112,1055],{"class":569},[47,1114,163],{"class":80},[47,1116,1117],{"class":49,"line":99},[47,1118,121],{"emptyLinePlaceholder":120},[47,1120,1121,1123,1126,1129,1132,1135,1138,1141],{"class":49,"line":117},[47,1122,61],{"class":60},[47,1124,1125],{"class":60}," FUNCTION",[47,1127,1128],{"class":67}," update_order_summary",[47,1130,1131],{"class":80},"() ",[47,1133,1134],{"class":60},"RETURNS",[47,1136,1137],{"class":80}," trigger ",[47,1139,1140],{"class":60},"AS",[47,1142,1143],{"class":80}," $$\n",[47,1145,1146],{"class":49,"line":124},[47,1147,1148],{"class":60},"BEGIN\n",[47,1150,1151],{"class":49,"line":130},[47,1152,1153],{"class":53},"  -- Only the orders table is updated — customer_name is read-only through the view\n",[47,1155,1156,1159,1162,1164,1167,1169,1172,1174,1176,1178,1180,1182,1184,1186,1188],{"class":49,"line":136},[47,1157,1158],{"class":60},"  UPDATE",[47,1160,1161],{"class":80}," orders ",[47,1163,551],{"class":60},[47,1165,1166],{"class":80}," amount ",[47,1168,108],{"class":60},[47,1170,1171],{"class":569}," NEW",[47,1173,1052],{"class":80},[47,1175,1065],{"class":569},[47,1177,561],{"class":60},[47,1179,564],{"class":80},[47,1181,108],{"class":60},[47,1183,1171],{"class":569},[47,1185,1052],{"class":80},[47,1187,1055],{"class":569},[47,1189,163],{"class":80},[47,1191,1192,1195],{"class":49,"line":166},[47,1193,1194],{"class":60},"  RETURN",[47,1196,1197],{"class":80}," NEW;\n",[47,1199,1200,1203],{"class":49,"line":171},[47,1201,1202],{"class":60},"END",[47,1204,163],{"class":80},[47,1206,1207,1210,1213],{"class":49,"line":177},[47,1208,1209],{"class":80},"$$ ",[47,1211,1212],{"class":60},"LANGUAGE",[47,1214,1215],{"class":80}," plpgsql;\n",[47,1217,1218],{"class":49,"line":201},[47,1219,121],{"emptyLinePlaceholder":120},[47,1221,1222,1224,1227],{"class":49,"line":207},[47,1223,61],{"class":60},[47,1225,1226],{"class":60}," TRIGGER",[47,1228,1229],{"class":67}," update_order_summary\n",[47,1231,1232,1235,1238,1240,1242],{"class":49,"line":213},[47,1233,1234],{"class":60},"  INSTEAD",[47,1236,1237],{"class":80}," OF ",[47,1239,433],{"class":60},[47,1241,315],{"class":60},[47,1243,1244],{"class":80}," order_summary\n",[47,1246,1248,1251,1254,1257,1260,1262],{"class":49,"line":1247},16,[47,1249,1250],{"class":60},"  FOR",[47,1252,1253],{"class":80}," EACH ",[47,1255,1256],{"class":60},"ROW",[47,1258,1259],{"class":60}," EXECUTE",[47,1261,1125],{"class":60},[47,1263,1264],{"class":80}," update_order_summary();\n",[18,1266],{},[21,1268,1270],{"id":1269},"materialized-views-stored-results","Materialized Views: Stored Results",[14,1272,1273,1274,1277,1278,626],{},"A materialized view (MV) ",[29,1275,1276],{},"stores the query result"," — it's a cross between a view and a table. Querying it reads the stored data (fast, no re-computation), but it's ",[29,1279,1280],{},"stale until refreshed",[34,1282,1283],{"language":36},[38,1284,1286],{"className":40,"code":1285,"language":36,"meta":42,"style":42},"-- Create: runs the query once, stores the result as a physical table-like object\nCREATE MATERIALIZED VIEW sales_by_city AS\n  SELECT c.city,\n         SUM(o.amount) AS total,\n         COUNT(*)      AS order_count\n  FROM customers c\n  JOIN orders o ON c.id = o.customer_id\n  GROUP BY c.city;\n\n-- Query: reads stored data — O(result size), no join, no aggregation\nSELECT * FROM sales_by_city ORDER BY total DESC;\n\n-- Refresh: re-runs the query, replaces the stored data\nREFRESH MATERIALIZED VIEW sales_by_city;\n\n-- Refresh without blocking readers (CONCURRENTLY requires a unique index)\nCREATE UNIQUE INDEX sales_by_city_city_idx ON sales_by_city(city);\nREFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n",[44,1287,1288,1293,1303,1317,1339,1357,1364,1388,1401,1405,1410,1432,1436,1441,1446,1450,1455,1471],{"__ignoreMap":42},[47,1289,1290],{"class":49,"line":50},[47,1291,1292],{"class":53},"-- Create: runs the query once, stores the result as a physical table-like object\n",[47,1294,1295,1297,1300],{"class":49,"line":57},[47,1296,61],{"class":60},[47,1298,1299],{"class":80}," MATERIALIZED VIEW sales_by_city ",[47,1301,1302],{"class":60},"AS\n",[47,1304,1305,1307,1309,1311,1314],{"class":49,"line":74},[47,1306,77],{"class":60},[47,1308,1108],{"class":569},[47,1310,1052],{"class":80},[47,1312,1313],{"class":569},"city",[47,1315,1316],{"class":80},",\n",[47,1318,1319,1322,1325,1327,1329,1331,1334,1336],{"class":49,"line":90},[47,1320,1321],{"class":569},"         SUM",[47,1323,1324],{"class":80},"(",[47,1326,1060],{"class":569},[47,1328,1052],{"class":80},[47,1330,1065],{"class":569},[47,1332,1333],{"class":80},") ",[47,1335,1140],{"class":60},[47,1337,1338],{"class":80}," total,\n",[47,1340,1341,1344,1346,1349,1352,1354],{"class":49,"line":99},[47,1342,1343],{"class":569},"         COUNT",[47,1345,1324],{"class":80},[47,1347,1348],{"class":60},"*",[47,1350,1351],{"class":80},")      ",[47,1353,1140],{"class":60},[47,1355,1356],{"class":80}," order_count\n",[47,1358,1359,1361],{"class":49,"line":117},[47,1360,93],{"class":60},[47,1362,1363],{"class":80}," customers c\n",[47,1365,1366,1369,1371,1373,1375,1377,1379,1381,1383,1385],{"class":49,"line":124},[47,1367,1368],{"class":60},"  JOIN",[47,1370,1087],{"class":80},[47,1372,1095],{"class":60},[47,1374,1108],{"class":569},[47,1376,1052],{"class":80},[47,1378,1055],{"class":569},[47,1380,1105],{"class":60},[47,1382,1049],{"class":569},[47,1384,1052],{"class":80},[47,1386,1387],{"class":569},"customer_id\n",[47,1389,1390,1393,1395,1397,1399],{"class":49,"line":130},[47,1391,1392],{"class":60},"  GROUP BY",[47,1394,1108],{"class":569},[47,1396,1052],{"class":80},[47,1398,1313],{"class":569},[47,1400,163],{"class":80},[47,1402,1403],{"class":49,"line":136},[47,1404,121],{"emptyLinePlaceholder":120},[47,1406,1407],{"class":49,"line":166},[47,1408,1409],{"class":53},"-- Query: reads stored data — O(result size), no join, no aggregation\n",[47,1411,1412,1414,1416,1418,1421,1424,1427,1430],{"class":49,"line":171},[47,1413,139],{"class":60},[47,1415,142],{"class":60},[47,1417,145],{"class":60},[47,1419,1420],{"class":80}," sales_by_city ",[47,1422,1423],{"class":60},"ORDER BY",[47,1425,1426],{"class":80}," total ",[47,1428,1429],{"class":60},"DESC",[47,1431,163],{"class":80},[47,1433,1434],{"class":49,"line":177},[47,1435,121],{"emptyLinePlaceholder":120},[47,1437,1438],{"class":49,"line":201},[47,1439,1440],{"class":53},"-- Refresh: re-runs the query, replaces the stored data\n",[47,1442,1443],{"class":49,"line":207},[47,1444,1445],{"class":80},"REFRESH MATERIALIZED VIEW sales_by_city;\n",[47,1447,1448],{"class":49,"line":213},[47,1449,121],{"emptyLinePlaceholder":120},[47,1451,1452],{"class":49,"line":1247},[47,1453,1454],{"class":53},"-- Refresh without blocking readers (CONCURRENTLY requires a unique index)\n",[47,1456,1458,1460,1463,1466,1468],{"class":49,"line":1457},17,[47,1459,61],{"class":60},[47,1461,1462],{"class":60}," UNIQUE INDEX",[47,1464,1465],{"class":67}," sales_by_city_city_idx",[47,1467,315],{"class":60},[47,1469,1470],{"class":80}," sales_by_city(city);\n",[47,1472,1474],{"class":49,"line":1473},18,[47,1475,1476],{"class":80},"REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n",[225,1478,1480],{"id":1479},"refresh-strategies","Refresh Strategies",[895,1482,1483,1502],{},[898,1484,1485],{},[901,1486,1487,1490,1493,1496,1499],{},[904,1488,1489],{},"Strategy",[904,1491,1492],{},"Lock",[904,1494,1495],{},"Readers Blocked?",[904,1497,1498],{},"Requirement",[904,1500,1501],{},"Use Case",[914,1503,1504,1525,1549,1571],{},[901,1505,1506,1511,1516,1519,1522],{},[919,1507,1508],{},[44,1509,1510],{},"REFRESH MATERIALIZED VIEW",[919,1512,1513],{},[44,1514,1515],{},"ACCESS EXCLUSIVE",[919,1517,1518],{},"✅ Yes",[919,1520,1521],{},"None",[919,1523,1524],{},"Maintenance window, dev",[901,1526,1527,1532,1537,1540,1546],{},[919,1528,1529],{},[44,1530,1531],{},"REFRESH ... CONCURRENTLY",[919,1533,1534],{},[44,1535,1536],{},"SHARE",[919,1538,1539],{},"❌ No",[919,1541,1542,1545],{},[44,1543,1544],{},"UNIQUE"," index",[919,1547,1548],{},"Production, zero-downtime",[901,1550,1551,1558,1561,1563,1568],{},[919,1552,1553,1554,1557],{},"Scheduled (",[44,1555,1556],{},"pg_cron",")",[919,1559,1560],{},"As above",[919,1562,1560],{},[919,1564,1565,1567],{},[44,1566,1556],{}," extension",[919,1569,1570],{},"Periodic refresh",[901,1572,1573,1576,1578,1580,1583],{},[919,1574,1575],{},"On-demand (ETL trigger)",[919,1577,1560],{},[919,1579,1560],{},[919,1581,1582],{},"App\u002FETL logic",[919,1584,1585],{},"After data loads",[34,1587,1588],{"language":36},[38,1589,1591],{"className":40,"code":1590,"language":36,"meta":42,"style":42},"-- Schedule hourly refresh with pg_cron (extension must be enabled)\n-- SELECT cron.schedule(\n--   'refresh-sales-hourly',\n--   '0 * * * *',                                  -- cron: every hour at minute 0\n--   'REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city'\n-- );\n\n-- On-demand refresh at the end of an ETL pipeline\n-- (in your ETL script, after loading new orders):\n-- REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n",[44,1592,1593,1598,1603,1608,1613,1618,1623,1627,1632,1637],{"__ignoreMap":42},[47,1594,1595],{"class":49,"line":50},[47,1596,1597],{"class":53},"-- Schedule hourly refresh with pg_cron (extension must be enabled)\n",[47,1599,1600],{"class":49,"line":57},[47,1601,1602],{"class":53},"-- SELECT cron.schedule(\n",[47,1604,1605],{"class":49,"line":74},[47,1606,1607],{"class":53},"--   'refresh-sales-hourly',\n",[47,1609,1610],{"class":49,"line":90},[47,1611,1612],{"class":53},"--   '0 * * * *',                                  -- cron: every hour at minute 0\n",[47,1614,1615],{"class":49,"line":99},[47,1616,1617],{"class":53},"--   'REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city'\n",[47,1619,1620],{"class":49,"line":117},[47,1621,1622],{"class":53},"-- );\n",[47,1624,1625],{"class":49,"line":124},[47,1626,121],{"emptyLinePlaceholder":120},[47,1628,1629],{"class":49,"line":130},[47,1630,1631],{"class":53},"-- On-demand refresh at the end of an ETL pipeline\n",[47,1633,1634],{"class":49,"line":136},[47,1635,1636],{"class":53},"-- (in your ETL script, after loading new orders):\n",[47,1638,1639],{"class":49,"line":166},[47,1640,1641],{"class":53},"-- REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n",[225,1643,1645],{"id":1644},"concurrently-the-unique-index-requirement","CONCURRENTLY: The Unique Index Requirement",[14,1647,1648,1651,1652,1655,1656,1658],{},[44,1649,1650],{},"REFRESH MATERIALIZED VIEW CONCURRENTLY"," builds a new version of the data in the background, then swaps atomically. To do this, it needs to ",[29,1653,1654],{},"identify which rows to update in place"," — which requires a ",[44,1657,1544],{}," index:",[34,1660,1661],{"language":36},[38,1662,1664],{"className":40,"code":1663,"language":36,"meta":42,"style":42},"-- Without a unique index → error\nREFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n-- ERROR: cannot refresh materialized view \"sales_by_city\" concurrently\n-- because it does not have a UNIQUE index that refutes updates\n\n-- Fix: create a unique index on a column (or composite) that uniquely identifies each row\nCREATE UNIQUE INDEX sales_by_city_city_idx ON sales_by_city(city);\n\n-- Now CONCURRENTLY works — readers never block\nREFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_city;\n",[44,1665,1666,1671,1675,1680,1685,1689,1694,1706,1710,1715],{"__ignoreMap":42},[47,1667,1668],{"class":49,"line":50},[47,1669,1670],{"class":53},"-- Without a unique index → error\n",[47,1672,1673],{"class":49,"line":57},[47,1674,1476],{"class":80},[47,1676,1677],{"class":49,"line":74},[47,1678,1679],{"class":53},"-- ERROR: cannot refresh materialized view \"sales_by_city\" concurrently\n",[47,1681,1682],{"class":49,"line":90},[47,1683,1684],{"class":53},"-- because it does not have a UNIQUE index that refutes updates\n",[47,1686,1687],{"class":49,"line":99},[47,1688,121],{"emptyLinePlaceholder":120},[47,1690,1691],{"class":49,"line":117},[47,1692,1693],{"class":53},"-- Fix: create a unique index on a column (or composite) that uniquely identifies each row\n",[47,1695,1696,1698,1700,1702,1704],{"class":49,"line":124},[47,1697,61],{"class":60},[47,1699,1462],{"class":60},[47,1701,1465],{"class":67},[47,1703,315],{"class":60},[47,1705,1470],{"class":80},[47,1707,1708],{"class":49,"line":130},[47,1709,121],{"emptyLinePlaceholder":120},[47,1711,1712],{"class":49,"line":136},[47,1713,1714],{"class":53},"-- Now CONCURRENTLY works — readers never block\n",[47,1716,1717],{"class":49,"line":166},[47,1718,1476],{"class":80},[14,1720,1721,1722,1725,1726,1052],{},"The unique index must cover a set of columns that uniquely identify each MV row. If the MV groups by ",[44,1723,1724],{},"(city, month)",", the index must be on ",[44,1727,1724],{},[18,1729],{},[21,1731,1733],{"id":1732},"complex-implementation-dual-view-reporting-architecture","Complex Implementation: Dual-View Reporting Architecture",[14,1735,1736],{},"A production analytics system that needs both real-time detail and fast pre-computed aggregates:",[34,1738,1739],{"language":36},[38,1740,1742],{"className":40,"code":1741,"language":36,"meta":42,"style":42},"-- ============================================================================\n-- Schema: orders and customers for a retail analytics platform\n-- ============================================================================\nCREATE TABLE customers (\n  id BIGSERIAL PRIMARY KEY,\n  name TEXT NOT NULL,\n  city TEXT NOT NULL,\n  active BOOLEAN NOT NULL DEFAULT true\n);\n\nCREATE TABLE orders (\n  id BIGSERIAL PRIMARY KEY,\n  customer_id BIGINT NOT NULL REFERENCES customers(id),\n  amount NUMERIC(12,2) NOT NULL,\n  status TEXT NOT NULL DEFAULT 'pending',   -- pending | shipped | delivered | cancelled\n  ordered_on TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE INDEX orders_customer_idx ON orders(customer_id);\nCREATE INDEX orders_ordered_on_idx ON orders(ordered_on);\nCREATE INDEX orders_status_idx ON orders(status);\n\n-- ============================================================================\n-- Regular view: real-time order detail (always current, no storage cost)\n-- Use for: operational dashboards that need up-to-the-minute data\n-- ============================================================================\nCREATE VIEW order_detail AS\n  SELECT o.id,\n         o.amount,\n         o.status,\n         o.ordered_on,\n         c.name  AS customer_name,\n         c.city  AS customer_city\n  FROM orders o\n  JOIN customers c ON o.customer_id = c.id\n  WHERE c.active = true;          -- only active customers visible in operational views\n\n-- Query the view — planner expands and pushes down predicates\nSELECT * FROM order_detail WHERE status = 'shipped' ORDER BY ordered_on DESC LIMIT 20;\n\n-- ============================================================================\n-- Materialized view: pre-computed monthly sales aggregates (fast, slightly stale)\n-- Use for: executive dashboard, revenue reports — queried frequently, expensive to compute\n-- ============================================================================\nCREATE MATERIALIZED VIEW monthly_sales AS\n  SELECT date_trunc('month', o.ordered_on) AS month,\n         c.city,\n         COUNT(*)                       AS order_count,\n         SUM(o.amount)                  AS revenue,\n         AVG(o.amount)                  AS avg_order_value,\n         COUNT(*) FILTER (WHERE o.status = 'cancelled') AS cancelled_count\n  FROM orders o\n  JOIN customers c ON o.customer_id = c.id\n  WHERE c.active = true\n    AND o.status IN ('shipped', 'delivered')   -- only completed sales count in revenue\n  GROUP BY date_trunc('month', o.ordered_on), c.city;\n\n-- Unique index: required for CONCURRENTLY refresh, also speeds up point lookups\nCREATE UNIQUE INDEX monthly_sales_month_city_idx ON monthly_sales(month, city);\n\n-- Secondary index: speeds up city-filtered queries on the MV\nCREATE INDEX monthly_sales_city_idx ON monthly_sales(city);\n\n-- Query the MV — sub-millisecond, no aggregation at query time\nSELECT month, city, revenue\nFROM monthly_sales\nWHERE city = 'NYC'\nORDER BY month DESC;\n\n-- ============================================================================\n-- Refresh strategy: CONCURRENTLY every hour via pg_cron\n-- ============================================================================\n-- SELECT cron.schedule(\n--   'refresh-monthly-sales',\n--   '0 * * * *',                                                      -- every hour\n--   'REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales'\n-- );\n\n-- ============================================================================\n-- Security view: row-level isolation per tenant (with WITH CHECK OPTION)\n-- ============================================================================\nCREATE VIEW tenant_orders AS\n  SELECT * FROM orders\n  WHERE customer_id IN (SELECT id FROM customers WHERE tenant_id = current_tenant_id())\n  WITH CHECK OPTION;              -- prevents inserting orders for other tenants\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON tenant_orders TO app_user;\nREVOKE ALL ON orders FROM app_user;   -- app_user can only access orders through the view\n",[44,1743,1744,1749,1754,1758,1771,1784,1797,1809,1824,1828,1832,1843,1853,1869,1895,1915,1933,1937,1941,1957,1972,1992,1997,2002,2008,2014,2019,2031,2044,2056,2067,2079,2095,2109,2117,2141,2160,2165,2171,2209,2214,2219,2225,2231,2236,2246,2274,2285,2302,2323,2344,2380,2387,2410,2425,2456,2484,2489,2495,2516,2521,2527,2542,2547,2553,2563,2571,2582,2594,2599,2604,2610,2615,2620,2626,2632,2638,2643,2648,2653,2659,2664,2676,2687,2718,2733,2738,2767],{"__ignoreMap":42},[47,1745,1746],{"class":49,"line":50},[47,1747,1748],{"class":53},"-- ============================================================================\n",[47,1750,1751],{"class":49,"line":57},[47,1752,1753],{"class":53},"-- Schema: orders and customers for a retail analytics platform\n",[47,1755,1756],{"class":49,"line":74},[47,1757,1748],{"class":53},[47,1759,1760,1762,1765,1768],{"class":49,"line":90},[47,1761,61],{"class":60},[47,1763,1764],{"class":60}," TABLE",[47,1766,1767],{"class":67}," customers",[47,1769,1770],{"class":80}," (\n",[47,1772,1773,1776,1779,1782],{"class":49,"line":99},[47,1774,1775],{"class":80},"  id ",[47,1777,1778],{"class":60},"BIGSERIAL",[47,1780,1781],{"class":60}," PRIMARY KEY",[47,1783,1316],{"class":80},[47,1785,1786,1789,1792,1795],{"class":49,"line":117},[47,1787,1788],{"class":60},"  name",[47,1790,1791],{"class":60}," TEXT",[47,1793,1794],{"class":60}," NOT NULL",[47,1796,1316],{"class":80},[47,1798,1799,1802,1805,1807],{"class":49,"line":124},[47,1800,1801],{"class":80},"  city ",[47,1803,1804],{"class":60},"TEXT",[47,1806,1794],{"class":60},[47,1808,1316],{"class":80},[47,1810,1811,1814,1817,1819,1822],{"class":49,"line":130},[47,1812,1813],{"class":80},"  active ",[47,1815,1816],{"class":60},"BOOLEAN",[47,1818,1794],{"class":60},[47,1820,1821],{"class":60}," DEFAULT",[47,1823,670],{"class":80},[47,1825,1826],{"class":49,"line":136},[47,1827,523],{"class":80},[47,1829,1830],{"class":49,"line":166},[47,1831,121],{"emptyLinePlaceholder":120},[47,1833,1834,1836,1838,1841],{"class":49,"line":171},[47,1835,61],{"class":60},[47,1837,1764],{"class":60},[47,1839,1840],{"class":67}," orders",[47,1842,1770],{"class":80},[47,1844,1845,1847,1849,1851],{"class":49,"line":177},[47,1846,1775],{"class":80},[47,1848,1778],{"class":60},[47,1850,1781],{"class":60},[47,1852,1316],{"class":80},[47,1854,1855,1858,1861,1863,1866],{"class":49,"line":201},[47,1856,1857],{"class":80},"  customer_id ",[47,1859,1860],{"class":60},"BIGINT",[47,1862,1794],{"class":60},[47,1864,1865],{"class":60}," REFERENCES",[47,1867,1868],{"class":80}," customers(id),\n",[47,1870,1871,1874,1877,1879,1882,1885,1888,1890,1893],{"class":49,"line":207},[47,1872,1873],{"class":80},"  amount ",[47,1875,1876],{"class":60},"NUMERIC",[47,1878,1324],{"class":80},[47,1880,1881],{"class":569},"12",[47,1883,1884],{"class":80},",",[47,1886,1887],{"class":569},"2",[47,1889,1333],{"class":80},[47,1891,1892],{"class":60},"NOT NULL",[47,1894,1316],{"class":80},[47,1896,1897,1900,1902,1904,1906,1909,1912],{"class":49,"line":213},[47,1898,1899],{"class":60},"  status",[47,1901,1791],{"class":60},[47,1903,1794],{"class":60},[47,1905,1821],{"class":60},[47,1907,1908],{"class":159}," 'pending'",[47,1910,1911],{"class":80},",   ",[47,1913,1914],{"class":53},"-- pending | shipped | delivered | cancelled\n",[47,1916,1917,1920,1923,1925,1927,1930],{"class":49,"line":1247},[47,1918,1919],{"class":80},"  ordered_on ",[47,1921,1922],{"class":60},"TIMESTAMPTZ",[47,1924,1794],{"class":60},[47,1926,1821],{"class":60},[47,1928,1929],{"class":60}," now",[47,1931,1932],{"class":80},"()\n",[47,1934,1935],{"class":49,"line":1457},[47,1936,523],{"class":80},[47,1938,1939],{"class":49,"line":1473},[47,1940,121],{"emptyLinePlaceholder":120},[47,1942,1944,1946,1949,1952,1954],{"class":49,"line":1943},19,[47,1945,61],{"class":60},[47,1947,1948],{"class":60}," INDEX",[47,1950,1951],{"class":67}," orders_customer_idx",[47,1953,315],{"class":60},[47,1955,1956],{"class":80}," orders(customer_id);\n",[47,1958,1960,1962,1964,1967,1969],{"class":49,"line":1959},20,[47,1961,61],{"class":60},[47,1963,1948],{"class":60},[47,1965,1966],{"class":67}," orders_ordered_on_idx",[47,1968,315],{"class":60},[47,1970,1971],{"class":80}," orders(ordered_on);\n",[47,1973,1975,1977,1979,1982,1984,1987,1990],{"class":49,"line":1974},21,[47,1976,61],{"class":60},[47,1978,1948],{"class":60},[47,1980,1981],{"class":67}," orders_status_idx",[47,1983,315],{"class":60},[47,1985,1986],{"class":80}," orders(",[47,1988,1989],{"class":60},"status",[47,1991,523],{"class":80},[47,1993,1995],{"class":49,"line":1994},22,[47,1996,121],{"emptyLinePlaceholder":120},[47,1998,2000],{"class":49,"line":1999},23,[47,2001,1748],{"class":53},[47,2003,2005],{"class":49,"line":2004},24,[47,2006,2007],{"class":53},"-- Regular view: real-time order detail (always current, no storage cost)\n",[47,2009,2011],{"class":49,"line":2010},25,[47,2012,2013],{"class":53},"-- Use for: operational dashboards that need up-to-the-minute data\n",[47,2015,2017],{"class":49,"line":2016},26,[47,2018,1748],{"class":53},[47,2020,2022,2024,2026,2029],{"class":49,"line":2021},27,[47,2023,61],{"class":60},[47,2025,64],{"class":60},[47,2027,2028],{"class":67}," order_detail",[47,2030,71],{"class":60},[47,2032,2034,2036,2038,2040,2042],{"class":49,"line":2033},28,[47,2035,77],{"class":60},[47,2037,1049],{"class":569},[47,2039,1052],{"class":80},[47,2041,1055],{"class":569},[47,2043,1316],{"class":80},[47,2045,2047,2050,2052,2054],{"class":49,"line":2046},29,[47,2048,2049],{"class":569},"         o",[47,2051,1052],{"class":80},[47,2053,1065],{"class":569},[47,2055,1316],{"class":80},[47,2057,2059,2061,2063,2065],{"class":49,"line":2058},30,[47,2060,2049],{"class":569},[47,2062,1052],{"class":80},[47,2064,1989],{"class":569},[47,2066,1316],{"class":80},[47,2068,2070,2072,2074,2077],{"class":49,"line":2069},31,[47,2071,2049],{"class":569},[47,2073,1052],{"class":80},[47,2075,2076],{"class":569},"ordered_on",[47,2078,1316],{"class":80},[47,2080,2082,2085,2087,2089,2092],{"class":49,"line":2081},32,[47,2083,2084],{"class":569},"         c",[47,2086,1052],{"class":80},[47,2088,84],{"class":569},[47,2090,2091],{"class":60},"  AS",[47,2093,2094],{"class":80}," customer_name,\n",[47,2096,2098,2100,2102,2104,2106],{"class":49,"line":2097},33,[47,2099,2084],{"class":569},[47,2101,1052],{"class":80},[47,2103,1313],{"class":569},[47,2105,2091],{"class":60},[47,2107,2108],{"class":80}," customer_city\n",[47,2110,2112,2114],{"class":49,"line":2111},34,[47,2113,93],{"class":60},[47,2115,2116],{"class":80}," orders o\n",[47,2118,2120,2122,2124,2126,2128,2130,2132,2134,2136,2138],{"class":49,"line":2119},35,[47,2121,1368],{"class":60},[47,2123,1092],{"class":80},[47,2125,1095],{"class":60},[47,2127,1049],{"class":569},[47,2129,1052],{"class":80},[47,2131,1102],{"class":569},[47,2133,1105],{"class":60},[47,2135,1108],{"class":569},[47,2137,1052],{"class":80},[47,2139,2140],{"class":569},"id\n",[47,2142,2144,2146,2148,2150,2153,2155,2157],{"class":49,"line":2143},36,[47,2145,102],{"class":60},[47,2147,1108],{"class":569},[47,2149,1052],{"class":80},[47,2151,2152],{"class":569},"active",[47,2154,1105],{"class":60},[47,2156,111],{"class":80},[47,2158,2159],{"class":53},"-- only active customers visible in operational views\n",[47,2161,2163],{"class":49,"line":2162},37,[47,2164,121],{"emptyLinePlaceholder":120},[47,2166,2168],{"class":49,"line":2167},38,[47,2169,2170],{"class":53},"-- Query the view — planner expands and pushes down predicates\n",[47,2172,2174,2176,2178,2180,2183,2185,2188,2190,2193,2196,2199,2201,2204,2207],{"class":49,"line":2173},39,[47,2175,139],{"class":60},[47,2177,142],{"class":60},[47,2179,145],{"class":60},[47,2181,2182],{"class":80}," order_detail ",[47,2184,151],{"class":60},[47,2186,2187],{"class":60}," status",[47,2189,1105],{"class":60},[47,2191,2192],{"class":159}," 'shipped'",[47,2194,2195],{"class":60}," ORDER BY",[47,2197,2198],{"class":80}," ordered_on ",[47,2200,1429],{"class":60},[47,2202,2203],{"class":60}," LIMIT",[47,2205,2206],{"class":569}," 20",[47,2208,163],{"class":80},[47,2210,2212],{"class":49,"line":2211},40,[47,2213,121],{"emptyLinePlaceholder":120},[47,2215,2217],{"class":49,"line":2216},41,[47,2218,1748],{"class":53},[47,2220,2222],{"class":49,"line":2221},42,[47,2223,2224],{"class":53},"-- Materialized view: pre-computed monthly sales aggregates (fast, slightly stale)\n",[47,2226,2228],{"class":49,"line":2227},43,[47,2229,2230],{"class":53},"-- Use for: executive dashboard, revenue reports — queried frequently, expensive to compute\n",[47,2232,2234],{"class":49,"line":2233},44,[47,2235,1748],{"class":53},[47,2237,2239,2241,2244],{"class":49,"line":2238},45,[47,2240,61],{"class":60},[47,2242,2243],{"class":80}," MATERIALIZED VIEW monthly_sales ",[47,2245,1302],{"class":60},[47,2247,2249,2251,2254,2257,2259,2261,2263,2265,2267,2269,2272],{"class":49,"line":2248},46,[47,2250,77],{"class":60},[47,2252,2253],{"class":80}," date_trunc(",[47,2255,2256],{"class":159},"'month'",[47,2258,517],{"class":80},[47,2260,1060],{"class":569},[47,2262,1052],{"class":80},[47,2264,2076],{"class":569},[47,2266,1333],{"class":80},[47,2268,1140],{"class":60},[47,2270,2271],{"class":60}," month",[47,2273,1316],{"class":80},[47,2275,2277,2279,2281,2283],{"class":49,"line":2276},47,[47,2278,2084],{"class":569},[47,2280,1052],{"class":80},[47,2282,1313],{"class":569},[47,2284,1316],{"class":80},[47,2286,2288,2290,2292,2294,2297,2299],{"class":49,"line":2287},48,[47,2289,1343],{"class":569},[47,2291,1324],{"class":80},[47,2293,1348],{"class":60},[47,2295,2296],{"class":80},")                       ",[47,2298,1140],{"class":60},[47,2300,2301],{"class":80}," order_count,\n",[47,2303,2305,2307,2309,2311,2313,2315,2318,2320],{"class":49,"line":2304},49,[47,2306,1321],{"class":569},[47,2308,1324],{"class":80},[47,2310,1060],{"class":569},[47,2312,1052],{"class":80},[47,2314,1065],{"class":569},[47,2316,2317],{"class":80},")                  ",[47,2319,1140],{"class":60},[47,2321,2322],{"class":80}," revenue,\n",[47,2324,2326,2329,2331,2333,2335,2337,2339,2341],{"class":49,"line":2325},50,[47,2327,2328],{"class":569},"         AVG",[47,2330,1324],{"class":80},[47,2332,1060],{"class":569},[47,2334,1052],{"class":80},[47,2336,1065],{"class":569},[47,2338,2317],{"class":80},[47,2340,1140],{"class":60},[47,2342,2343],{"class":80}," avg_order_value,\n",[47,2345,2347,2349,2351,2353,2355,2358,2360,2362,2364,2366,2368,2370,2373,2375,2377],{"class":49,"line":2346},51,[47,2348,1343],{"class":569},[47,2350,1324],{"class":80},[47,2352,1348],{"class":60},[47,2354,1333],{"class":80},[47,2356,2357],{"class":60},"FILTER",[47,2359,511],{"class":80},[47,2361,151],{"class":60},[47,2363,1049],{"class":569},[47,2365,1052],{"class":80},[47,2367,1989],{"class":569},[47,2369,1105],{"class":60},[47,2371,2372],{"class":159}," 'cancelled'",[47,2374,1333],{"class":80},[47,2376,1140],{"class":60},[47,2378,2379],{"class":80}," cancelled_count\n",[47,2381,2383,2385],{"class":49,"line":2382},52,[47,2384,93],{"class":60},[47,2386,2116],{"class":80},[47,2388,2390,2392,2394,2396,2398,2400,2402,2404,2406,2408],{"class":49,"line":2389},53,[47,2391,1368],{"class":60},[47,2393,1092],{"class":80},[47,2395,1095],{"class":60},[47,2397,1049],{"class":569},[47,2399,1052],{"class":80},[47,2401,1102],{"class":569},[47,2403,1105],{"class":60},[47,2405,1108],{"class":569},[47,2407,1052],{"class":80},[47,2409,2140],{"class":569},[47,2411,2413,2415,2417,2419,2421,2423],{"class":49,"line":2412},54,[47,2414,102],{"class":60},[47,2416,1108],{"class":569},[47,2418,1052],{"class":80},[47,2420,2152],{"class":569},[47,2422,1105],{"class":60},[47,2424,670],{"class":80},[47,2426,2428,2431,2433,2435,2437,2440,2442,2445,2447,2450,2453],{"class":49,"line":2427},55,[47,2429,2430],{"class":60},"    AND",[47,2432,1049],{"class":569},[47,2434,1052],{"class":80},[47,2436,1989],{"class":569},[47,2438,2439],{"class":60}," IN",[47,2441,511],{"class":80},[47,2443,2444],{"class":159},"'shipped'",[47,2446,517],{"class":80},[47,2448,2449],{"class":159},"'delivered'",[47,2451,2452],{"class":80},")   ",[47,2454,2455],{"class":53},"-- only completed sales count in revenue\n",[47,2457,2459,2461,2463,2465,2467,2469,2471,2473,2476,2478,2480,2482],{"class":49,"line":2458},56,[47,2460,1392],{"class":60},[47,2462,2253],{"class":80},[47,2464,2256],{"class":159},[47,2466,517],{"class":80},[47,2468,1060],{"class":569},[47,2470,1052],{"class":80},[47,2472,2076],{"class":569},[47,2474,2475],{"class":80},"), ",[47,2477,1070],{"class":569},[47,2479,1052],{"class":80},[47,2481,1313],{"class":569},[47,2483,163],{"class":80},[47,2485,2487],{"class":49,"line":2486},57,[47,2488,121],{"emptyLinePlaceholder":120},[47,2490,2492],{"class":49,"line":2491},58,[47,2493,2494],{"class":53},"-- Unique index: required for CONCURRENTLY refresh, also speeds up point lookups\n",[47,2496,2498,2500,2502,2505,2507,2510,2513],{"class":49,"line":2497},59,[47,2499,61],{"class":60},[47,2501,1462],{"class":60},[47,2503,2504],{"class":67}," monthly_sales_month_city_idx",[47,2506,315],{"class":60},[47,2508,2509],{"class":80}," monthly_sales(",[47,2511,2512],{"class":60},"month",[47,2514,2515],{"class":80},", city);\n",[47,2517,2519],{"class":49,"line":2518},60,[47,2520,121],{"emptyLinePlaceholder":120},[47,2522,2524],{"class":49,"line":2523},61,[47,2525,2526],{"class":53},"-- Secondary index: speeds up city-filtered queries on the MV\n",[47,2528,2530,2532,2534,2537,2539],{"class":49,"line":2529},62,[47,2531,61],{"class":60},[47,2533,1948],{"class":60},[47,2535,2536],{"class":67}," monthly_sales_city_idx",[47,2538,315],{"class":60},[47,2540,2541],{"class":80}," monthly_sales(city);\n",[47,2543,2545],{"class":49,"line":2544},63,[47,2546,121],{"emptyLinePlaceholder":120},[47,2548,2550],{"class":49,"line":2549},64,[47,2551,2552],{"class":53},"-- Query the MV — sub-millisecond, no aggregation at query time\n",[47,2554,2556,2558,2560],{"class":49,"line":2555},65,[47,2557,139],{"class":60},[47,2559,2271],{"class":60},[47,2561,2562],{"class":80},", city, revenue\n",[47,2564,2566,2568],{"class":49,"line":2565},66,[47,2567,342],{"class":60},[47,2569,2570],{"class":80}," monthly_sales\n",[47,2572,2574,2576,2578,2580],{"class":49,"line":2573},67,[47,2575,151],{"class":60},[47,2577,154],{"class":80},[47,2579,108],{"class":60},[47,2581,809],{"class":159},[47,2583,2585,2587,2589,2592],{"class":49,"line":2584},68,[47,2586,1423],{"class":60},[47,2588,2271],{"class":60},[47,2590,2591],{"class":60}," DESC",[47,2593,163],{"class":80},[47,2595,2597],{"class":49,"line":2596},69,[47,2598,121],{"emptyLinePlaceholder":120},[47,2600,2602],{"class":49,"line":2601},70,[47,2603,1748],{"class":53},[47,2605,2607],{"class":49,"line":2606},71,[47,2608,2609],{"class":53},"-- Refresh strategy: CONCURRENTLY every hour via pg_cron\n",[47,2611,2613],{"class":49,"line":2612},72,[47,2614,1748],{"class":53},[47,2616,2618],{"class":49,"line":2617},73,[47,2619,1602],{"class":53},[47,2621,2623],{"class":49,"line":2622},74,[47,2624,2625],{"class":53},"--   'refresh-monthly-sales',\n",[47,2627,2629],{"class":49,"line":2628},75,[47,2630,2631],{"class":53},"--   '0 * * * *',                                                      -- every hour\n",[47,2633,2635],{"class":49,"line":2634},76,[47,2636,2637],{"class":53},"--   'REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales'\n",[47,2639,2641],{"class":49,"line":2640},77,[47,2642,1622],{"class":53},[47,2644,2646],{"class":49,"line":2645},78,[47,2647,121],{"emptyLinePlaceholder":120},[47,2649,2651],{"class":49,"line":2650},79,[47,2652,1748],{"class":53},[47,2654,2656],{"class":49,"line":2655},80,[47,2657,2658],{"class":53},"-- Security view: row-level isolation per tenant (with WITH CHECK OPTION)\n",[47,2660,2662],{"class":49,"line":2661},81,[47,2663,1748],{"class":53},[47,2665,2667,2669,2671,2674],{"class":49,"line":2666},82,[47,2668,61],{"class":60},[47,2670,64],{"class":60},[47,2672,2673],{"class":67}," tenant_orders",[47,2675,71],{"class":60},[47,2677,2679,2681,2683,2685],{"class":49,"line":2678},83,[47,2680,77],{"class":60},[47,2682,142],{"class":60},[47,2684,145],{"class":60},[47,2686,379],{"class":80},[47,2688,2690,2692,2694,2697,2699,2701,2703,2705,2708,2710,2713,2715],{"class":49,"line":2689},84,[47,2691,102],{"class":60},[47,2693,386],{"class":80},[47,2695,2696],{"class":60},"IN",[47,2698,511],{"class":80},[47,2700,139],{"class":60},[47,2702,564],{"class":80},[47,2704,342],{"class":60},[47,2706,2707],{"class":80}," customers ",[47,2709,151],{"class":60},[47,2711,2712],{"class":80}," tenant_id ",[47,2714,108],{"class":60},[47,2716,2717],{"class":80}," current_tenant_id())\n",[47,2719,2721,2723,2725,2727,2730],{"class":49,"line":2720},85,[47,2722,675],{"class":60},[47,2724,678],{"class":60},[47,2726,681],{"class":60},[47,2728,2729],{"class":80},";              ",[47,2731,2732],{"class":53},"-- prevents inserting orders for other tenants\n",[47,2734,2736],{"class":49,"line":2735},86,[47,2737,121],{"emptyLinePlaceholder":120},[47,2739,2741,2743,2745,2747,2749,2751,2753,2755,2757,2759,2762,2764],{"class":49,"line":2740},87,[47,2742,309],{"class":60},[47,2744,312],{"class":60},[47,2746,517],{"class":80},[47,2748,429],{"class":60},[47,2750,517],{"class":80},[47,2752,433],{"class":60},[47,2754,517],{"class":80},[47,2756,436],{"class":60},[47,2758,315],{"class":60},[47,2760,2761],{"class":80}," tenant_orders ",[47,2763,321],{"class":60},[47,2765,2766],{"class":80}," app_user;\n",[47,2768,2770,2772,2775,2777,2779,2781,2784],{"class":49,"line":2769},88,[47,2771,332],{"class":60},[47,2773,2774],{"class":80}," ALL ",[47,2776,1095],{"class":60},[47,2778,1161],{"class":80},[47,2780,342],{"class":60},[47,2782,2783],{"class":80}," app_user;   ",[47,2785,2786],{"class":53},"-- app_user can only access orders through the view\n",[14,2788,2789,2792],{},[29,2790,2791],{},"Why this architecture",": the regular view gives real-time detail (no staleness, no storage), while the materialized view gives fast aggregates (pre-computed, CONCURRENTLY refreshed). Using a regular view for the aggregates would recompute the full join + aggregation on every query — fine for 100 rows, catastrophic for 10M.",[18,2794],{},[21,2796,2798],{"id":2797},"anti-pattern-regular-view-for-expensive-aggregates","Anti-Pattern: Regular View for Expensive Aggregates",[34,2800,2801],{"language":36},[38,2802,2804],{"className":40,"code":2803,"language":36,"meta":42,"style":42},"-- ❌ WRONG: regular view for a dashboard query that runs on every page load\nCREATE VIEW dashboard_sales AS\n  SELECT c.city, SUM(o.amount) AS total\n  FROM customers c\n  JOIN orders o ON c.id = o.customer_id   -- join of 1M orders + 100K customers\n  GROUP BY c.city;                        -- full aggregation every single query\n\n-- Every SELECT * FROM dashboard_sales recomputes the entire join + aggregation\n-- 10-second query on every dashboard load → unusable\n\n-- ✅ RIGHT: materialized view with CONCURRENTLY refresh\nCREATE MATERIALIZED VIEW dashboard_sales AS\n  SELECT c.city, SUM(o.amount) AS total\n  FROM customers c\n  JOIN orders o ON c.id = o.customer_id\n  GROUP BY c.city;\n\nCREATE UNIQUE INDEX dashboard_sales_city_idx ON dashboard_sales(city);  -- required for CONCURRENTLY\n\n-- Refresh hourly (or after ETL loads)\nREFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales;\n\n-- Dashboard query is now sub-millisecond — reads pre-computed data\nSELECT * FROM dashboard_sales ORDER BY total DESC;\n",[44,2805,2806,2811,2822,2852,2858,2883,2899,2903,2908,2913,2917,2922,2931,2959,2965,2987,2999,3003,3020,3024,3029,3034,3038,3043],{"__ignoreMap":42},[47,2807,2808],{"class":49,"line":50},[47,2809,2810],{"class":53},"-- ❌ WRONG: regular view for a dashboard query that runs on every page load\n",[47,2812,2813,2815,2817,2820],{"class":49,"line":57},[47,2814,61],{"class":60},[47,2816,64],{"class":60},[47,2818,2819],{"class":67}," dashboard_sales",[47,2821,71],{"class":60},[47,2823,2824,2826,2828,2830,2832,2834,2837,2839,2841,2843,2845,2847,2849],{"class":49,"line":74},[47,2825,77],{"class":60},[47,2827,1108],{"class":569},[47,2829,1052],{"class":80},[47,2831,1313],{"class":569},[47,2833,517],{"class":80},[47,2835,2836],{"class":569},"SUM",[47,2838,1324],{"class":80},[47,2840,1060],{"class":569},[47,2842,1052],{"class":80},[47,2844,1065],{"class":569},[47,2846,1333],{"class":80},[47,2848,1140],{"class":60},[47,2850,2851],{"class":80}," total\n",[47,2853,2854,2856],{"class":49,"line":90},[47,2855,93],{"class":60},[47,2857,1363],{"class":80},[47,2859,2860,2862,2864,2866,2868,2870,2872,2874,2876,2878,2880],{"class":49,"line":99},[47,2861,1368],{"class":60},[47,2863,1087],{"class":80},[47,2865,1095],{"class":60},[47,2867,1108],{"class":569},[47,2869,1052],{"class":80},[47,2871,1055],{"class":569},[47,2873,1105],{"class":60},[47,2875,1049],{"class":569},[47,2877,1052],{"class":80},[47,2879,1102],{"class":569},[47,2881,2882],{"class":53},"   -- join of 1M orders + 100K customers\n",[47,2884,2885,2887,2889,2891,2893,2896],{"class":49,"line":117},[47,2886,1392],{"class":60},[47,2888,1108],{"class":569},[47,2890,1052],{"class":80},[47,2892,1313],{"class":569},[47,2894,2895],{"class":80},";                        ",[47,2897,2898],{"class":53},"-- full aggregation every single query\n",[47,2900,2901],{"class":49,"line":124},[47,2902,121],{"emptyLinePlaceholder":120},[47,2904,2905],{"class":49,"line":130},[47,2906,2907],{"class":53},"-- Every SELECT * FROM dashboard_sales recomputes the entire join + aggregation\n",[47,2909,2910],{"class":49,"line":136},[47,2911,2912],{"class":53},"-- 10-second query on every dashboard load → unusable\n",[47,2914,2915],{"class":49,"line":166},[47,2916,121],{"emptyLinePlaceholder":120},[47,2918,2919],{"class":49,"line":171},[47,2920,2921],{"class":53},"-- ✅ RIGHT: materialized view with CONCURRENTLY refresh\n",[47,2923,2924,2926,2929],{"class":49,"line":177},[47,2925,61],{"class":60},[47,2927,2928],{"class":80}," MATERIALIZED VIEW dashboard_sales ",[47,2930,1302],{"class":60},[47,2932,2933,2935,2937,2939,2941,2943,2945,2947,2949,2951,2953,2955,2957],{"class":49,"line":201},[47,2934,77],{"class":60},[47,2936,1108],{"class":569},[47,2938,1052],{"class":80},[47,2940,1313],{"class":569},[47,2942,517],{"class":80},[47,2944,2836],{"class":569},[47,2946,1324],{"class":80},[47,2948,1060],{"class":569},[47,2950,1052],{"class":80},[47,2952,1065],{"class":569},[47,2954,1333],{"class":80},[47,2956,1140],{"class":60},[47,2958,2851],{"class":80},[47,2960,2961,2963],{"class":49,"line":207},[47,2962,93],{"class":60},[47,2964,1363],{"class":80},[47,2966,2967,2969,2971,2973,2975,2977,2979,2981,2983,2985],{"class":49,"line":213},[47,2968,1368],{"class":60},[47,2970,1087],{"class":80},[47,2972,1095],{"class":60},[47,2974,1108],{"class":569},[47,2976,1052],{"class":80},[47,2978,1055],{"class":569},[47,2980,1105],{"class":60},[47,2982,1049],{"class":569},[47,2984,1052],{"class":80},[47,2986,1387],{"class":569},[47,2988,2989,2991,2993,2995,2997],{"class":49,"line":1247},[47,2990,1392],{"class":60},[47,2992,1108],{"class":569},[47,2994,1052],{"class":80},[47,2996,1313],{"class":569},[47,2998,163],{"class":80},[47,3000,3001],{"class":49,"line":1457},[47,3002,121],{"emptyLinePlaceholder":120},[47,3004,3005,3007,3009,3012,3014,3017],{"class":49,"line":1473},[47,3006,61],{"class":60},[47,3008,1462],{"class":60},[47,3010,3011],{"class":67}," dashboard_sales_city_idx",[47,3013,315],{"class":60},[47,3015,3016],{"class":80}," dashboard_sales(city);  ",[47,3018,3019],{"class":53},"-- required for CONCURRENTLY\n",[47,3021,3022],{"class":49,"line":1943},[47,3023,121],{"emptyLinePlaceholder":120},[47,3025,3026],{"class":49,"line":1959},[47,3027,3028],{"class":53},"-- Refresh hourly (or after ETL loads)\n",[47,3030,3031],{"class":49,"line":1974},[47,3032,3033],{"class":80},"REFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales;\n",[47,3035,3036],{"class":49,"line":1994},[47,3037,121],{"emptyLinePlaceholder":120},[47,3039,3040],{"class":49,"line":1999},[47,3041,3042],{"class":53},"-- Dashboard query is now sub-millisecond — reads pre-computed data\n",[47,3044,3045,3047,3049,3051,3054,3056,3058,3060],{"class":49,"line":2004},[47,3046,139],{"class":60},[47,3048,142],{"class":60},[47,3050,145],{"class":60},[47,3052,3053],{"class":80}," dashboard_sales ",[47,3055,1423],{"class":60},[47,3057,1426],{"class":80},[47,3059,1429],{"class":60},[47,3061,163],{"class":80},[14,3063,3064,3067],{},[29,3065,3066],{},"Why the wrong way fails",": a regular view has no storage — every query re-runs the full join and aggregation. For a dashboard loaded 1000×\u002Fday over millions of rows, this is a CPU and I\u002FO disaster.",[18,3069],{},[21,3071,3073],{"id":3072},"anti-pattern-materialized-view-without-refresh-strategy","Anti-Pattern: Materialized View Without Refresh Strategy",[34,3075,3076],{"language":36},[38,3077,3079],{"className":40,"code":3078,"language":36,"meta":42,"style":42},"-- ❌ WRONG: MV created but never refreshed → data goes stale forever\nCREATE MATERIALIZED VIEW user_stats AS\n  SELECT user_id, COUNT(*) AS login_count, MAX(login_at) AS last_login\n  FROM logins GROUP BY user_id;\n-- No index, no refresh schedule, no trigger → data frozen at creation time\n\n-- ✅ RIGHT: MV with unique index + scheduled CONCURRENTLY refresh\nCREATE MATERIALIZED VIEW user_stats AS\n  SELECT user_id, COUNT(*) AS login_count, MAX(login_at) AS last_login\n  FROM logins GROUP BY user_id;\n\nCREATE UNIQUE INDEX user_stats_user_idx ON user_stats(user_id);\n\n-- Schedule daily refresh\n-- SELECT cron.schedule('refresh-user-stats', '0 2 * * *',\n--   'REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats');\n\n-- Or trigger after ETL: REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;\n",[44,3080,3081,3086,3095,3127,3139,3144,3148,3153,3161,3187,3197,3201,3215,3219,3224,3229,3234,3238],{"__ignoreMap":42},[47,3082,3083],{"class":49,"line":50},[47,3084,3085],{"class":53},"-- ❌ WRONG: MV created but never refreshed → data goes stale forever\n",[47,3087,3088,3090,3093],{"class":49,"line":57},[47,3089,61],{"class":60},[47,3091,3092],{"class":80}," MATERIALIZED VIEW user_stats ",[47,3094,1302],{"class":60},[47,3096,3097,3099,3102,3105,3107,3109,3111,3113,3116,3119,3122,3124],{"class":49,"line":74},[47,3098,77],{"class":60},[47,3100,3101],{"class":80}," user_id, ",[47,3103,3104],{"class":569},"COUNT",[47,3106,1324],{"class":80},[47,3108,1348],{"class":60},[47,3110,1333],{"class":80},[47,3112,1140],{"class":60},[47,3114,3115],{"class":80}," login_count, ",[47,3117,3118],{"class":569},"MAX",[47,3120,3121],{"class":80},"(login_at) ",[47,3123,1140],{"class":60},[47,3125,3126],{"class":80}," last_login\n",[47,3128,3129,3131,3134,3136],{"class":49,"line":90},[47,3130,93],{"class":60},[47,3132,3133],{"class":80}," logins ",[47,3135,947],{"class":60},[47,3137,3138],{"class":80}," user_id;\n",[47,3140,3141],{"class":49,"line":99},[47,3142,3143],{"class":53},"-- No index, no refresh schedule, no trigger → data frozen at creation time\n",[47,3145,3146],{"class":49,"line":117},[47,3147,121],{"emptyLinePlaceholder":120},[47,3149,3150],{"class":49,"line":124},[47,3151,3152],{"class":53},"-- ✅ RIGHT: MV with unique index + scheduled CONCURRENTLY refresh\n",[47,3154,3155,3157,3159],{"class":49,"line":130},[47,3156,61],{"class":60},[47,3158,3092],{"class":80},[47,3160,1302],{"class":60},[47,3162,3163,3165,3167,3169,3171,3173,3175,3177,3179,3181,3183,3185],{"class":49,"line":136},[47,3164,77],{"class":60},[47,3166,3101],{"class":80},[47,3168,3104],{"class":569},[47,3170,1324],{"class":80},[47,3172,1348],{"class":60},[47,3174,1333],{"class":80},[47,3176,1140],{"class":60},[47,3178,3115],{"class":80},[47,3180,3118],{"class":569},[47,3182,3121],{"class":80},[47,3184,1140],{"class":60},[47,3186,3126],{"class":80},[47,3188,3189,3191,3193,3195],{"class":49,"line":166},[47,3190,93],{"class":60},[47,3192,3133],{"class":80},[47,3194,947],{"class":60},[47,3196,3138],{"class":80},[47,3198,3199],{"class":49,"line":171},[47,3200,121],{"emptyLinePlaceholder":120},[47,3202,3203,3205,3207,3210,3212],{"class":49,"line":177},[47,3204,61],{"class":60},[47,3206,1462],{"class":60},[47,3208,3209],{"class":67}," user_stats_user_idx",[47,3211,315],{"class":60},[47,3213,3214],{"class":80}," user_stats(user_id);\n",[47,3216,3217],{"class":49,"line":201},[47,3218,121],{"emptyLinePlaceholder":120},[47,3220,3221],{"class":49,"line":207},[47,3222,3223],{"class":53},"-- Schedule daily refresh\n",[47,3225,3226],{"class":49,"line":213},[47,3227,3228],{"class":53},"-- SELECT cron.schedule('refresh-user-stats', '0 2 * * *',\n",[47,3230,3231],{"class":49,"line":1247},[47,3232,3233],{"class":53},"--   'REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats');\n",[47,3235,3236],{"class":49,"line":1457},[47,3237,121],{"emptyLinePlaceholder":120},[47,3239,3240],{"class":49,"line":1473},[47,3241,3242],{"class":53},"-- Or trigger after ETL: REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;\n",[18,3244],{},[21,3246,3248],{"id":3247},"materialized-view-vs-regular-view-decision-matrix","Materialized View vs Regular View: Decision Matrix",[895,3250,3251,3263],{},[898,3252,3253],{},[901,3254,3255,3257,3260],{},[904,3256,1498],{},[904,3258,3259],{},"Regular View",[904,3261,3262],{},"Materialized View",[914,3264,3265,3276,3287,3298,3309,3320,3331],{},[901,3266,3267,3270,3273],{},[919,3268,3269],{},"Real-time data",[919,3271,3272],{},"✅ Always current",[919,3274,3275],{},"❌ Stale until refresh",[901,3277,3278,3281,3284],{},[919,3279,3280],{},"Expensive query, queried often",[919,3282,3283],{},"❌ Recomputes every time",[919,3285,3286],{},"✅ Pre-computed, fast read",[901,3288,3289,3292,3295],{},[919,3290,3291],{},"Storage cost",[919,3293,3294],{},"✅ Zero",[919,3296,3297],{},"❌ Proportional to result set",[901,3299,3300,3303,3306],{},[919,3301,3302],{},"Updatable",[919,3304,3305],{},"✅ (if simple)",[919,3307,3308],{},"❌ Only REFRESH",[901,3310,3311,3314,3317],{},[919,3312,3313],{},"Indexes",[919,3315,3316],{},"On base tables",[919,3318,3319],{},"On the MV itself",[901,3321,3322,3325,3328],{},[919,3323,3324],{},"Schema flexibility",[919,3326,3327],{},"✅ CREATE OR REPLACE",[919,3329,3330],{},"❌ DROP + CREATE to change query",[901,3332,3333,3336,3339],{},[919,3334,3335],{},"Zero-downtime refresh",[919,3337,3338],{},"N\u002FA",[919,3340,3341],{},"✅ CONCURRENTLY (needs unique index)",[18,3343],{},[21,3345,3347],{"id":3346},"cross-engine-support","Cross-Engine Support",[895,3349,3350,3363],{},[898,3351,3352],{},[901,3353,3354,3357,3360],{},[904,3355,3356],{},"Engine",[904,3358,3359],{},"Materialized Views",[904,3361,3362],{},"Refresh",[914,3364,3365,3381,3392,3403,3414],{},[901,3366,3367,3370,3375],{},[919,3368,3369],{},"PostgreSQL",[919,3371,3372],{},[44,3373,3374],{},"CREATE MATERIALIZED VIEW",[919,3376,3377,3378],{},"Manual: ",[44,3379,3380],{},"REFRESH [CONCURRENTLY]",[901,3382,3383,3386,3389],{},[919,3384,3385],{},"Oracle",[919,3387,3388],{},"Mature MVs",[919,3390,3391],{},"Fast (incremental) or complete, auto on commit",[901,3393,3394,3397,3400],{},[919,3395,3396],{},"SQL Server",[919,3398,3399],{},"Indexed views",[919,3401,3402],{},"Auto-maintained on base-table changes",[901,3404,3405,3408,3411],{},[919,3406,3407],{},"MySQL",[919,3409,3410],{},"No native MVs",[919,3412,3413],{},"Emulate: table + scheduled job",[901,3415,3416,3419,3422],{},[919,3417,3418],{},"SQLite",[919,3420,3421],{},"No MVs",[919,3423,3424],{},"Emulate: table + triggers",[34,3426,3427],{"language":36},[38,3428,3430],{"className":40,"code":3429,"language":36,"meta":42,"style":42},"-- Emulating an MV in MySQL\u002FSQLite (no native MV support)\nCREATE TABLE sales_by_city_mv AS\n  SELECT c.city, SUM(o.amount) AS total\n  FROM customers c JOIN orders o ON c.id = o.customer_id\n  GROUP BY c.city;\n\n-- Refresh: truncate + re-insert (brief staleness window, or lock during refresh)\nTRUNCATE sales_by_city_mv;\nINSERT INTO sales_by_city_mv\n  SELECT c.city, SUM(o.amount) AS total\n  FROM customers c JOIN orders o ON c.id = o.customer_id\n  GROUP BY c.city;\n",[44,3431,3432,3437,3448,3476,3502,3514,3518,3523,3531,3538,3566,3592],{"__ignoreMap":42},[47,3433,3434],{"class":49,"line":50},[47,3435,3436],{"class":53},"-- Emulating an MV in MySQL\u002FSQLite (no native MV support)\n",[47,3438,3439,3441,3443,3446],{"class":49,"line":57},[47,3440,61],{"class":60},[47,3442,1764],{"class":60},[47,3444,3445],{"class":67}," sales_by_city_mv",[47,3447,71],{"class":60},[47,3449,3450,3452,3454,3456,3458,3460,3462,3464,3466,3468,3470,3472,3474],{"class":49,"line":74},[47,3451,77],{"class":60},[47,3453,1108],{"class":569},[47,3455,1052],{"class":80},[47,3457,1313],{"class":569},[47,3459,517],{"class":80},[47,3461,2836],{"class":569},[47,3463,1324],{"class":80},[47,3465,1060],{"class":569},[47,3467,1052],{"class":80},[47,3469,1065],{"class":569},[47,3471,1333],{"class":80},[47,3473,1140],{"class":60},[47,3475,2851],{"class":80},[47,3477,3478,3480,3482,3484,3486,3488,3490,3492,3494,3496,3498,3500],{"class":49,"line":90},[47,3479,93],{"class":60},[47,3481,1092],{"class":80},[47,3483,986],{"class":60},[47,3485,1087],{"class":80},[47,3487,1095],{"class":60},[47,3489,1108],{"class":569},[47,3491,1052],{"class":80},[47,3493,1055],{"class":569},[47,3495,1105],{"class":60},[47,3497,1049],{"class":569},[47,3499,1052],{"class":80},[47,3501,1387],{"class":569},[47,3503,3504,3506,3508,3510,3512],{"class":49,"line":99},[47,3505,1392],{"class":60},[47,3507,1108],{"class":569},[47,3509,1052],{"class":80},[47,3511,1313],{"class":569},[47,3513,163],{"class":80},[47,3515,3516],{"class":49,"line":117},[47,3517,121],{"emptyLinePlaceholder":120},[47,3519,3520],{"class":49,"line":124},[47,3521,3522],{"class":53},"-- Refresh: truncate + re-insert (brief staleness window, or lock during refresh)\n",[47,3524,3525,3528],{"class":49,"line":130},[47,3526,3527],{"class":60},"TRUNCATE",[47,3529,3530],{"class":80}," sales_by_city_mv;\n",[47,3532,3533,3535],{"class":49,"line":136},[47,3534,497],{"class":60},[47,3536,3537],{"class":80}," sales_by_city_mv\n",[47,3539,3540,3542,3544,3546,3548,3550,3552,3554,3556,3558,3560,3562,3564],{"class":49,"line":166},[47,3541,77],{"class":60},[47,3543,1108],{"class":569},[47,3545,1052],{"class":80},[47,3547,1313],{"class":569},[47,3549,517],{"class":80},[47,3551,2836],{"class":569},[47,3553,1324],{"class":80},[47,3555,1060],{"class":569},[47,3557,1052],{"class":80},[47,3559,1065],{"class":569},[47,3561,1333],{"class":80},[47,3563,1140],{"class":60},[47,3565,2851],{"class":80},[47,3567,3568,3570,3572,3574,3576,3578,3580,3582,3584,3586,3588,3590],{"class":49,"line":171},[47,3569,93],{"class":60},[47,3571,1092],{"class":80},[47,3573,986],{"class":60},[47,3575,1087],{"class":80},[47,3577,1095],{"class":60},[47,3579,1108],{"class":569},[47,3581,1052],{"class":80},[47,3583,1055],{"class":569},[47,3585,1105],{"class":60},[47,3587,1049],{"class":569},[47,3589,1052],{"class":80},[47,3591,1387],{"class":569},[47,3593,3594,3596,3598,3600,3602],{"class":49,"line":177},[47,3595,1392],{"class":60},[47,3597,1108],{"class":569},[47,3599,1052],{"class":80},[47,3601,1313],{"class":569},[47,3603,163],{"class":80},[18,3605],{},[21,3607,3609],{"id":3608},"tips-tricks","💡 Tips & Tricks",[3611,3612,3613,3623,3637,3646,3655,3676],"ul",{},[233,3614,3615,3618,3619,3622],{},[29,3616,3617],{},"Idiom"," — use views to ",[29,3620,3621],{},"name and centralize business definitions",": \"active customer,\" \"high-value order,\" \"current inventory\" — every query uses the same definition instead of each re-implementing the filter (and drifting). A view is a single source of truth for a derived concept.",[233,3624,3625,3628,3629,3632,3633,3636],{},[29,3626,3627],{},"Performance"," — use materialized views for ",[29,3630,3631],{},"expensive aggregations queried often",": an MV refreshed hourly turns a 10-second aggregation into a 1ms read. Always pair with ",[44,3634,3635],{},"CONCURRENTLY"," (and a unique index) for zero-downtime refresh in production.",[233,3638,3639,3642,3643,3645],{},[29,3640,3641],{},"Safety"," — use ",[44,3644,618],{}," on updatable views that expose a subset of rows: it prevents inserts\u002Fupdates that create rows invisible to the view, closing a subtle authorization gap.",[233,3647,3648,3650,3651,3654],{},[29,3649,249],{}," — prefer ",[29,3652,3653],{},"security views"," (expose a projection, grant on the view, revoke on the base table) over column-level privileges on the base table: views are simpler to reason about, compose, and survive base-table column renames.",[233,3656,3657,3660,3661,3664,3665,3667,3668,517,3670,3672,3673,3675],{},[29,3658,3659],{},"Debug"," — when a view query is slow, check whether predicate pushdown is working: ",[44,3662,3663],{},"EXPLAIN"," the view query with an outer ",[44,3666,151],{},"; if the outer predicate doesn't appear in the base-table scan, the view has a construct (",[44,3669,934],{},[44,3671,947],{},", window function, ",[44,3674,970],{},") that blocks pushdown.",[233,3677,3678,3642,3681,3683,3684,3687,3688,3691,3692,3696],{},[29,3679,3680],{},"Scheduling",[44,3682,1556],{}," for scheduled ",[44,3685,3686],{},"REFRESH CONCURRENTLY"," jobs: ",[44,3689,3690],{},"cron.schedule('name', '0 * * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name')",". Always create the unique index ",[3693,3694,3695],"em",{},"before"," scheduling the first concurrent refresh.",[18,3698],{},[21,3700,3702],{"id":3701},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[3611,3704,3705,3715,3721,3736,3753,3773,3797,3810,3820,3829,3838],{},[233,3706,3707,3710,3711,3714],{},[29,3708,3709],{},"Views aren't indexed",": a view has no storage, so you can't index it. Indexes go on the ",[29,3712,3713],{},"base tables","; the planner uses them when expanding the view. If a view query is slow, index the base table's columns.",[233,3716,3717,3720],{},[29,3718,3719],{},"Materialized views are stale until refreshed",": readers see the last-refreshed snapshot. If freshness matters, refresh frequently or trigger a refresh after data loads.",[233,3722,3723,3729,3730,3732,3733,3735],{},[29,3724,3725,3728],{},[44,3726,3727],{},"REFRESH"," (non-concurrent) blocks readers",": it takes an ",[44,3731,1515],{}," lock. Use ",[44,3734,3635],{}," (requires a unique index) for production MVs.",[233,3737,3738,3743,3744,3746,3747,3749,3750,1052],{},[29,3739,3740,3742],{},[44,3741,3635],{}," requires a unique index",": ",[44,3745,1650],{}," needs at least one ",[44,3748,1544],{}," index. Without it: ",[44,3751,3752],{},"ERROR: cannot refresh materialized view concurrently without a unique index",[233,3754,3755,3758,3759,517,3761,3763,3764,3766,3767,3769,3770,3772],{},[29,3756,3757],{},"Non-updatable views",": views with ",[44,3760,934],{},[44,3762,947],{},", aggregation, window functions, ",[44,3765,970],{},", or joins aren't auto-updatable. Use ",[44,3768,1014],{}," triggers or ",[44,3771,1018],{}," rules.",[233,3774,3775,3743,3780,3783,3784,3786,3787,3789,3790,3793,3794,3796],{},[29,3776,3777,3779],{},[44,3778,618],{}," scope",[44,3781,3782],{},"LOCAL"," checks only the view's own ",[44,3785,151],{},"; ",[44,3788,888],{}," (default) checks this view ",[3693,3791,3792],{},"and"," all views it's built on. Use ",[44,3795,888],{}," for layered security views.",[233,3798,3799,3743,3802,3805,3806,3809],{},[29,3800,3801],{},"View dependency",[44,3803,3804],{},"DROP TABLE customers CASCADE"," drops dependent views too — sometimes surprising. Check ",[44,3807,3808],{},"pg_depend"," before cascading drops.",[233,3811,3812,3815,3816,3819],{},[29,3813,3814],{},"View schema is frozen at creation",": if the base table adds a column, the view doesn't automatically include it. ",[44,3817,3818],{},"CREATE OR REPLACE VIEW"," can change the definition but can't remove\u002Freorder existing columns — only add new ones at the end.",[233,3821,3822,3825,3826,1052],{},[29,3823,3824],{},"MV storage",": an MV takes space proportional to its result set. A large MV with many indexes can be bigger than the base table. Monitor disk usage with ",[44,3827,3828],{},"pg_total_relation_relation_size('mv_name')",[233,3830,3831,3834,3835,3837],{},[29,3832,3833],{},"MV refresh failure is atomic",": if ",[44,3836,3727],{}," fails (e.g., a constraint violation in the new data), the MV keeps its old data. But the error must be fixed before the MV updates.",[233,3839,3840,3845,3846,3849,3850,1052],{},[29,3841,3842,3844],{},[44,3843,3818],{}," limitations",": can't remove or reorder columns, can't change column types — only add new columns at the end. For structural changes, ",[44,3847,3848],{},"DROP VIEW"," + ",[44,3851,3852],{},"CREATE VIEW",[18,3854],{},[21,3856,3858],{"id":3857},"spot-the-bug","🧠 Spot the Bug",[14,3860,3861,3862,3864],{},"A team creates a materialized view for a dashboard, refreshed nightly. The dashboard shows data up to 24 hours stale, and a stakeholder complains morning data isn't visible. The team tries ",[44,3863,1650],{}," and gets an error.",[34,3866,3867],{"language":36},[38,3868,3870],{"className":40,"code":3869,"language":36,"meta":42,"style":42},"CREATE MATERIALIZED VIEW dashboard_sales AS\n  SELECT city, SUM(amount) AS total FROM orders GROUP BY city;\n\n-- 24h later...\nREFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales;\n-- ERROR: cannot refresh materialized view \"dashboard_sales\" concurrently\n-- because it does not have a UNIQUE index that refutes updates\n",[44,3871,3872,3880,3905,3909,3914,3918,3923],{"__ignoreMap":42},[47,3873,3874,3876,3878],{"class":49,"line":50},[47,3875,61],{"class":60},[47,3877,2928],{"class":80},[47,3879,1302],{"class":60},[47,3881,3882,3884,3887,3889,3892,3894,3896,3898,3900,3902],{"class":49,"line":57},[47,3883,77],{"class":60},[47,3885,3886],{"class":80}," city, ",[47,3888,2836],{"class":569},[47,3890,3891],{"class":80},"(amount) ",[47,3893,1140],{"class":60},[47,3895,1426],{"class":80},[47,3897,342],{"class":60},[47,3899,1161],{"class":80},[47,3901,947],{"class":60},[47,3903,3904],{"class":80}," city;\n",[47,3906,3907],{"class":49,"line":74},[47,3908,121],{"emptyLinePlaceholder":120},[47,3910,3911],{"class":49,"line":90},[47,3912,3913],{"class":53},"-- 24h later...\n",[47,3915,3916],{"class":49,"line":99},[47,3917,3033],{"class":80},[47,3919,3920],{"class":49,"line":117},[47,3921,3922],{"class":53},"-- ERROR: cannot refresh materialized view \"dashboard_sales\" concurrently\n",[47,3924,3925],{"class":49,"line":124},[47,3926,1684],{"class":53},[3928,3929,3930,3934,3939,3966,4023,4032],"details",{},[3931,3932,3933],"summary",{},"Answer",[14,3935,3936,626],{},[29,3937,3938],{},"Two issues",[230,3940,3941,3947],{},[233,3942,3943,3946],{},[29,3944,3945],{},"Staleness"," — the MV is only refreshed nightly, so it's up to 24 hours stale. The team needs to refresh more frequently (hourly) or trigger a refresh after data loads.",[233,3948,3949,3954,3955,3957,3958,3961,3962,3965],{},[29,3950,3951,3953],{},[44,3952,3635],{}," error"," — ",[44,3956,1650],{}," requires a ",[29,3959,3960],{},"unique index"," on the MV. ",[44,3963,3964],{},"dashboard_sales"," has none.",[34,3967,3968],{"language":36},[38,3969,3971],{"className":40,"code":3970,"language":36,"meta":42,"style":42},"-- Fix 1: add a unique index on the grouping column(s)\nCREATE UNIQUE INDEX dashboard_sales_city_idx ON dashboard_sales(city);\n\n-- Fix 2: now concurrent refresh works without blocking readers\nREFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales;\n\n-- Fix 3: schedule hourly refresh via pg_cron\n-- SELECT cron.schedule('refresh-dashboard', '0 * * * *',\n--   'REFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales');\n",[44,3972,3973,3978,3991,3995,4000,4004,4008,4013,4018],{"__ignoreMap":42},[47,3974,3975],{"class":49,"line":50},[47,3976,3977],{"class":53},"-- Fix 1: add a unique index on the grouping column(s)\n",[47,3979,3980,3982,3984,3986,3988],{"class":49,"line":57},[47,3981,61],{"class":60},[47,3983,1462],{"class":60},[47,3985,3011],{"class":67},[47,3987,315],{"class":60},[47,3989,3990],{"class":80}," dashboard_sales(city);\n",[47,3992,3993],{"class":49,"line":74},[47,3994,121],{"emptyLinePlaceholder":120},[47,3996,3997],{"class":49,"line":90},[47,3998,3999],{"class":53},"-- Fix 2: now concurrent refresh works without blocking readers\n",[47,4001,4002],{"class":49,"line":99},[47,4003,3033],{"class":80},[47,4005,4006],{"class":49,"line":117},[47,4007,121],{"emptyLinePlaceholder":120},[47,4009,4010],{"class":49,"line":124},[47,4011,4012],{"class":53},"-- Fix 3: schedule hourly refresh via pg_cron\n",[47,4014,4015],{"class":49,"line":130},[47,4016,4017],{"class":53},"-- SELECT cron.schedule('refresh-dashboard', '0 * * * *',\n",[47,4019,4020],{"class":49,"line":136},[47,4021,4022],{"class":53},"--   'REFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_sales');\n",[14,4024,4025,4026,4028,4029,4031],{},"If ",[44,4027,1313],{}," isn't unique in the result (e.g., the query groups by more columns), add all grouping columns to the unique index. The unique index must cover a set of columns that uniquely identify each MV row — ",[44,4030,3635],{}," needs to identify rows to update them in place.",[14,4033,4034,3743,4037,4039],{},[29,4035,4036],{},"The lesson",[44,4038,3635],{}," requires a unique index (it needs to identify rows during a concurrent refresh). Create the unique index right after creating the MV, before the first concurrent refresh. And match the refresh frequency to the freshness requirement.",[18,4041],{},[21,4043,4044],{"id":3931},"Summary",[14,4046,4047,4048,4050,4051,4053],{},"You can now create views for encapsulation\u002Fsecurity\u002Fconsistency, make them updatable with ",[44,4049,618],{},", and build materialized views for expensive-query acceleration — with ",[44,4052,3635],{}," refresh (and its unique-index requirement) for zero-downtime. Next: date and time handling, with its time-zone traps.",[4055,4056,4057],"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 .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}",{"title":42,"searchDepth":57,"depth":57,"links":4059},[4060,4063,4068,4072,4073,4074,4075,4076,4077,4078,4079,4080],{"id":23,"depth":57,"text":24,"children":4061},[4062],{"id":227,"depth":74,"text":228},{"id":418,"depth":57,"text":419,"children":4064},[4065,4066,4067],{"id":617,"depth":74,"text":618},{"id":766,"depth":74,"text":767},{"id":892,"depth":74,"text":893},{"id":1269,"depth":57,"text":1270,"children":4069},[4070,4071],{"id":1479,"depth":74,"text":1480},{"id":1644,"depth":74,"text":1645},{"id":1732,"depth":57,"text":1733},{"id":2797,"depth":57,"text":2798},{"id":3072,"depth":57,"text":3073},{"id":3247,"depth":57,"text":3248},{"id":3346,"depth":57,"text":3347},{"id":3608,"depth":57,"text":3609},{"id":3701,"depth":57,"text":3702},{"id":3857,"depth":57,"text":3858},{"id":3931,"depth":57,"text":4044},"md",{},"\u002Fsql\u002F16-views-and-materialized-views",{"title":5,"description":16},"sql\u002F16-views-and-materialized-views","UV706EBXYFOmU7LdSX6t7UJAbaRQ-hx7s8CJJx7SgWw",1789924654689]