Saturday, October 29, 2011

Max Dump File Size


ผมจำได้ว่ามากกว่าหนึ่งครั้งที่เราเจอว่าวันดีคืนดี Oracle จะเกิด Internal Error ขึ้น และ Dump เอา Memory ที่ Error นั้นลงมาใน Trace File และที่ยิ่งแย่กว่านั้น Error ตัวนี้เกิดขึ้นอยู่เรื่อย ๆ อาจจะทุก ๆ ครึ่งหรือหนึ่งชั่วโมง ทำให้ดิสค์ของเราเต็มอย่างรวดเร็ว ระหว่างที่เรายังหาสาเหตุเพื่อหยุด Error ตัวนี้ไว้ไม่ได้ เราอาจจะต้องใช้วิธีการคอยตรวจสอบขนาดของข้อมูลในไดเรคทอรีที่เก็บ Trace ไฟล์เหล่านี้และคอยลบมันออกไปจากระบบ
จริง ๆ แล้วเราสามารถใช้พารามิเตอร์ MAX_DUMP_FILE_SIZE ในการกำหนดขนาดของ Trace File เช่น Trace File ใน bdump และ udump เป็นต้น โดยมีเงื่อนไขคือ เราต้องการให้ขนาด dump trace file ไม่โตเกินที่ต้องการ เรายอมรับที่ส่วนท้ายของไฟล์จะถูก truncate ได้ วิธีการนั้นง่ายมาก โดยเราสามารถกำหนดพารามิเตอร์ MAX_DUMP_FILE_SIZE แบบ Online โดยไม่ต้อง Restart ฐานข้อมูลได้ โดยมีขั้นตอนดังต่อไปนี้
- เช็คขนาด Trace File ที่ใหญ่ที่สุดที่ Oracle ให้มีได้โดยพิมพ์

SQL> show parameter max_dump_file_size

โดยหน่วยที่ได้จะเป็นจำนวน OS Block เช่นถ้า OS Block มีขนาด 512 ไบท์ แล้วค่า MAX_DUMP_FILE_SIZE เท่ากับ 50 แสดงว่าขนาดของ Trace File อาจจะใหญ่ใด้ถึง 25600 ไบท์ หาก MAX_DUMP_FILE_SIZE เท่ากับ UNLIMITED ก็แสดงว่ามีขนาดใหญ่ได้ไม่จำกัด
- กำหนดขนาดของ Trace File ได้โดยการพิมพ์คำสั่ง

SQL> alter system set max_dump_file_size = 50;

Friday, April 29, 2011

ความลึกลับของ Trigger ตอนที่ 2

ปัญหาการนำทริกเกอร์ไปใช้คือผู้พัฒนามักจะไม่รู้ผลข้างเคียงในแง่ของการ ควบคุมการใช้งานพร้อมกัน (Concurrency) และระดับความเป็นส่วนตัว (Isolation Level) ของแต่ละ Session บนฐานข้อมูล จุดแข็งที่สุดอันหนึ่งของ Oracle เหนือฐานข้อมูลค่ายอื่น ๆ คือ การอ่านข้อมูล (ใน Session หนึ่ง)ไม่ขัดขวางการเขียนข้อมูล (ในอีก Session หนึ่ง) และในทางกลับกันการเขียนข้อมูลก็ไม่ขัดขวางการอ่านข้อมูลเช่นกัน แต่คุณสมบัติอันนี้ถ้าผู้พัฒนาโปรแกรมไม่เข้าใจ อาจจะทำให้เกิดความผิดพลาดได้เมื่อมีการใช้ทริกเกอร์ โดยเฉพาะเมื่อพยายามนำทริกเกอร์มาเพื่อบังคับใช้กฎบางอย่าง ลองดูตัวอย่าง


สมมติว่าเรามีตารางสกุลเงิน ซึ่งมีสกุลเงินหลักเพียงสกุลเดียวของแต่ละประเทศดังนี้
Country Currency Primary_Currency
US USD Y
US USN N
US USS N

เราต้องการจะบังคับใช้กฎว่ามีเพียงหนึ่งสกุลเงินเท่านั้นที่จะสามารถเป็น สกุลเงินหลักได้ในแต่ละประเทศ (Primary_Currency = 'Y') เราสร้างทริกเกอร์ BEFORE UPDATE แบบทำงาน "ทีละบรรทัด" (For Each Row) ในตารางข้างบนไว้ และใช้ Autonomous Transaction เพื่อหลีกเลี่ยงปัญหา Mutating Error (จะกล่าวถึงในภายหลัง) เพื่อที่จะเช็คว่าประเทศนั้น ๆ มี Primary_currency='Y') แล้วหรือยัง โดยที่ทริกเกอร์มีหน้าตาแบบนี้

SQL> create or replace
2 trigger currencies_trigger
3 before update on currencies
4 for each row
5 declare
6 PRAGMA AUTONOMOUS_TRANSACTION;
7 l_cnt number;
8 begin
9 select count(*)
10 into l_cnt
11 from currencies
12 where primary_currency='Y'
13 and country=:new.country;
14 if (l_cnt>1)
15 then
16 raise_application_error
17 (-20000,'only on allowed');
18 end if;
19 end;
20 /
Trigger created.


มีหลายสิ่งผิดปกติกับทริกเกอร์อันนี้ สิ่งที่เห็นได้ชัดคือความจำเป็นที่มันจะต้องใช้ Autonomous Transaction ซึ่งถ้าไม่ใช้จะเกิดข้อผิดพลาด
ORA-04091: table SCOTT.CURRENCIES is mutating, trigger/function may not see it และ
ORA-06512: at "SCOTT.CURRENCIES_TRIGGER", line 4
ORA-04088: error during execution of trigger 'SCOTT.CURRENCIES_TRIGGER'
ซึ่งหมายถึง "คุณกำลังทำบางสิ่งบางอย่างที่ผิดกฎเบื้องต้น ซึ่งระบบฐานข้อมูล Oracle ไม่ยอมให้ทำแบบนั้น" ซึ่งถ้าระบบฯ ยอมให้ทริกเกอร์อ่านข้อมูลในตารางขณะที่ทำการอัพเดทไปทีละเรคคอร์ด ตัวทริกเกอร์ก็จะเห็นตารางที่ถูกอัพเดทไปทีละแถว ๆ เช่นถ้าเราอัพเดทข้อมูล 5 แถว (โดยคำสั่งอัพเดทคำสั่งเดียว) ทริกเกอร์ (แบบ For Each Row) ก็จะเห็นข้อมูลเปลี่ยนไปเมื่ออัพเดทแถวแรกเป็นจำนวนหนึ่งแถว ครั้งที่สองเปลี่ยนไปสองแถว ฯลฯ ซึ่งมันผิดปกติวิสัยของการมองเห็นตารางในแง่ของระบบฐานข้อมูล

