View และ Materialized View
query บางตัวยาว หรือถูกใช้หลายที่ หรือ join และ aggregate แบบเดียวกันทุกครั้ง แทนที่จะคัดลอก SQL ไปทั่ว PostgreSQL ให้คุณตั้งชื่อให้ query แล้วใช้เหมือน table: view ส่วน materialized view ไปอีกขั้น คือเก็บ row ผลลัพธ์ของ query ไว้ แลกความสดกับความเร็ว
ทุกอย่างด้านล่างรันบน PostgreSQL 18.6 (postgres:18.6, aarch64, Docker Desktop บน Mac, กันยายน 2026) fence sql รันจากบนลงล่างในฐานข้อมูลเปล่าหนึ่งตัว ผลลัพธ์คัดลอกจาก psql การรันสอง session เป็นของจริง (โปรเซส psql สองตัวที่เริ่มพร้อมกัน) ตัวเลขเวลามาจาก Mac เครื่องนี้
flowchart LR Q["Your query on the view"] --> V["View: stored SELECT, merged into your query"] V --> T["Base tables, read now"] Q2["Your query on the materialized view"] --> M["Materialized view: stored rows"] M -. "REFRESH" .-> T
ชุดข้อมูล
หัวข้อที่มีชื่อว่า “ชุดข้อมูล”ชุดข้อมูลมาตรฐานของ module อีกครั้ง สังเกตว่า books มี copies_sold ซึ่ง view ใช้
-- sql/setup.sqlCREATE TABLE authors ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, born int);CREATE TABLE books ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, author_id bigint REFERENCES authors (id), published int, copies_sold int NOT NULL DEFAULT 0);CREATE TABLE sales ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, book_id bigint REFERENCES books (id), region text NOT NULL, copies int NOT NULL);INSERT INTO authors (name, born) VALUES ('Mara Linde', 1968), ('Owen Pryce', 1975), ('Sela Vance', 1990);INSERT INTO books (title, author_id, published, copies_sold) VALUES ('Quiet Harbors', 1, 2003, 450), ('The Glass Orchard', 1, 2018, 1250), ('Tidal Notes', 2, 2011, 600), ('Unlinked Draft', NULL, 2020, 0);INSERT INTO sales (book_id, region, copies) VALUES (2, 'East', 900), (3, 'West', 400), (1, 'West', 400), (3, 'East', 200), (2, 'West', 150), (1, 'East', 50);view คือ query ที่บันทึกไว้
หัวข้อที่มีชื่อว่า “view คือ query ที่บันทึกไว้”CREATE VIEW บันทึก SELECT ไว้ใต้ชื่อ มันไม่เก็บ row ทุกครั้งที่คุณ query view PostgreSQL รัน query ที่บันทึกไว้กับข้อมูลปัจจุบัน ตัวนี้สรุปผู้เขียนแต่ละคน โดยเก็บผู้เขียนที่ไม่มีหนังสือไว้:
-- sql/create-view.sqlCREATE VIEW author_sales ASSELECT a.id AS author_id, a.name, count(b.id) AS books, coalesce(sum(b.copies_sold), 0) AS copies_soldFROM authors AS aLEFT JOIN books AS b ON b.author_id = a.idGROUP BY a.id, a.name;-- sql/query-view.sqlSELECT * FROM author_sales ORDER BY name;const res = await pool.query(` SELECT * FROM author_sales ORDER BY name`);cur.execute(""" SELECT * FROM author_sales ORDER BY name""")rows = cur.fetchall()rows, err := conn.Query(ctx, ` SELECT * FROM author_sales ORDER BY name`)defer rows.Close()let rows = sqlx::query( "SELECT * FROM author_sales ORDER BY name",).fetch_all(&pool).await?; author_id | name | books | copies_sold-----------+------------+-------+------------- 1 | Mara Linde | 2 | 1700 2 | Owen Pryce | 1 | 600 3 | Sela Vance | 0 | 0(3 rows)$ (each snippet run once against the dataset above; multi-line driver output joined onto one line)[view] Node.js (pg 8.23.0): [ { author_id: '1', name: 'Mara Linde', books: '2', copies_sold: '1700' }, { author_id: '2', name: 'Owen Pryce', books: '1', copies_sold: '600' }, { author_id: '3', name: 'Sela Vance', books: '0', copies_sold: '0' } ][view] Python (psycopg 3.3.6): [(1, 'Mara Linde', 2, 1700), (2, 'Owen Pryce', 1, 600), (3, 'Sela Vance', 0, 0)][view] Go (pgx v5.11.0): [1 Mara Linde 2 1700] [2 Owen Pryce 1 600] [3 Sela Vance 0 0][view] Rust (sqlx 0.9.0): 1 Mara Linde 2 1700 / 2 Owen Pryce 1 600 / 3 Sela Vance 0 0[refresh] Node.js: [ { region: 'East', sales: '4', copies: '2150' }, { region: 'West', sales: '4', copies: '955' } ][refresh] Python: [('East', 4, 2150), ('West', 4, 955)][refresh] Go / Rust: East 4 2150 / West 4 955เพราะ view ไม่เก็บอะไร ผลของมันจึงสดเสมอ และการวาง plan ก็ไม่เสียเพิ่ม: view ถูกขยายเข้าไปใน query ของคุณก่อนการ optimize จึงไม่ใช่กล่องดำที่ planner ต้องรันก่อน ขอผู้เขียนหนึ่งคนแล้วตัวกรองไปถึง table ต้นทาง:
-- sql/view-plan.sql-- the view is not a black box: its query is merged into yours, and the filter reaches the base tableEXPLAIN (COSTS OFF) SELECT * FROM author_sales WHERE name = 'Owen Pryce'; QUERY PLAN--------------------------------------------------------------- GroupAggregate Group Key: a.id -> Sort Sort Key: a.id -> Hash Right Join Hash Cond: (b.author_id = a.id) -> Seq Scan on books b -> Hash -> Seq Scan on authors a Filter: (name = 'Owen Pryce'::text)(10 rows)Filter: (name = 'Owen Pryce') อยู่บนการ scan authors ใต้ join: GROUP BY ของ view ไม่ได้ขัดขวางการผลักลงไป view เป็นเครื่องมือตั้งชื่อและใช้ซ้ำ ไม่ใช่ฟีเจอร์ด้านประสิทธิภาพ (docs)
view จำ column ไม่ได้จำเจตนาของคุณ
หัวข้อที่มีชื่อว่า “view จำ column ไม่ได้จำเจตนาของคุณ”SELECT * ในนิยามของ view ถูกขยายครั้งเดียวตอนสร้าง เพิ่ม column ให้ table ทีหลังแล้ว view ไม่แสดง:
-- sql/select-star-frozen.sqlCREATE VIEW book_titles AS SELECT * FROM books;ALTER TABLE books ADD COLUMN isbn text;SELECT * FROM book_titles LIMIT 1;SELECT * FROM books LIMIT 1;CREATE VIEWALTER TABLE id | title | author_id | published | copies_sold----+---------------+-----------+-----------+------------- 1 | Quiet Harbors | 1 | 2003 | 450(1 row)
id | title | author_id | published | copies_sold | isbn----+---------------+-----------+-----------+-------------+------ 1 | Quiet Harbors | 1 | 2003 | 450 |(1 row)book_titles ยังมีห้า column; books ตอนนี้มีหก เขียน column ที่ต้องการให้ชัด
view ที่ update ได้ และ WITH CHECK OPTION
หัวข้อที่มีชื่อว่า “view ที่ update ได้ และ WITH CHECK OPTION”view บน table เดียวที่ไม่มี aggregate, DISTINCT หรือ set operation เป็น update ได้อัตโนมัติ: INSERT, UPDATE และ DELETE บน view เปลี่ยน table ต้นทาง WITH CHECK OPTION เก็บ row ที่คุณเขียนผ่าน view ให้อยู่ใน WHERE ของ view เอง:
-- sql/updatable-view.sqlCREATE VIEW recent_books ASSELECT id, title, author_id, published, copies_soldFROM booksWHERE published >= 2010WITH CHECK OPTION;SELECT id, title, published FROM recent_books ORDER BY id;BEGIN;-- a simple view is updatable: this changes the base tableUPDATE recent_books SET copies_sold = copies_sold + 10 WHERE title = 'Tidal Notes' RETURNING id, title, copies_sold;ROLLBACK;CREATE VIEW id | title | published----+-------------------+----------- 2 | The Glass Orchard | 2018 3 | Tidal Notes | 2011 4 | Unlinked Draft | 2020(3 rows)
BEGIN id | title | copies_sold----+-------------+------------- 3 | Tidal Notes | 610(1 row)
UPDATE 1ROLLBACK-- @expect-error 44000INSERT INTO recent_books (title, author_id, published) VALUES ('Old Book', 1, 1999);ERROR: new row violates check option for view "recent_books"DETAIL: Failing row contains (5, Old Book, 1, 1999, 0, null).ถ้าไม่มี WITH CHECK OPTION การ insert นั้นสำเร็จ และ row ใหม่จะแค่มองไม่เห็นผ่าน view
Dependency
หัวข้อที่มีชื่อว่า “Dependency”view พึ่งพา table ต้นทาง และ PostgreSQL รู้เรื่องนี้ การ drop table ที่ view ใช้ถูกปฏิเสธ และข้อความลิสต์ทุกตัวที่พึ่งพา:
-- @expect-error 2BP01DROP TABLE books;ERROR: cannot drop table books because other objects depend on itDETAIL: constraint sales_book_id_fkey on table sales depends on table booksview author_sales depends on table booksview book_titles depends on table booksview recent_books depends on table booksHINT: Use DROP ... CASCADE to drop the dependent objects too.security_invoker
หัวข้อที่มีชื่อว่า “security_invoker”โดยค่าเริ่มต้น view รันด้วยสิทธิ์ของ เจ้าของ view ไม่ใช่ผู้เรียก นั่นเป็นฟีเจอร์ (ซ่อน column และ row ได้) และเป็นกับดัก (role ที่อ่าน table ไม่ได้อ่าน view ได้) PostgreSQL 15 เพิ่ม security_invoker: view จึงรันด้วยสิทธิ์ของผู้เรียก role ที่มี SELECT บน view เท่านั้น:
-- sql/security-invoker.sqlCREATE ROLE report_reader NOLOGIN;GRANT USAGE ON SCHEMA public TO report_reader;GRANT SELECT ON author_sales TO report_reader; -- the view only, no table privileges
-- default: the view runs with its OWNER's privileges, so the reader gets dataSET ROLE report_reader;SELECT name, books FROM author_sales ORDER BY name;RESET ROLE;
-- security_invoker (PostgreSQL 15+): the view runs with the CALLER's privilegesALTER VIEW author_sales SET (security_invoker = true);CREATE ROLEGRANTGRANTSET name | books------------+------- Mara Linde | 2 Owen Pryce | 1 Sela Vance | 0(3 rows)
RESETALTER VIEWทีนี้ role เดียวกัน หลัง security_invoker = true:
-- @expect-error 42501SET ROLE report_reader;SELECT name, books FROM author_sales ORDER BY name;SETERROR: permission denied for table authorsเมื่อมี security_invoker ผู้เรียกต้องมีสิทธิ์บน table ต้นทางด้วย และนโยบาย row-level security ใช้กับผู้เรียกไม่ใช่เจ้าของ (docs) ล้าง role สาธิต:
-- sql/security-cleanup.sqlRESET ROLE;DROP OWNED BY report_reader;DROP ROLE report_reader;ALTER VIEW author_sales RESET (security_invoker);materialized view เก็บผลลัพธ์ไว้
หัวข้อที่มีชื่อว่า “materialized view เก็บผลลัพธ์ไว้”CREATE MATERIALIZED VIEW รัน query ตอนนี้และเก็บ row การอ่านมันถูกเท่ากับอ่าน table และเก่าเท่ากับการ refresh ครั้งล่าสุด เรา insert ยอดขายใหม่ และ materialized view ไม่รู้เรื่องจนกว่าเราจะ REFRESH:
-- sql/materialized-view.sqlCREATE MATERIALIZED VIEW sales_by_region ASSELECT region, count(*) AS sales, sum(copies) AS copiesFROM salesGROUP BY region;SELECT * FROM sales_by_region ORDER BY region;-- the source changes, the materialized view does notINSERT INTO sales (book_id, region, copies) VALUES (3, 'East', 1000);SELECT 'view says' AS source, region, copies FROM sales_by_region WHERE region = 'East'UNION ALLSELECT 'live table', region, sum(copies) FROM sales WHERE region = 'East' GROUP BY region;REFRESH MATERIALIZED VIEW sales_by_region;SELECT 'after refresh' AS source, region, copies FROM sales_by_region WHERE region = 'East';SELECT 2 region | sales | copies--------+-------+-------- East | 3 | 1150 West | 3 | 950(2 rows)
INSERT 0 1 source | region | copies------------+--------+-------- view says | East | 1150 live table | East | 2150(2 rows)
REFRESH MATERIALIZED VIEW source | region | copies---------------+--------+-------- after refresh | East | 2150(1 row)view says 1150, live table 2150: materialized view คือ snapshot เป็นเครื่องมือที่เหมาะกับ aggregate แพง ๆ ที่ผู้อ่านหลายคนต้องการและเก่าเล็กน้อยได้ (dashboard, รายงานรายคืน)
WITH NO DATA สร้างนิยามโดยไม่รัน query การอ่านก่อน refresh ครั้งแรกเป็น error:
-- sql/with-no-data.sqlCREATE MATERIALIZED VIEW empty_mv AS SELECT region, sum(copies) AS copies FROM sales GROUP BY region WITH NO DATA;-- @expect-error 55000SELECT * FROM empty_mv;ERROR: materialized view "empty_mv" has not been populatedHINT: Use the REFRESH MATERIALIZED VIEW command.REFRESH บล็อกผู้อ่าน CONCURRENTLY ไม่บล็อก
หัวข้อที่มีชื่อว่า “REFRESH บล็อกผู้อ่าน CONCURRENTLY ไม่บล็อก”REFRESH MATERIALIZED VIEW ธรรมดาถือ lock ACCESS EXCLUSIVE ขณะเขียน row ที่เก็บใหม่ ผู้อ่านจึงรอ REFRESH MATERIALIZED VIEW CONCURRENTLY สร้างเนื้อหาใหม่ข้าง ๆ และสลับเฉพาะส่วนต่าง ผู้อ่านจึงอ่าน row เก่าต่อไปได้ มันต้องมี unique index บน materialized view ไม่เช่นนั้นปฏิเสธ:
-- @expect-error 55000REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region;ERROR: cannot refresh materialized view "public.sales_by_region" concurrentlyHINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.-- sql/concurrently.sqlCREATE UNIQUE INDEX sales_by_region_region_idx ON sales_by_region (region);-- sql/refresh-concurrently.sqlINSERT INTO sales (book_id, region, copies) VALUES (1, 'West', 5);REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region;SELECT * FROM sales_by_region ORDER BY region;await pool.query('REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region');const res = await pool.query('SELECT * FROM sales_by_region ORDER BY region');cur.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region")cur.execute("SELECT * FROM sales_by_region ORDER BY region")rows = cur.fetchall()_, err := conn.Exec(ctx, "REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region")rows, err2 := conn.Query(ctx, "SELECT * FROM sales_by_region ORDER BY region")defer rows.Close()sqlx::query("REFRESH MATERIALIZED VIEW CONCURRENTLY sales_by_region") .execute(&pool) .await?;let rows = sqlx::query("SELECT * FROM sales_by_region ORDER BY region") .fetch_all(&pool) .await?;INSERT 0 1REFRESH MATERIALIZED VIEW region | sales | copies--------+-------+-------- East | 4 | 2150 West | 4 | 955(2 rows)แต่ละ driver รัน refresh และอ่านเดียวกันหนึ่งครั้ง ผลลัพธ์อยู่ในบรรทัด [refresh] ของบล็อกใกล้ต้นหน้านี้
เพื่อเห็นความต่าง เราต้องมี refresh ที่ช้าพอจะสังเกต materialized view นี้จึง ช้าโดยตั้งใจ: pg_sleep(1) รันหนึ่งครั้งต่อกลุ่ม ทำให้ทุก refresh ใช้ราว 2 วินาที
-- sql/slow-mv.sql-- an artificially slow materialized view: pg_sleep makes the refresh take about 2 secondsCREATE MATERIALIZED VIEW slow_mv ASSELECT region, sum(copies) AS copies, pg_sleep(1) IS NOT NULL AS sleptFROM sales GROUP BY region;CREATE UNIQUE INDEX slow_mv_region_idx ON slow_mv (region);session A refresh มัน ขณะที่ session B ลองอ่านหลังจากนั้น 0.3 วินาที:
-- session A: script\timing onREFRESH MATERIALIZED VIEW slow_mv;-- session A: psql outputTiming is on.REFRESH MATERIALIZED VIEWTime: 2011.220 ms (00:02.011)
-- session B (starts 0.3 s later): script\o /dev/nullSELECT pg_sleep(0.3);\o\timing onSELECT region, copies FROM slow_mv ORDER BY region;-- session B: psql outputTiming is on. region | copies--------+-------- East | 2150 West | 955(2 rows)
Time: 1710.883 ms (00:01.711)B รอ 1.7 วินาที คือเวลาที่เหลือของ refresh ของ A ทีนี้แบบเดียวกันด้วย CONCURRENTLY:
-- session A: script\timing onREFRESH MATERIALIZED VIEW CONCURRENTLY slow_mv;-- session A: psql outputTiming is on.REFRESH MATERIALIZED VIEWTime: 2016.039 ms (00:02.016)
-- session B (same script as above): psql outputTiming is on. region | copies--------+-------- East | 2150 West | 955(2 rows)
Time: 1.892 msA ใช้เวลาเท่ากัน (2.0 วินาที) แต่ B อ่าน row เก่าได้ใน 1.9 ms ราคาของ CONCURRENTLY คือ unique index และงานเพิ่มต่อการ refresh (มันคำนวณ diff) (docs)
เลือกระหว่างสองแบบ
หัวข้อที่มีชื่อว่า “เลือกระหว่างสองแบบ”| คำถาม | View | Materialized view |
|---|---|---|
| ผลสดเสมอไหม? | ใช่ | ไม่ สดเท่า REFRESH ครั้งล่าสุด |
| เร็วกว่า query ดิบไหม? | ไม่ (รวมเข้า query) | เร็ว: row คำนวณไว้แล้ว |
| ใช้พื้นที่เก็บไหม? | ไม่ | ใช่ บวก index ที่คุณเพิ่ม |
| ใคร refresh? | ไม่มีใคร ไม่ต้อง | คุณ: ตารางเวลา trigger หรือ pg_cron |
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”ผู้ช่วย AI ตั้ง job รายคืนเพื่อ refresh ตารางสรุป “โดยไม่บล็อกผู้อ่าน” ด้วย CONCURRENTLY:
🐛 หา bug ในโค้ดนี้
-- AI comment: "nightly cron: refresh without blocking readers"CREATE MATERIALIZED VIEW daily_totals ASSELECT region, sum(copies) AS copies FROM sales GROUP BY region;
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_totals;เฉลย
มันรัน และล้มทุกคืน: CONCURRENTLY ต้องมี unique index บน materialized view และผู้ช่วยไม่เคยสร้าง materialized view ถูกสร้าง (ส่วนนั้นทำงาน) แล้ว refresh ถูกปฏิเสธ job บันทึก error ที่ไม่มีใครอ่าน และข้อมูลเก่าลง:
-- sql/stb-setup.sqlCREATE MATERIALIZED VIEW daily_totals ASSELECT region, sum(copies) AS copies FROM sales GROUP BY region;-- @expect-error 55000REFRESH MATERIALIZED VIEW CONCURRENTLY daily_totals;ERROR: cannot refresh materialized view "public.daily_totals" concurrentlyHINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.วิธีแก้คือ unique index (ที่นี่ region ไม่ซ้ำต่อ row) แล้ว refresh ทำงานขณะที่ผู้อ่านอ่านต่อได้:
-- sql/stb-fixed.sqlCREATE UNIQUE INDEX daily_totals_region_idx ON daily_totals (region);REFRESH MATERIALIZED VIEW CONCURRENTLY daily_totals;SELECT * FROM daily_totals ORDER BY region;CREATE INDEXREFRESH MATERIALIZED VIEW region | copies--------+-------- East | 2150 West | 955(2 rows)Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”view คือ นิยาม: ชื่อของ query ที่ขยายเข้าไปในทุก query ที่ใช้มัน materialized view คือ table ที่จำได้ว่าตัวเองสร้างอย่างไร: row ถูกเก็บ และ REFRESH รันนิยามใหม่ ทุกคำถามเกี่ยวกับมันตามมาจากนี้: view สดเสมอและเร็วเท่า query ของมัน; materialized view อ่านเร็วและเก่าเท่า refresh ครั้งล่าสุด กินพื้นที่ มี index ได้ และต้องมีคน refresh
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- ใช้ view เพื่อตั้งชื่อและใช้ query ซ้ำ เพื่อเปิดอินเทอร์เฟซคงที่เหนือ table ที่เปลี่ยน หรือเพื่อจำกัดสิ่งที่ role มองเห็น
- ระบุ column ให้ชัด;
SELECT *ใน view ถูกตรึงตอนสร้าง - ตั้ง
security_invoker = trueบน view ที่ควรเคารพสิทธิ์ของผู้เรียกและ row-level security - เพิ่ม index ให้ materialized view เหมือน table;
REFRESH ... CONCURRENTLYต้องใช้ตัวที่ unique - ตั้งเวลา refresh และเฝ้าดู: refresh ที่ล้มเงียบ ๆ ทิ้งข้อมูลเก่าไปตลอด
- view ซ้อน view ได้ แต่ละชั้นถูกขยาย กองลึกอาจสร้าง plan ใหญ่
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| View | สดเสมอ ไม่ใช้พื้นที่ ใช้ซ้ำและจัดการสิทธิ์ได้ดี | ไม่เร็วขึ้น; การเปลี่ยน table ต้นทางอาจทำให้พัง |
| Materialized view | อ่าน query แพงได้เร็ว | เก่าระหว่าง refresh; พื้นที่; refresh ธรรมดาบล็อกผู้อ่าน 1.7 วินาที |
REFRESH ... CONCURRENTLY | ไม่บล็อกผู้อ่าน (1.9 ms) | ต้องมี unique index; ช้ากว่า งานมากกว่าต่อการ refresh |
view แบบ security_invoker | สิทธิ์และ RLS ของผู้เรียกมีผล | ผู้เรียกต้องมีสิทธิ์บน table ต้นทาง |
WITH CHECK OPTION | การเขียนไม่หลุดจาก view | เฉพาะ view ที่ update ได้ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “view เก็บผลลัพธ์ไว้ใน cache” มันไม่เก็บ row; รันใหม่ทุกครั้ง (และรวมเข้า query ของคุณ)
- “view ทำให้ query ช้าเร็วขึ้น” ตัวกรองไปถึง table ต้นทาง แต่งานเท่ากับ query ดิบ
- “
SELECT *ใน view ตาม column ใหม่” มันถูกขยายตอนสร้าง:book_titlesไม่ได้isbn - “materialized view refresh เอง” เฉพาะเมื่อคุณ
REFRESH: 1150 ยังเป็น 1150 ขณะ table บอก 2150 - “
REFRESH ... CONCURRENTLYใช้ได้เลย” มันต้องมี unique index ไม่เช่นนั้นล้มด้วย55000 - “role ต้องเข้าถึง table เพื่ออ่านผ่าน view” ไม่ใช่โดยค่าเริ่มต้น: view รันในฐานะเจ้าของ นอกจากตั้ง
security_invoker