สมมติว่าตาราง CURRENCIES ข้างบนมีทริกเกอร์ดังตัวอย่าง (CURRENCIES_TRIGGER) ซึ่งยอมให้อ่านข้อมูลจากตารางขณะที่ทำการอัพเดท ลองรันคำสั่งนี้ดู

SQL> update currencies
2 set primary_currency =
3 decode (currency,'USD','N','USN','Y')
4 where country = 'US'
5 and currency in ('USD','USN');


คำสั่งนี้น่าจะโอเค เมื่อรันเสร็จควรจะมี Primary Currency เพียงหนึ่งแถวในแต่ละประเทศ ปกติระบบฯที่ไม่มีทริกเกอร์จะถือว่านี่เป็นหนึ่งคำสั่ง และรันมันตามปกติ แต่เนื่องจาก Autonomous Transaction ทริกเกอร์จะสามารถอัพเดทไปในขณะที่เช็คแถวที่มี Primary Currency เป็น 'Y' ไปทีละแถวได้ เราจะพบว่าถ้าระบบรันโดยอัพเดทแถวที่มี Currency เป็น 'USD' ก่อนเป็นแถวแรกก็จะทำให้แถวนี้มี Primary Currency เป็น 'N' เมื่อรันแถวต่อไปซึ่ง Currency เป็น 'USN' ก็จะอัพเดทให้ Primary Currency ของ 'USN' เป็น 'Y' แต่ถ้าลำดับของการรันเริ่มจากแถวที่มี Currency เป็น 'USN' ก่อน (ปกติลำดับการปรากฎอยู่ของแถวของฐานข้อมูลตามแนวคิดของระบบฐานข้อมูลเชิง สัมพันธ์ จะไม่มีนัยสำคัญแต่อย่างใด คือข้อมูลที่เรา Insert เข้าไปอาจจะอยู่ที่ลำดับแถวใด ๆ ในตารางก็ได้) ผลที่ได้คือเมื่อทริกเกอร์รันครั้งแรกมันจะเห็นว่า 'USN' มี PRIMARY_CURRENCY='Y' (ของใหม่) ในขณะเดียวกัน 'USD' ก็มี PRIMARY_CURRENCY='Y' (ของเก่า) ซึ่งก็จะเกิด Error ซึ่งไม่ใช่เพราะคำสั่งนี้จริง ๆ แล้วต้องทำได้และสำเร็จ ดังนั้นทริกเกอร์ตัวนี้จะใช้ได้ (สำเร็จ) กับข้อมูลบางส่วน และบางส่วนจะใช้ไม่ได้ ถ้าสองฐานข้อมูลมีข้อมูลเหมือนกันแต่แถวอยู่ต่างลำดับกัน ซึ่งทำให้ลำดับการรันต่างกัน ก็จะให้ผลที่ต่างกัน และยอมรับไม่ได้ และนั่นเป็นเหตุผลว่าทำไมจะต้องมีการกำหนดข้อบังคับเกี่ยวกับการ Mutating Table เพื่อป้องกันความผิดพลาดของเราเอง แต่อาจจะโชคดีหรือไม่ดีที่ผู้พัฒนาโปรแกรมสามารถที่จะเลือกที่จะใช้ Autonomouse Transaction เพื่อหลีกเลี่ยงปัญหาดังกล่าวและเปิดช่องให้มีการคิวรีข้อมูลจากตารางในขณะ ที่ทริกเกอร์ที่อยู่บนตารางนั้นกำลังทำงานดังกับว่ามันอยู่คนละ Session กัน ทริกเกอร์จะไม่สามารถจะเห็นความเปลี่ยนแปลงที่เกิดขึ้นกับตารางจากคำสั่งอัพ เดทได้ (ดังเช่นที่ทริกเกอร์ไม่สามารถเห็นว่าในที่สุดแล้ว 'USD' จะถูกเซ็ท Primary Currencyเป็น 'N' ในท้ายที่สุด หากระบบฯ รันเอาตัว 'USN' ขึ้นก่อน) จริง ๆ แล้วหน้าที่ของทริกเกอร์คือการตรวจสอบการเปลี่ยนแปลงที่จะเกิดขึ้นกับข้อมูล (ว่าถูกต้องหรือไม่) กลับไปอ่านข้อมูลก่อนที่การเปลี่ยนแปลงจะเกิดขึ้น ซึ่งทำให้ไม่สามารถยอมรับได้

ผู้พัฒนาอาจจะหาหนทางจัดการกับปัญหานี้ โดยการใช้ Package และทริกเกอร์ 3 ตัว ดูเรื่องนี้ใน http://asktom.oracle.com/tkyte/Mutate ใน Oracle11g มีความสามารถใหม่เรียกว่า "Compound Trigger" ซึ่งสามารถใช้แทนแนวคิด Package และทริกเกอร์ 3 ตัวนี้ได้ ซึ่งผลลัพท์ที่ได้จะเหมือนกันดังต่อไปนี้

เทคนิคนี้ Package จะมี Global Variable เป็นประเภท PLSQL table (ตัวอย่างเช่น Array), Global Variable ตัวนี้จะถูกตั้งให้เป็นค่าว่าง โดยทริกเกอร์ BEFORE Statement หลังจากนั้น Global Variable จะถูกใส่ค่า Primary Keys โดยทริกเกอร์อีกตัว (FOR EACH ROW) ซึ่งเก็บทุกคีย์ของแถวที่ถูกอัพเดท หรือเก็บ rowid ของทุกแถวก็ได้ และในที่สุดทริกเกอร์ AFTER Statement จะตรวจสอบค่าใน Global Variable (Array) ซึ่งได้ผ่านการเปลี่ยนแปลงตามคำสั่งทั้งหมดทุกแถวแล้ว ซึ่งฟังดูน่าจะใช้ได้ แต่วิธีการ Package และทริกเกอร์ 3 ตัวก็ได้ผลกับเฉพาะถ้าคุณเป็นคนเดียวที่ใช้ระบบฯ อยู่และจะต้องไม่มีธุรกรรมมากกว่าหนึ่งธุรกรรม ณ ขณะใดขณะหนึ่ง ทริกเกอร์จำนวนมากที่มีลักษณะที่ทำงานได้ดีถ้าใช้งานโดยผู้ใช้เพียงคนเดียว แต่จะมีปัญหาถ้ามีผู้ใช้มากกว่าหนึ่งคนทำงานร่วมกัน ตัวอย่างเช่น ถ้าข้อมูลในตาราง Currencies เริ่มต้นด้วย primary_currency เป็น 'N' ทั้งหมด

Country Currency Primary_Currency
US USD N
US USN N
US USS N

ใน Session แรก เรารันคำสั่งนี้

SQL> update currencies
2 set primary_currency = 'Y'
4 where country = 'US'
5 and currency = 'USD';


โดยการใช้วิธีการทริกเกอร์ 3 ตัว ทริกเกอร์ BEFORE จะเซ็ทค่า Globa Variable ให้เป็นค่าว่าง ต่อมาทริกเกอร์ FOR EACH ROW จะทำงานและจะจำประเทศที่ถูกแก้ไขเอาไว้ใน Global Variable หลังจากนั้นทริกเกอร์ AFTER จะทำงานโดยการตรวจสอบว่าใน Country ที่ระบุไว้ใน Global Variable มีกี่ตัวที่มีค่า Primary Currency เป็น 'Y' ถ้ามีตัวเดียวก็ถือว่าใช้ได้ ซึ่งแนวคิดทริกเกอร์ 3 ตัวนี้น่าจะใช้ได้ แต่ถ้าเกิดมี Session อื่น หลังจากที่ Session แรกทำการอัพเดทแล้วแต่ยังไม่ได้ Commit รันคำสั่ง

SQL> update currencies
2 set primary_currency = 'Y'
4 where country = 'US'
5 and currency = 'USN';


ทริกเกอร์ก็จะทำงานของมัน ทริกเกอร์ AFTER จะทำการนับจำนวน Primary Currency ที่เป็น 'Y' เหมือน ๆ กันและจะไม่เห็นว่ามี Primary Currency ที่เป็น 'Y' ในอีกหนึ่ง Session ที่ยังไม่ได้ Commit ผลที่ได้คือทริกเกอร์ AFTER ก็จะพบว่าคำสั่งนี้ไม่ทำให้มีแถวที่มี Primary Currency เป็น 'Y' มากกว่าหนึ่งแถว แต่เมื่อทั้งสองแถวทำการ Commit จะพบว่าทั้ง Currency 'USD' และ 'USN' ต่างก็มี Primary Currency เป็น 'Y' นั่นหมายความว่ากฎที่เราพยายามที่จะบังคับไม่สามารถทำงานได้ถูกต้อง การจะทำให้กฎนี้ทำงานได้จะต้องทำการ "Serialized" (ยอมให้การกระทำใด ๆ ต่อตารางต้องทำทีละหนึ่ง การกระทำที่ตามมาจะต้องรอให้การกระทำก่อนหน้าเสร็จก่อน) หรือทำการล๊อคเรคคอร์ดที่เราต้องการอัพเดทนั้นไว้ แต่กฎนี้จะต้องสามารถใช้ได้กับคำสั่ง Insert ด้วยซึ่งทำให้เราจะต้องล๊อคกันในระดับตารางเลยทีเดียวเพื่อป้องกันไม่ให้ผู้ อื่นเข้ามา Insert ลงในตารางเดียวกันในขณะที่เราทำการ Insert ได้

อาจ จะกล่าวได้ว่าถ้ามีการใช้ทริกเกอร์ที่ทำการบังคับใช้กฎข้ามแถว (เช่นกฎที่ระบุว่าในทุก ๆ แถวของคอลัมน์หนึ่งจะมีค่าที่ระบุเกิดขึ้นในคอลัมน์นั้นได้เพียงครั้งเดียว ดังตัวอย่าง Primary Currency ที่กล่าวมาแล้ว) แล้วไม่มีการใช้คำสั่ง LOCK TABLE เพื่อทำการ Serialized การเข้าถึงตารางแล้ว ผลที่ได้น่าจะมีความผิดพลาดอยู่อย่างค่อนข้างแน่ชัด ซึ่งส่งผลกับความถูกต้องของข้อมูลอย่างหลีกเลี่ยงไม่ได้ การจะบังคับใช้กฎกับตารางโดยใช้ทริกเกอร์นั้นจึงเป็นเรื่องซับซ้อน และมักจะมีความจำเป็นต้องมีการล๊อคในระดับตารางดังได้กล่าวมาแล้ว

(ยังไม่จบนะครับ ยังมีตอนต่อไป...ขอขอบคุณที่ให้ความสนใจและโปรดติดตามตอนต่อไปนะครับ)

แปลจาก "The Trouble with Triggers"
โดย Tom Kyte, Oracle Magazine Volume XXII / Number 5 September/October2008, ASK TOM

Monday, February 14, 2011

Shared Pool Memory Structure


Oracle Shared Pool เป็นคุณสมบัติสำคัญที่ช่วยในการแชร์ให้ User กลุ่มใหญ่สามารถใช้ Object ที่ซับซ้อนร่วมกันได้ ในอดีตบ่อยครั้งที่ DBA ใช้เวลาส่วนใหญ่เรียนรู้เกี่ยวกับการจัดการหน่วยความจำ Shared Pool   และตั้งค่าตลอดจนปรับแต่ง (Tune) การใช้งาน Shared Pool ต่อมาจึงมีคุณสมบัติที่เรียกว่า Automatic Shared Memory Management (ASMM) ที่เริ่มมีใน Oracle 10gR1 ซึ่งน่าจะสามารถช่วยในการแก้ปัญหานี้ โดยได้สร้างกลไกในการปรับแต่งตัวเองแบบอัตโนมัติในการตั้งค่าส่วนความจำ SGA ซึ่งรวมถึง Buffer Cache และ Shared Pool

วัตถุประสงค์ของ Shared Pool
Shared Pool ได้ถือกำเนิดขึ้นโดยเป็นคุณสมบัติหนึ่งของ Oracle นับแต่ Oracle version 7 โดยวัตถุประสงค์เริ่มแรกคือเป็นที่เก็บหน่วยความจำสำหรับ SQL และ PL/SQL เพื่อเอาไว้ใช้ร่วมกัน ซึ่งต่อมามันได้พัฒนามารองรับหน้าที่หลักในการเป็นที่พัก (Cache) สำหรับ Cursors ที่ใช้ร่วมกันระหว่าง Session ที่เชื่อมต่อกับฐานข้อมูล Oracle อีกด้วย
กล่าวโดยหน่วยที่พื้นฐานที่สุด   Shared Pool คือ ที่พักของ Metadata โดยที่ Buffer Cache เป็นที่สำหรับพักของข้อมูล, Shared Pool เป็นที่สำหรับพัก Object อันซับซ้อนที่จะบอกว่าข้อมูลถูกเก็บไว้ที่ใด และข้อมูลนั้นมีความสัมพันธ์กับข้อมูลอื่นอย่างไร และจะดึงออกมาใช้อย่างไร แบบไหน
การใช้ Shared Pool ส่วนใหญ่ก็เพื่อใช้ในการ Execute คำสั่ง SQL และ Package PL/SQL ที่ใช้ร่วมกัน แต่เพื่อที่จะสร้าง Cursor หรือคอมไพล์ PL/SQL Procedure ระบบจะต้องรู้ข้อมูลเกี่ยวกับ Object ที่ถูกอ้างถึงใน SQL หรือ PL/SQL ที่กำลังจะถูกคอมไพล์ เช่น เพื่อที่จะสร้าง Cursor จากคำสั่ง Select ง่าย ๆ จากหนึ่งตาราง เราจำเป็นต้องรู้ Metadata เกี่ยวกับตารางเช่น ชื่อคอลัมน์, ชนิดของข้อมูล, อินเด็กซ์ และข้อมูลสถิติที่ใช้กับ Optimizer,  ข้อมูล Metadata เหล่านี้จะถูกพักไว้ใน Shared Pool โดยแยกอิสระออกจาก Cursor และส่วนที่ใช้เก็บ Program (เช่น Package เป็นต้น)

องค์ประกอบของ Shared Pool
โดยทั่วไปสิ่งที่ DBA มักจะสนใจเป็นพิเศษคือการจัดสรรหน่วยความจำซึ่งเป็นที่พักสำหรับของ Object ต่าง ๆ ดังได้กล่าวมาแล้ว เนื่องจากการสร้าง Object ใหม่ใน Shared Pool นั้น สิ้นเปลืองทรัพยากร และกระทบต่อขนาดและประสิทธิภาพการทำงานของระบบ   การจัดสรรหน่วยความจำให้กับ Object ซึ่งสามารถถูกสร้างใหม่ได้นั้นบางครั้งเราใช้คำว่า ‘Recreatable’

V$SGASTAT
วิธีที่ง่ายที่สุดในการตรวจสอบสิ่งที่อยู่ใน Shared Pool ณ ขณะใดขณะหนึ่งคือการคิวรี Fixed View ชื่อ V$SGASTAT เนื่องจาก Oracle จะคอยรันเก็บจำนวนของการจัดสรรหน่วยความจำทั้งหมดใน Shared Pool เอาไว้ การคิวรีข้อมูลจากวิวนี้จึงไม่ได้ใช้ทรัพยากรมากและไม่มีผลกระทบต่อระบบงานบนเครื่อง Production เท่าไรนัก ใน Oracle10gR2 วิวตัวนี้ได้รับการปรับปรุงให้แสดงผลได้เป็นหน่วยละเอียดขึ้น
ตัวอย่าง:

SELECT * FROM(SELECT NAME, BYTES/(1024*1024) MB FROM V$SGASTAT WHERE POOL = 'shared pool' ORDER BY BYTES DESC)WHERE ROWNUM <= 10;

NAME                               MB
-------------------------- ----------
ktcmvcb                    6856.96056
KGH: NO ACCESS             1610.02487
sql area                    571.81263
free memory                502.999672
CCursor                    301.799118
PCursor                     176.69886
library cache               112.56636
kglsim object batch        77.3775787
gcs resources              75.5597076
sql area:PLSQL             51.2854691


การ Select จาก V$SGASTAT หลายครั้ง แสดงให้เห็นว่าองค์ประกอบบางตัวจะมีขนาดที่คงที่

Cache Dynamics
ขนาดขององค์ประกอบเหล่านี้จะเคลื่อนไหวขึ้น ๆ ลง ๆ ตามการสร้าง SQL และ PL/SQL ใหม่ ๆ ภายในหน่วยความจำ Cache ซึ่งแปรผันตามความเปลี่ยนแปลงของ Application องค์ประกอบเหล่านี้แต่ละตัวจะมีส่วนเกี่ยวข้องกับกระบวนการสร้าง SQL (และ PL/SQL) นี้ในระดับหนึ่ง โดยที่ในบางขณะองค์ประกอบจะคืนหน่วยความจำออกมาเพื่อที่องค์ประกอบอื่นสามารถนำไปใช้ต่อ
shared_pool_comp

วิวในระดับ Object (Object-level View)
ดังกล่าวมาข้างต้นจะเห็นได้ชัดว่าแต่ละองค์ประกอบมี Object อาศัยอยู่ได้เป็นร้อย ๆ อย่างเช่นใน Shared Pool อาจจะมี Cursor หนึ่งตัว, ตารางหนึ่งตาราง, วิว หรือหนึ่ง Package
ปกติแล้ว Objects เหล่านี้มีขนาดไม่ใหญ่นัก แต่ก็สามารถที่จะโตได้ไปจนถึงหลาย MB ขึ้นอยู่กับประเภทของ Objects กับ Attribute ที่กำหนด (เช่นถ้ากำหนดคอลัมน์ให้เป็น CHAR ขนาดใหญ่ ก็จะกินพื้นที่มากเป็นต้น) ไม่น่าแปลกใจเลยที่ Package Body ที่เก็บ Procedure จำนวนมากจะกินพื้นที่ใน Shared Pool มากกว่าคำสั่ง Select ง่าย ๆ
ขนาดและรายละเอียดอื่น ๆ ของ Object แต่ละชิ้นใน Shared Pool สามารถหาได้จากการคิวรี Fixed View ชื่อ V$DB_OBJECT_CACHE (10g ขึ้นไป – หรือจากวิวที่เกี่ยวข้องเช่น V$SQL และ V$SQLAREA) คิวรีข้างล่างนี้จะแสดง Object ที่ใหญ่ที่สุดสองตัวแรกจากแต่ละ Namespace ใน V$DB_OBJECT_CACHE

SELECT * FROM (SELECT ROW_NUMBER () over (PARTITION BY NAMESPACE ORDER BY SHARABLE_MEM DESC) ROW_within , NAMESPACE, SHARABLE_MEM, SUBSTR(NAME,1,40) NAME FROM V$DB_OBJECT_CACHE ORDER BY SHARABLE_MEM DESC) WHERE ROW_WITHIN <= 2 ORDER BY NAMESPACE, ROW_WITHIN;

ROW_WITHIN NAMESPACE           SHARABLE_MEM NAME
---------- ------------------- ------------ -----------------------
1          CLUSTER                     2794 C_OBJ#_INTCOL#
2          CLUSTER                     1684 SMON_SCN_TO_TIME
1          RSRC PLAN                   5117 SYS_GROUP
2          RSRC PLAN                      0 OTHER_GROUPS
1          RULESET                    34367 ALERT_QUE_R


Memory Heaps
มีความเชื่อผิด ๆ ยอดนิยมอันหนึ่งว่าเราจะเจอปัญหาถ้าเราพยายามจัดสรรหน่วยความจำให้กับ Object ขนาดใหญ่เหล่านี้ ในกรณีสุดโต่งก็อาจจะเป็นจริงได้ แต่ก็ไม่เสมอไป ทำไมหรือ? เราต้องเข้าใจก่อนว่าแต่ละ Object ไม่ได้ประกอบด้วยการใช้หน่วยความจำขนาดใหญ่ก้อนเดียว แต่จะถูกแบ่งออกหน่วยความจำที่เป็นอิสระจากกันขนาดเล็กกว่าเป็นส่วน ๆ เรียกว่า Heap   จำนวนของ Heap ต่อ หนึ่ง Object ขึ้นอยู่กับประเภทของ Object เช่น Cursor ของ SQL ตัวหนึ่งมี 2 Heap: Heap ตัวเล็กสำหรับ Library Cache Metadata (ปกติเรียก Heap 0) และตัวที่ใหญ่กว่าเก็บรูปแบบการรัน  (Executable Representation) ของ Cursor (ปกติเรียก SQLAREA) ความเข้าใจนี้มีประโยชน์ต่อการเข้าใจการทำงานภายในของพารามิเตอร์ init.ora บางตัวเช่น SESSION_CACHED_CURSORS
แม้ว่าตัวของ Heap เองอาจจะมีขนาดค่อนข้างใหญ่ แต่ตัวของมันเองก็ยังประกอบด้วยหน่วยความจำหนึ่งก้อน (Chunk) หรือมากกว่า แผนภูมิต่อไปนี้แสดงเลย์เอาท์ของ Heap สำหรับ Shared Pool Object ส่วนใหญ่

shared pool comp2

การใช้พื้นที่ใน Shared Pool (Space Allocation in the Shared Pool)
สิ่งแรกที่ต้องเข้าใจคือ Oracle ต้องการหน่วยความจำที่ติดกันต่อเนื่องเพื่อนำมาใช้งาน เช่น ถ้ามีความต้องการใช้หน่วยความจำก้อนขนาด 4K, Heap Manager (ตัวที่คอยจดบัญชีพื้นที่ว่างใน Shared Pool) ไม่สามารถที่จะเอาหน่วยความจำก้อนขนาด 3K มาหนึ่งก้อน แล้วเอาอีกหนึ่งก้อนที่มีขนาด 1K มาได้ ถ้าไม่มีหน่วยความจำก้อนขนาดที่ต้องการ,  Process จะเริ่มคืนกลุ่ม (Batch) ของ Heap จาก Shared Pool และคอยเช็คดูหลังจากคืนทีละกลุ่ม Batch จนกว่าหน่วยความจำที่คืนมาจะมีขนาดใหญ่เพียงพอ
แต่ถ้าท้ายที่สุด Shared Pool ก็ยังไม่มีพื้นที่เหลือพอตามที่ต้องการ ก็จะเกิดข้อผิดพลาด ORA-4031  อย่างไรก็ตาม ก่อนจะขึ้นข้อผิดพลาด Heap Manager จะพยายามคืนหน่วยความจำให้มากเท่าที่จะทำได้ก่อนที่จะเลิก; ทำซ้ำ ๆ กันไปตลอดทั้ง Shared Pool LRU (LRU – Least Recently Used ปลายของหน่วยความจำด้านที่มีการเรียกใช้ซ้ำน้อยที่สุด) เป็นจำนวน 5 ครั้งเพื่อคืนหน่วยความจำและมอบก้อนข้อมูลที่ต่อเนื่องกันตามที่ถูกร้องขอมา ก่อนที่จะขึ้น 4031  ส่วนหน่วยความจำที่คืนมาจะถูกรวมกันเป็นก้อน (Coalesced) ถ้าสามารถทำได้โดยอัตโนมัติ

SQL & PL/SQL ใน Shared Pool
ทุก ๆ SQL Cursor และ PL/SQL Package ถูกจัดสรรหน่วยความจำเป็นก้อน ๆ ให้ โดยมีขนาดก้อนละประมาณ 4K  ยิ่ง Cursor มีขนาดใหญ่ จำนวนของการใช้ก้อนหน่วยความจำ (4K) ก็จะมากขึ้นด้วย และมักไม่ค่อยจะมีขนาดก้อนใหญ่กว่า 4K นอกจากว่ามีความต้องการพิเศษจาก SQL Optimizer หรือจาก PL/SQL runtime engine
เนื่องจากเมื่อตอนจัดสรรหน่วยความจำ จะจัดเป็น 4K เท่า ๆ กัน ดังนั้นตอนจะคืนหน่วยความจำก็จะคืนเป็นหน่วยของ 4K เช่นกัน ดังนั้นจึงไม่น่าจะเป็นปัญหาถ้าจะสร้าง Cursor หรือ PL/SQL Package ตัวใหม่ ถ้ามี Cursor หรือ Package เก่า ๆ พร้อมที่จะถูกคืนออกมา

การแปล (Parsing)
เนื่องจากเรากำลังพูดถึงเรื่องการจัดสรรหน่วยความจำให้กับ SQL และ PL/SQL สิ่งที่จะข้ามไปเสียมิได้คือเรื่องของการแปล (Parsing) เพราะถ้าปราศจากการแปลใหม่ (Hard Parsing) ก็ไม่จำเป็นจะต้องมีการจัดสรรหน่วยความจำจาก Shared Pool เลย การแปลใหม่มีผลกับระบบในหลายระดับ แม้แต่ก่อนที่จะมีการสร้าง Cursor, SQL จะต้องถูกแปล และทำให้อยู่ในรูปที่มีประสิทธิภาพในการรันสูงสุด (Optimized) กระบวนการนี้ต้องใช้ทรัพยากรใน Library Cache และ Row Cache (Dictionary Cache) และอาจจะทำให้เกิดการโหลด Object ใน Library Cache และ Row Cache ซ้ำแล้วซ้ำอีก ซึ่งก่อให้เกิดภาระกับ Library Cache, Row Cache และองค์ประกอบใน Shared Pool และเป็นตัวอธิบายว่าทำไมอัตราการแปลใหม่ (Hard Parse) จึงมักจะมาพร้อม ๆ กับ Latch Contention
เมื่อเปรียบเทียบกันการแปลทบทวน (Soft Parse) จะกินทรัพยากรน้อยกว่ามาก อย่างไรก็ตามแม้แต่แปลทบทวน (Soft Parse) ก็ไม่ได้หมายความว่าจะไม่ได้กินทรัพยากรเลย เนื่องจากการแปลทบทวน (Soft Parse) ระบบจะต้องมองหาคำสั่ง SQL ใน Shared Pool, ทำการล๊อค และตรึง (Pin) Object ที่จำเป็น และตอนรัน Cursor นั้นต้องมีการทำงานของ Latch มาเกี่ยวข้องด้วยเสมอ และแม้ว่าค่าใช้จ่ายของ Soft Parse จะน้อยกว่า Hard Parse หลายเท่า แต่ความถี่ในการใช้มักจะมากกว่าหลายเท่าเช่นกัน นี่เป็นเหตุผลว่าทำไมจึงมีออปชั่นในการเก็บ (Cache) SQL เหล่านี้ไว้ในหน่วยความจำทั้งบนฝั่ง Server และ Client เพื่อช่วยให้ระบบที่มีการส่งผ่านข้อมูลจำนวนมาก สามารถทำงานได้มากขึ้นหรือใหญ่ขึ้น

การตั้งค่า Shared Pool
อย่างไรก็ตามการกำหนดค่าตั้งต้นของ Shared Pool มักจะเป็นเรื่องยากเนื่องมาจากธรรมชาติที่เปลี่ยนแปลงไปอยู่เรื่อย ๆ ของ Workload ของ Application และการจะกำหนดหน่วยความจำที่จะใช้ก็ยากที่จะกำหนดตายตัวลงไป แม้แต่เมื่อเราได้ระดับประสิทธิภาพของ Shared Pool ที่ยอมรับได้แล้ว ณ ขณะหนึ่งกับค่าที่ตั้งไว้ แต่เมื่อเวลาผ่านไป การเปลี่ยนแปลง Application, การเปลี่ยนแปลงจำนวนผู้ใช้งาน และการเปลี่ยนแปลงของค่าพารามิเตอร์เริ่มต้น (Initialization Parameters) ต่างก็สามารถส่งผลในทางลบกับระบบได้ทั้งสิ้น
สมมติว่าระบบหนึ่งได้รันมาจนอยู่ตัวแล้วในระดับหนึ่ง ตอนต่อไปนี้จะให้ทิปสำคัญว่าด้วยควรจะดูอะไรเพื่อประเมินความต้องการ  SGA
การกำหนดขนาดของ Shared Pool และการ Tune เมื่อเกิดปัญหาขึ้นควรจะถูกจัดการต่างกันโดย การ Tune นั้นควรจะถูกขับเคลื่อนด้วยคอขวดที่ใหญ่ที่สุดในระบบ ซึ่งกำหนดได้จากการตรวจสอบเหตุการณ์รอคอย (Wait Events) ร่วมกับ ข้อมูล Time Model (V$SYS_TIME_MODEL) ข้อมูลนี้สามารถใช้กำหนดบริเวณที่เป็นปัญหาใหญ่ที่สุดซึ่งควรจะสนใจเป็นอันดับแรก

การกำหนดขนาดของ Shared Pool ที่เล็กเกินไป
การมี Shared Pool ที่มีขนาดเล็กเกินไป อาจทำให้เกิดปัญหาดังต่อไปนี้
  • ORA-04031 out of shared pool memory
  • Library cache lock and pin contention (มีการ Reload ของ Library Cache เป็นจำนวนมาก)
  • Row cache enqueue contention (มีการ Reload ของ Row Cache เป็นจำนวนมาก)
  • Latch Contention (shared pool, library cache, row cache)
เราจะมาขยายความกันตามลำดับดังนี้

ORA-04031
ข้อผิดพลาดตัวนี้ไม่ควรจะปรากฎใน Application Log, alert log หรือ Trace Files ใด ๆ ข้อผิดพลาด 4031 จะปรากฎใน Alert Log เมื่อมันมีผลต่อการทำงานของ Background Process (เช่น PMON), 4031 ไม่ใช่ข้อผิดพลาดภายใน (Internal Errors) ดังนั้นจึงสามารถถูกดักจับ และจัดการโดย Application ได้ (แม้ว่าจะไม่แนะนำ)
นับแต่ Oracle10gR1 เป็นต้นมา Trace File 4031 จะถูกเขียนลงไดเรคทอรี USER_DUMP_DEST หรือ BACKGROUND_DUMP_DEST, Trace File เหล่านี้มีประโยชน์ในการวิเคราะห์ธรรมชาติของปัญหา

Library Cache Reloads
Shared Pool ควรจะมีขนาดใหญ่พอ เพื่อหลีกเลี่ยง Overhead จากการ Reload Object เดิม ๆ อยู่บ่อย ๆ
ก่อนการกำหนดขนาดของ Shared Pool (เพื่อหลีกเลี่ยงการ Reload) คุณควรจะเช็คว่าการ Reload ไม่ได้เป็นผลมาจากการ Invalidation (การที่ Object เกิดข้อผิดพลาดบางประการทำให้ต้องโหลดใหม่) ถ้าจำนวนของ Invalidation มีมากจนมีนัยสำคัญ (มากกว่า 20%) เมื่อเทียบกับจำนวน Reload ทั้งหมด ก็ให้ตรวจสอบสาเหตุและแก้ปัญหาการ Invalidation ก่อน คิวรีต่อไปนี้แสดงว่ามีการ Reload และ Invalidation จำนวนเท่าใดตั้งแต่ Start Instance ขั้นมา

SELECT NAMESPACE, PINS, PINS-PINHITS LOADS, RELOADS,INVALIDATIONS, 100 * (RELOADS - INVALIDATIONS) / (PINS-PINHITS) "%RELOADS" FROM V$LIBRARYCACHE WHERE PINS >0 ORDER BY NAMESPACE;

NAMESPACE             PINS      LOADS    RELOADS INVALIDATIONS   %RELOADS
 
--------------- ---------- ---------- ---------- ------------- ----------
BODY             104188273       1052        603             0 57.3193916
CLUSTER               5613        140         79             0 56.4285714
INDEX             20675002       6151       4369             0 71.029101
SQL AREA         627459674    9140453     238610        175744 .687777728
TABLE/PROCEDURE  265612275      86391      37095             0 42.9385005
TRIGGER             483898        316        232             0 73.4177215


แม้ว่าจะไม่มีคำแนะนำที่แน่นอนตายตัว, Library Cache Reloads ควรจะมีปริมาณคิดเป็นเปอร์เซ็นต์เพียงเล็กน้อย (ประมาณ 10%) เมื่อเทียบกับจำนวนการโหลดทั้งหมด

Row Cache Misses
ถ้า Library Cache มีจำนวน Reloads สูง (หรือเรียกว่ามี Library Cache Misses สูงนั่นเอง) มักจะส่งผลให้ Dictionary Cache Misses สูงเช่นกัน ลองดูคิวรีบน V$ROWCACHE เพื่อดูข้อมูล Dictionary Cache Misses

SELECT PARAMETER , SUM(GETS) GETS , SUM(GETMISSES) GETMISSES, SUM("COUNT") NUM, SUM(USAGE) USAGE FROM V$ROWCACHE WHERE GETMISSES > 0 GROUP BY PARAMETER ORDER BY PARAMETER;

PARAMETER                    GET   GETMISSES        NUM      USAGE
 
---------------------- ---------- ---------- ---------- ----------
dc_awr_control              16460        415          1          1
dc_constraints               2001        677          0          0
dc_database_links        11383620         89          5          5
dc_files                     4248       1313          0          0
dc_global_oids             513894       1084         34         34
dc_hintsets                     1          1          0          0
dc_histogram_data       213983893      87858       5959       5959
dc_histogram_defs        61303102      97242       3983       3983
dc_object_grants         13188017       1827        250        250
dc_object_ids           301224033      22093        930        930
dc_objects               73246110      30001       1089       1089
dc_profiles              16354351         20          3          3
dc_rollback_segments     45923272        644        584        584
dc_segments              30937535      34097       1230       1230
dc_sequences                13754       3036         11         11
dc_table_scns            46760420    1505551          1          1
dc_tablespace_quotas        12121       1225         22         22
dc_tablespaces           70787039        103        102        102
dc_usernames             66902952        306         24         24
dc_users                413738271       1048        103        103
kqlsubheap_object               1          1          1          1
outstanding_alerts         314186     306164          5          5


แปลจาก "Shared Pool Memory Structures", http://wiki.oracle.com

Saturday, January 22, 2011

Materialized View: Refresh Fast VS Refresh Complete


Materialized View เป็นเครื่องมือในการทำสำรองข้อมูลขึ้นอีกชุด เพื่อใช้ประโยชน์ในด้านต่าง ๆ ที่เห็นได้ชัดคือการสำรองข้อมูลที่อยู่บน Database ตัวหนึ่งไปไว้บน Database อีกตัวหนึ่ง
เวลาเราสร้าง Materialized View จะมีสามออปชั่นหลัก ๆ ที่เกี่ยวกับการอัพเดทข้อมูลปลายทางให้เหมือนต้นทางคือ
1. เอาเฉพาะส่วนต่าง (Refresh Fast)
2. เอาทั้งหมดมาทับ (Refresh Complete)
3. เอาเฉพาะส่วนต่าง ถ้าไม่ได้ก็เอาทั้งหมดมาทับ (Refresh Force)
บทความนี้จะแสดงให้เห็นว่าเมื่อใดควรจะใช้ Fast Refresh และเมื่อใดควรใช้ Complete Refresh เราเริ่มจากการสร้างตารางทดสอบ TM เพื่อใช้เป็น Master จากนั้นเพิ่มข้อมูลให้กับตารางโดยการ Insert ... Select จนในตารางมีข้อมูลมากพอสมควร (ผมเพิ่มข้อมูลเข้าในตารางให้มีมากเพื่อจะได้เห็นความแตกต่างของเวลาตอน Refresh ได้ชัด หากผู้อ่านนำไปทดสอบอาจจะไม่ต้องใช้ข้อมูลมากขนาดนี้ก็น่าจะพอเห็นความแตกต่างของเวลาที่ใช้ในการ Refresh ครับ)

SQL> create table tm as select * from all_objects;
Table created
SQL> insert into tm select * from tm;
....
....
.....
SQL> select count(*) from tm;
COUNT(*)
----------
867668

เราจะสร้าง Materialized View Log ที่จำเป็นสำหรับการทำ Fast Refresh ก่อน จากนั้นจึงสร้าง Materialized View ชื่อ TM_MV ซึ่งเป็น Mview ที่ใช้วิธีการ Refresh แบบ Fast Refresh เนื่องจากตารางต้นทางไม่มี PK เราจึงต้องสร้าง Materialized View Log และ Materialized View บน ROWID แทน สังเกตเมื่อตอนสร้างเสร็จใหม่ ๆ Materialized View Log ไม่มีข้อมูลเลย (0 Row)

SQL> create materialized view log on tm with rowid;
Materialized view log created
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
0
SQL> create materialized view tm_mv refresh fast with rowid as select * from tm;
Materialized view created
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867668

เราจะเริ่มทดสอบการทำ Fast Refresh โดยการ Insert ข้อมูลเข้าตาราง TM ซึ่งเป็น Master ของ Materialized View เป็นจำนวน 100 แถว สังเกตว่าในตาราง mlog$_tm ซึ่งเกิดจากการ Create Materialized View Log จะมีข้อมูลเพิ่มขึ้นมาด้วยจำนวนที่เท่า ๆ กัน หรือไม่ก็ใกล้เคียงกัน สังเกตว่าการ Refresh ข้อมูลจำนวน 100 เรคคอร์ด ใช้เวลาน้อยมาก (0.078 วินาที) เมื่อ Refresh เสร็จ จำนวนของข้อมูลใน Materialized View Log กลับไปเป็น 0

SQL> insert into tm select * from tm where rownum < 101;
100 rows inserted
SQL> commit;
Commit complete
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
100
SQL> exec dbms_mview.refresh('TM_MV');
PL/SQL procedure successfully completed
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867768
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
0

คราวนี้ลองมาดูว่าถ้ามีการ Refresh ข้อมูลจำนวนมากแล้ว การใช้ Fast Refresh ยังคง "Fast" อยู่หรือไม่ เราจะ Delete ข้อมูลทั้งหมดออกก่อนแล้วจึง Insert ใหม่เข้าไปในตาราง Master จะเห็นว่าเมื่อเรา Refresh ข้อมูลจะใช้เวลาถึง 465 วินาทีทีเดียว ให้สังเกตจำนวนเรคคอร์ดใน Materialized View Log ไปด้วยทุกครั้งที่ทำ DML (Insert, Update, Delete) บนตาราง Master ซึ่งจะเพิ่มขึ้นเมื่อเรา Delete และ Insert

ก่อนจะลบข้อมูลออกจากตาราง TM ผมได้ทำสำรองข้อมูลไว้ก่อน จะได้ไม่ต้องมาเริ่มต้นกระบวนการสร้างข้อมูลใหม่อีกครั้ง โดยวิธีที่ใช้ในการสำรองข้อมูลคือ CTAS (Create Table As Select) แบบนี้ CREATE TABLE TM2 AS SELECT * FROM TM

SQL> select count(*) from tm;
COUNT(*)
----------
867768
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867768
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
0
SQL> set timing on
SQL> delete from tm;
867768 rows deleted
Executed in 139.375 seconds
SQL> commit;
Commit complete
Executed in 0.078 seconds
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
867768
Executed in 15.657 seconds
SQL> insert into tm select * from tm2;
867668 rows inserted
Executed in 143.578 seconds
SQL> commit;
Commit complete
Executed in 0 seconds
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
1735436
Executed in 16.422 seconds
SQL> select count(*) from tm;
COUNT(*)
----------
867668
Executed in 14.657 seconds
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867768
Executed in 1.765 seconds
SQL> exec dbms_mview.refresh('TM_MV');
PL/SQL procedure successfully completed
Executed in 465.156 seconds
SQL> set timing off

คราวนี้มาลองใช้ Materialized View แบบ Complete Refresh ดูบ้าง เรา Drop Materialized View TM_MV และ Materialized View Log บนตาราง TM ออก แล้วจึงสร้าง Materialized View TM_MV ใหม่ให้เป็นแบบ Complete Refresh
SQL> drop materialized view tm_mv;
Materialized view dropped
SQL> drop materialized view log on tm;
Materialized view log dropped
SQL> select count(*) from tm;
COUNT(*)
----------
867668
SQL> create materialized view tm_mv refresh complete with rowid as select * from tm;
Materialized view created
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867668

จากนั้นเราจะลบข้อมูลในตาราง TM ซึ่งเป็นตาราง Master แล้วจึง Insert เข้าไปใหม่ แล้วจึง Refresh Materialized View TM_MV จะเห็นว่า Complete Refresh ใช้เวลาเพียง 68 วินาที

SQL> set timing on
SQL> delete from tm;
867668 rows deleted
Executed in 69.875 seconds
SQL> insert into tm select * from tm2;
867668 rows inserted
Executed in 40.468 seconds
SQL> commit;
Commit complete
Executed in 0.015 seconds
SQL> exec dbms_mview.refresh('TM_MV');
PL/SQL procedure successfully completed
Executed in 67.593 seconds
SQL> set timing off

คราวนี้ลองมาดูผลการ Refresh ถ้าตาราง Master ใช้วิธีการ Truncate แล้วจึงค่อย Insert จะเห็นว่าเวลาที่ใช้ในการ Refresh Complete ไม่ต่างกัน แต่หากถ้าเราใช้ Fast Refresh (ซึ่งต้องใช้ร่วมกับ Materialized View Log) เราจะพบ Error ORA-12034: materialized view log on ... younger than last refresh

SQL> truncate table tm;
Table truncated
SQL> insert into tm select * from tm2;
867668 rows inserted
SQL> commit;
Commit complete
SQL> set timing on
SQL> exec dbms_mview.refresh('TM_MV');
PL/SQL procedure successfully completed
Executed in 63.218 seconds
SQL> set timing off
SQL> drop materialized view tm_mv;
Materialized view dropped
SQL> create materialized view log on tm with rowid;
Materialized view log created
SQL> create materialized view tm_mv refresh fast with rowid as select * from tm;
Materialized view created
SQL> select count(*) from tm;
COUNT(*)
----------
867668
SQL> select count(*) from tm_mv;
COUNT(*)
----------
867668
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
0
SQL> exec dbms_mview.refresh('TM_MV');
PL/SQL procedure successfully completed
SQL> set timing on
SQL> truncate table tm;
Table truncated
Executed in 2.14 seconds
SQL> insert into tm select * from tm2;
867668 rows inserted
Executed in 106.031 seconds
SQL> commit;
Commit complete
Executed in 0 seconds
SQL> select count(*) from mlog$_tm;
COUNT(*)
----------
867668
Executed in 15.593 seconds
SQL> exec dbms_mview.refresh('TM_MV');
begin dbms_mview.refresh('TM_MV'); end;
ORA-12034: materialized view log on "SCOTT"."TM" younger than last refresh
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 820
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 877
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 858
ORA-06512: at line 1

กล่าวโดยสรุป
1. ใช้ Complete Refresh เมื่อมีการเปลี่ยนแปลงข้อมูลบนตาราง Master เป็นจำนวนมาก (ตาราง Master ที่มีลักษณะเป็น Batch Process) และใช้ Complete Refresh เมื่อตารางมีการเปลี่ยนแปลงแบบล้างข้อมูลออกทั้งหมดแล้วแทนที่ด้วยข้อมูลใหม่ รวมทั้งกรณี Truncate ด้วย
2. ใช้ Fast Refresh เมื่อตาราง Master มีการเปลี่ยนแปลงข้อมูลเพียงเล็กน้อย และอาจจะมีการเปลี่ยนแปลงบ่อย ๆ (ตาราง Master มีลักษณะเป็น Transaction Process)