Sunday, November 22, 2009

Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 2 จบ)

ข้อเขียนที่แล้วได้แนะนำการใช้งานและประโยชน์ของ Regular Expression สำหรับตอนนี้เราจะมาพูดถึงไวยกรณ์ของ Regular Expression บน Oracle กันนะครับ


source_string คือ Text ที่เราต้องการจัดการ ส่วนมากจะเป็นชื่อคอลัมน์ในตาราง และเป็นคอลัมน์ที่มีประเภทข้อมูลเป็น Char, Varchar2, Nchar, Nvarchar2, Clob, NClob

pattern คือ รูปแบบทีเป็นไวยกรณ์ของ Regular Expression ที่เอาไว้จัดการ Text ใน source_string

match_parameter คือตัวที่กำหนดพฤติกรรมในการจับคู่ตาม pattern ที่กำหนด ตัวอย่างเช่น
'i' หมายถึงการจับคู่ตาม pattern ไม่สนใจเรื่องตัวอักษรใหญ่หรือเล็ก (Case Insensitive)
'c' หมายถึงการจับคู่ตาม pattern ที่จะต้องตรงกันตามอักษรตัวเล็ก-ใหญ่ (Case Sensitive)
'n' ทำให้สัญญลักษณ์จุด (Period)ใน Regular Expression ซึ่งโดยปกติหมายถึงตัวอักษรใด ๆ ก็ได้หนึ่งตัว กลายเป็นหมายถึงตัวขึ้นบรรทัดใหม่ (New Line Character)
'm' ทำให้เครื่องหมาย ^ และ $ เป็นจุดเริ่มต้นและจุดสิ้นสุดบรรทัดใน source_string ซึ่งหมายถึงทำให้มอง source_string เป็นหลาย ๆ บรรทัดได้ ถ้าไม่ได้กำหนดตัวนี้ source_string จะถูกมองเห็นเป็นบรรทัดเดียว และ ^ กับ $ จะหมายถึงเป็นจุดเริ่มต้นกับสิ้นสุดของ Text ทั้งหมดใน source_string

ถ้าเราระบุ match_parameter ที่ขัดแย้งกันเช่น 'ic' จะใช้ค่าตัวหลังสุดคือ 'c' ซึ่งหมายถึง Case Sensitive ตัวอักษรจะต้องเป็นตัวเล็ก-ใหญ่ตรงกัน หรือถ้าเราไม่ระบุ match_parameter เลย ค่าดีฟอลต์จะมีผลดังนี้
- การจับคู่จะเป็น Case Sensitive หรือไม่ขึ้นอยู่กับตัวแปร NLS_SORT
- จุด (Period) จะหมายถึงตัวใด ๆ หนึ่งตัว ไม่ได้แทนตัวขึ้นบรรทัดใหม่
- source_string จะถูกมองเป็นบรรทัดเดียว และเครื่องหมาย ^ และ $ จะหมายถึงจุดเริ่มต้นและสิ้นสุด Text ทั้งหมดใน source_string

ตัวอย่างที่ 1
คิวรีข้างล่างนี้จะแสดงชื่อและนามสกุลของพนักงาน ซึ่งเราต้องการคนที่มีชื่อประมาณว่า "สตีเฟ่น" ซึ่งภาษาอังกฤษอาจจะเขียนเป็น Steven หรือ Stephen ก็ได้(มีชื่อขึ้นต้นด้วย Ste และลงท้ายด้วย en และตรงกลางเป็น v หรือไม่ก็ ph)

SQL select first_name, last_name
from employees
where REGEXP_LIKE (first_name, '^Ste(v|ph)en$');

FIRST_NAME LAST_NAME
---------------------- ------------------------------------------
Steven King
Steven Markle
Stephen Stiles

เครื่องหมาย ^ คื่อให้เริ่มตรวจสอบ(ด้วย pattern)ตั้งแต่อักขระตัวแรกของ source_string โดยที่อักษรตัวแรกจะต้องเป็นตัว S, ตัวที่สองสามเป็น te ตัวถัดมาอาจจะเป็น v หรือ ph ก็ได้ และจบท้ายด้วย en เครื่องหมาย $ แสดงจุดสิ้นสุดของ Text ที่เราต้องการตรวจสอบ เมื่อใช้ en$ หมายถึงว่า Text ที่จะเข้าเงื่อนไขจะต้องลงท้ายด้วย en แต่ถ้าเราใส่แค่ en เฉย ๆ ก็หมาย ความว่าตัวอักษรตัวที่ห้า-หก หรือตัวที่หก-เจ็ด จะเป็น en (ขึ้นอยู่กับว่าจะเป็น Stev หรือ Steph)ส่วนหลังจากนั้นจะเป็นอะไรก็ได้

ตัวอย่างที่ 2
คิวรีข้างล่างนี้แสดงนามสกุลของพนักงาน ที่มีสระเหมือนกันติดกันสองตัว (คอลัมน์ last_name มีตัวอักษรที่เป็นสระ (a, e, i, o หรือ u) ซ้ำกันสองตัวที่อยู่ติดกันด้วย โดยไม่สนใจว่าตัวเล็ก หรือตัวใหญ่

SQL> select last_name
from employees
where REGEXP_LIKE (last_name,'([aeiou])\1','i');

LAST_NAME
-------------------------
Bloom
De Haan
Feeney
Gee
Greenberg
Greene
Khoo
Lee

ตัว Bracket "[]" ครอบตัวอักษรใด ๆ ที่เป็นไปได้ ในกรณีนี้คือตัวใด ๆ ระหว่าง a,e,i,o หรือ u ส่วนเครื่องหมาย Backslash "\" หมายถึงซ้ำตัวที่อยู่ในวงเล็บ "()" ข้างหน้า และตัว i ข้างหลังสุดคือการจับคู่ไม่สนใจว่าจะเป็นตัวใหญ่หรือเล็ก เราจะพบว่า LAST_NAME ที่ปรากฎทุก row จะมีสระซ้ำกันสองตัว

สรุปสัญญลักษณ์ต่าง ๆ ที่ใช้ใน Regular Expression (ส่วนที่เป็น Pattern)
\ (Backslash) เป็นเครื่องหมาย Backslash ตรงตัวหรือทำให้ตัวอักษรซึ่งเป็น Operator (เช่นเครื่องหมายคูณ, หาร, บวก และลบ) กลายเป็นตัวอักษรธรรมดาตัวหนึ่ง เช่น \* จะหมายถึง ตัวดอกจันหนึ่งตัว ไม่ใช่เครื่องหมายคูณเป็นต้น

* แทนการเกิดขึ้น 0 ครั้งขึ้นไป

+ แทนการเกิดขึ้น1 ครั้งเป็นต้นไป

? แทนการเกิดขึ้น 0 หรือ 1 ครั้ง

| ใช้เป็นตัวคั่นเพื่อแสดงตัวเลือก

^ แสดงว่าเป็นจุดเริ่มของข้อความ ถ้าใช้ร่วมกับ match_parameter 'm' จะใช้เป็นจุดเริ่มต้นของบรรทัดใด ๆ ใน source_string

$ แสดงว่าเป็นจุดสิ้นสุดของข้อความ ถ้าใช้ร่วมกับ match_parameter 'm' จะใช้เป็นจุดสิ้นสุดของบรรทัดใด ๆ ใน source_string

. แทนตัวอักษรใด ๆ หนึ่งตัวยกเว้น NULL และแทนตัวขึ้นบรรทัดใหม่ (New Line) ถ้า match_parameter เป็น 'n'

[ ] ใน Bracket นี้จะเป็นลิสต์ของตัวอักษร ซึ่งตัวใดตัวหนึ่งในลิสต์สามารถจับคู่ได้กับใน source_string ถ้าต้องการให้เป็นตรงกันข้ามคือต้องการ source_string ที่ไม่มีในลิสต์จะต้องใส่ ^ เข้าไปเช่นจากคิวรีข้างบนถ้าเปลี่ยน [aeiou] เป็น [^aeiou] จะได้ข้อมูล last_name ที่เป็นตรงกันข้ามคือแสดงทุกเรคคอร์ดใน employees ยกเว้น 8 เรคคอร์ดนี้

( ) เครื่องหมายกลุ่ม, สัญญลักษณ์ทั้งหลายในนี้จะถือเป็นหนึ่งตัว

{m} แทนการเกิดขึ้น m ครั้งเท่านั้น

{m,} แทนการเกิดขึ้น m ครั้งขึ้นไป

{m,n} แทนการเกิดขึ้น m ถึง n ครั้ง

\n แทนการเกิดขึ้นซ้ำครั้งที่ n ของตัว ( ) ที่อยู่ข้างหน้าตัวมัน เช่น ([aeiou])\1 หมายถึงการเกิดขึ้นซ้ำของตัว a, e,
i, o หรือ u ครั้งที่หนึ่ง เช่น aa, ee, ii, oo หรือ uu ตัว n สามารถเป็นได้จาก 1 ถึง 9

[. .] แสดงตัวอักษรหนึ่งตัว ซึ่งอาจจะประกอบด้วยอักขระมากกว่าหนึ่งตัวก็ได้ (เช่น [.ch.] ในภาษาสเปน) ใช้ในกรณีที่เราต้องการแทนตัวอักษรตัวใดๆ ในลำดับ เช่นในภาษาอังกฤษ จาก a ไปจนถึง c เราจะใช้ [a-c] แต่ในบางภาษาตัวอักษรแต่ละตัวอาจจะไม่ได้ประกอบด้วยอักขระเพียงตัวเดียวดังเช่น ch ในภาษาสเปน เราก็เลยต้องใช้ [..] ช่วยเช่นจาก a ถึง ch เราก็สามารถใช้ [a-[.ch.]] แทน

[: :] แสดงคลาสของตัวอักษร (เช่น [:alpha:]) โดยจับคู่กับตัวอักษรใด ๆ ในคลาสของตัวอักษรที่ระบุ
[:alnum:] ตัวอักษรและตัวเลขทุกตัว (alphanumeric)
[:alpha:] ตัวอักษรทุกตัว
[:digit:] ตัวเลขทุกตัว
[:blank:] ตัวช่องว่าง (Space) ทุกตัว
[:space:] ตัวช่องว่างที่ไม่สามารถพิมพ์ได้ (Nonprinting)
[:cntrl:] แสดงตัวอักษร Control ทุกตัว (ตัวอักษรที่พิมพ์ไม่ได้ - Nonprinting)
[:punct:] ตัวอักขระพิเศษทุกตัว
[:lower:] ตัวอักษรที่เป็นตัวเล็กทุกตัว
[:upper:] ตัวอักษรที่เป็นตัวใหญ่ทุกตัว
[:graph:] ตัวอักษรที่อยู่ในคลาส [:punct:], [:upper:], [:lower:] และ [:digit:] ทุกตัว
[:print:] ตัวอักษรทุกตัวที่พิมพ์ได้
[:xdigit:] ตัวอักษรที่เป็นเลขฐานสิบหกทุกตัว
คลาสเหล่านี้จะต้องใช้ภายใน Bracket เท่านั้นเช่น [[:alnum:]] หรือ [[:alpha:][:digit:]] เป็นต้น

[==] จับคู่กับตัวอักษรที่มีตัวอักษรฐาน (Base Character) ตรงกัน เช่น [=a=] จะจับคู่กับตัวอักษรที่มีตัวอักษรฐานเป็น a (เช่น Á และ Ä เป็นต้น) จะต้องใช้ภายใน Bracket เท่านั้น

ลองเล่นดูนะครับ เล่นไป ๆ จะพบว่ามันไม่ยากอย่างที่คิดเลย ขอให้สนุกกับ Regular Expression นะครับ!

บทความที่เกี่ยวเนื่องกัน
Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 1)

Sunday, November 15, 2009

Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 1)

สวัสดีครับ สำหรับตอนนี้ผมตั้งชื่อไว้เสียโก้หรู ไม่ใช่เพราะจะเลียนแบบโฆษณาหรอกครับ แต่ด้วยรู้สึกว่า Regular Expression เป็นเครื่องมือสำหรับนักฐานข้อมูลที่มหัศจรรย์จริง ๆ และควรจะมีมาตั้งนานแล้ว (Regular Expression เดิมมีอยู่บน Unix,Oracle เพิ่งเริ่มจะมีเมื่อ Version 10g นี้เอง) เราเริ่มกันเลยนะครับ

Regular Expression เป็นเรื่องเกี่ยวกับรูปแบบการเรียงตัวของตัวอักษร (Pattern) ซึ่ง Regular Expression ทำให้เราสามารถจับ Pattern ในข้อมูลที่เป็น Text ยาว ๆ ได้ ซึ่งการที่เราสามารถจับ Pattern ใน Text ได้นี้ทำให้เราสามารถค้นหาโดยใช้เงื่อนไข รวมทั้งสามารถจัดการกับข้อมูลได้อย่างมีประสิทธิภาพ

ตัวอย่างการหารูปแบบการเรียงตัวของตัวอักษรที่พบบ่อยคือข้อมูล "ที่อยู่" ที่เป็นข้อความติดต่อกัน เช่น
"11/999 ม.4 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160"

ถ้าข้อมูลในตารางที่อยู่มีข้อมูลที่มีลักษณะเหมือน ๆ กันกับข้อมูลข้างบนนี้ เราอาจจะบอกถึงรูปแบบคร่าว ๆ ได้ว่า
1. เลขที่บ้านจะอยู่ข้างหน้าเสมอ และจะต้องมีตัวเลขอย่างน้อยหนึ่งตัว อาจจะมีเครื่องหมาย "/" ด้วยก็ได้
2. "ม." หมายถึง หมู่ที่
3. "ถ." หมายถึง ถนน
4. "แขวง", "เขต" ความหมายตรงตัว
5. รหัสไปรษณีย์ เป็นเลขห้าหลักอยู่หลังสุด

สมมติว่าเดิมข้อมูลในตารางของเรา (cust_addr) ไม่ได้ตัดแบ่งที่อยู่ออกเป็นคอลัมน์ และเราต้องการตัดข้อมูลที่อยู่ข้างบนนี้ออกเป็นคอลัมน์ ๆ ดังข้างล่างนี้
1. เลขที่ = '11/999'
2. หมู่ = '4'
3. ถนน = 'พุทธมณฑลสาย 3'
4. แขวง/ตำบล = 'บางไผ่'
5. เขต/อำเภอ = 'บางแค'
6. จังหวัด = 'กรุงเทพฯ'
7. รหัสไปรษณีย์ = '10160'

สมมติอีกว่าเรามีข้อมูลในตาราง cust_addr ดังข้างล่างนี้

SQL> select * from cust_addr;

CUST_ID CUST_ADDR
------- ------------------------------------------------------------------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

คิวรีข้างล่างเป็นการค้นหาเรคคอร์ดที่มีหมายเลขโทรศัพท์อยู่ด้วย โดยใช้ REGEXP_LIKE ยังไม่ต้องกังวลเรื่องตัวอักษรแปลก ๆ นะครับ เราจะมาดูรายละเอียดกันทีหลัง

SQL> select cust_addr from cust_addr where regexp_like (cust_addr,'\(?[0-9]{2,3}\)?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}');

CUST_ADDR
-----------------------------------------------------------------
501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834
12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235

เราจะได้แค่สองเรคคอร์ดเนื่องจากเรคคอร์ดที่ cust_id = 1 ไม่มีหมายเลขโทรศัพท์

เราสามารถใช้ Regular Expression ในการตัดเอาเฉพาะส่วนที่ต้องการจากข้อความยาว ๆ มาก็ได้ โดยใช้ REGEXP_SUBSTR เช่น

SQL> select cust_addr,regexp_substr(cust_addr,'\(?[0-9]{2,3}\)?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') as tel_no from cust_addr

CUST_ADDR TEL_NO
------------------------------------------------------------------ ------------
501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834 (02)285-2834
12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235 24311235
15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เป้าหมายของเราคือแยกข้อมูลที่อยู่ออกเป็นคอลัมน์ ๆ ในขั้นแรกเราจะสร้างตารางที่จะใช้เก็บที่อยู่ขึ้นมาอีกตาราง โดยแยกที่อยู่ออกเป็นคอลัมน์ ๆ ดังตาราง cust_addr2 ข้างล่าง

SQL> desc cust_addr2;

Name Type Nullable Default Comments
-------- ------------ -------- ------- --------
CUST_ID NUMBER Y
ADDR_NO VARCHAR2(20) Y
MOO VARCHAR2(5) Y
ROAD VARCHAR2(50) Y
TAMBON VARCHAR2(50) Y
AMPHUR VARCHAR2(50) Y
PROVINCE VARCHAR2(50) Y
POSTCODE VARCHAR2(5) Y

เราจะใช้ REGEXP_SUBSTR ในการตัดข้อความที่อยู่ออกเป็นส่วนๆ เริ่มกันที่บ้านเลขที่่ก่อนนะครับ อย่าเพิ่งกังวลกับตัวอักษรแปลก ๆ นะครับ

SQL> select regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no from cust_addr;

ADDR_NO
-----------------
501/121
12/45
15/120

คราวนี้เราจะตัดเอาถนน (ROAD) ออกมาอีกคอลัมน์นะครับ

SQL> select regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
, regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
from cust_addr;

ADDR_NO ROAD
---------- ----------------
501/121 ถนนจันทร์
12/45 ถนนเพชรเกษม
15/120 ถ.พุทธมณฑลสาย 3

ถ้าต้องการตัดทุกคอลัมน์จะได้ดังนี้ครับ

SQL> select
2 cust_id
3 , regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
4 , regexp_substr(cust_addr,'(ม\.|หมู่)[0-9]{1,2}') as moo
5 , regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
6 , regexp_substr(cust_addr,'(แขวง|ตำบล)[[:alpha:]]+') as tambon
7 , regexp_substr(cust_addr,'(เขต|อำเภอ)[[:alpha:]]+') as amphur
8 , regexp_substr(cust_addr,'(จ\.|จังหวัด)[[:alpha:]]+|กรุงเทพฯ') as province
9 , regexp_substr(cust_addr,'[[:digit:]]{5}$') as postcode
10 , regexp_substr(cust_addr,'\(?[0-9]{1,3}\)?[[:blank:]]?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') tel_no
11 from cust_addr;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม 11235 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เราใช้คำสั่ง insert into ... select เพื่อ Insert ข้อมูลจากตาราง cust_addr ที่แปลงแล้วเข้าไปในตาราง cust_addr2

SQL> insert into cust_addr2
select
cust_id
, regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
, regexp_substr(cust_addr,'(ม\.|หมู่)[0-9]{1,2}') as moo
, regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
, regexp_substr(cust_addr,'(แขวง|ตำบล)[[:alpha:]]+') as tambon
, regexp_substr(cust_addr,'(เขต|อำเภอ)[[:alpha:]]+') as amphur
, regexp_substr(cust_addr,'(จ\.|จังหวัด)[[:alpha:]]+|กรุงเทพฯ') as province
, regexp_substr(cust_addr,'[[:digit:]]{5}$') as postcode
, regexp_substr(cust_addr,'\(?[0-9]{1,3}\)?[[:blank:]]?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') tel_no
from cust_addr;

3 rows inserted

SQL> select * from cust_addr2;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม 11235 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เราจะเห็นว่ายังมีคำที่ไม่ต้องการในแต่ละคอลัมน์อีกเช่น "แขวง" หรือ "ถนน" ฯลฯ เราจะใช้ REGEXP_REPLACE ในการตัดเอาตัวอักษรที่ไม่ต้องการนี้ออก

SQL> update cust_addr2
set moo = regexp_replace(moo,'ม\.|หมู')
, road = regexp_replace(road,'ถนน|ถ\.')
, tambon = regexp_replace(tambon,'ตำบล|แขวง')
, amphur = regexp_replace(amphur,'อำเภอ|เขต')
, province = regexp_replace(province,'จังหวัด');

3 rows updated

SQL> select * from cust_addr2;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 จันทร์ ทุ่งวัดดอน ยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 เพชรเกษม นครปฐม 11235 0 24311235
1 15/120 3 พุทธมณฑลสาย 3 บางไผ่ บางแค กรุงเทพฯ 10160

จะเห็นว่า Regular Expression ช่วยเราได้มากในการจัดการกับข้อความที่มีรูปแบบหรือ "Pattern" บางอย่างในเนื้อข้อมูล อย่างเช่นข้อมูล "ที่อยู่"ได้ บทความตอนต่อไปเราจะนำเอาไวยกรณ์ ของ regular expression มาคุยกันต่อ ขอขอบคุณที่ติดตามแล้วพบกันครั้งหน้านะครับ

บทความที่เกี่ยวเนื่องกัน
Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 2 จบ)

Saturday, November 7, 2009

การใช้ Parallel Hint (ตอนที่ 2 จบ)

ข้อเขียนวันนี้ต่อเนื่องและเป็นตอนจบจากตอนที่แล้วคือ การใช้ Parallel Hint (ตอนที่ 1) โดยในตอนนี้จะเป็นวิธีการตรวจสอบการทำงานของ Parallel Query ที่เรากำลังรันอยู่ โดยแปลจากบทความของ gpike เช่นกัน ขอให้สนุกกับบทความและหวังว่าจะได้ประโยชน์กันนะครับ

การตรวจสอบ Parallel Process
ในขณะที่การใช้ Parallel Process เป็นสิ่งที่มีประโยชน์เมื่อต้องจัดการกับข้อมูลขนาดใหญ่ มันก็อาจจะทำให้คุณงงงวยเมื่อไม่รู้ว่ากำลังเกิดอะไรขึ้นภายในระบบฐานข้อมูล ระหว่างการทำ Parallel Query ซึ่ง Oracle ได้เตรียม V$ Views ไว้หลายตัวที่จะช่วยให้เราสามารถตรวจสอบกระบวนการของ Parallel Query ได้ ซึ่งจะช่วยให้เราสามารถเขียนคำสั่งได้ดีขึ้น และสามารถที่จะ Kill Process ที่กินทรัพยากรของระบบจำนวนมากเกินไป

อะไรคือ V$ Views
คิวรีในตัวอย่างต่อจากนี้ไป จะต้องใช้ V_$ View ซึ่งผู้ที่เป็นเจ้าของคือ SYS (คืออยู่ใน Schema "SYS") ปกติแล้ว V_$ View เราจะมองเห็นเป็น (โดยผ่าน Public Synonym) ซึ่งได้เอา Underscore ออก ตัวอย่างเช่นวิว SYS.V_$PX_SESSION เวลาเราใช้วิวตัวนี้เราจะใช้ผ่าน Synonym ชื่อ V$PX_SESSION วิธีการที่ง่ายที่สุด (แต่ไม่ปลอดภัย) คือการ GRANT SELECT ANY TABLE ให้กับ User ทุกคน

การดู Parallel Query Process เบื้องต้น
โดยปกติเมื่อมีการทำ Parallel Query จะเกิด Session ที่คอยประสานการทำงานของ Parallel Servers โดยในขณะทำคิวรี จะมี Parallel Servers 2 ชุดเกิดขึ้นได้แก่ 1) Producers ซึ่งคอยจัดการเรื่อง Full Table Scan และ 2) Consumers ซึ่งเอาข้อมูลทีได้จาก Producers มาทำ Operation ต่างเช่น Hash, Join เป็นต้น Degree ของการทำ Parallel ที่กำหนดในคำสั่ง Hint เป็นตัวกำหนดว่าจะมี Parallel Server Process มากน้อยแค่ไหนในแต่ละ (Parallel) Server Set ไม่ใช่จำนวนทั้งหมดในแต่ละคิวรี ดังตัวอย่างคิวรีข้างล่างนี้

select /*+ PARALLEL(s 2) */ distinct time_of_day from sales s;

ในขณะที่ทำการคิวรี เราจะรันคำสั่งเพื่อดูการทำงานภายในของ Parallel Query ข้างบน

select qcsid,
sid,
nvl(server_group,0) server_group,
server_set,
degree,
req_degree
from SYS.V_$PX_SESSION
order by qcsid,
nvl(server_group,0),
server_set;
คิวรีข้างบนจะให้ผลดังต่อไปนี้ (ผลที่ได้จะปรากฎ ณ ขณะที่ Parallel คิวรีตัวบนยังคงทำงานอยู่)

QCSID SID SERVER_GROUP SERVER_SET DEGREE REQ_DEGREE
------ ------- ---------- ---------- ---------- --------
390 390 0
390 368 1 1 2 2
390 353 1 1 2 2
390 332 1 2 2 2
390 330 1 2 2 2

ผลที่ได้แสดงให้เห็นว่าจากการทำ Parallel Query โดยมี Degree =2 ทำให้เกิด Process ขึ้นมา 5 Process แถวบนสุดเป็นตัวประสานงานของ Process อื่น ๆ เรียกว่า Query Coordinator (QC) แถว 2 และ 3 แสดง เซ็ทของ Parallel Process (server_set = 1) ซึ่งมีหน้าที่ทำการ Full Table Scan ตาราง SALES ในขณะที่แถวที่ 4 และ 5 (server_set = 2) จะแสดงการทำงานของ Parallel Process ซึ่งทำงานส่วนที่เหลือ (Distinct) ของข้อมูลบนตาราง SALES

สมมติว่าเราเพิ่ม Degree เป็น 4 แต่ในระบบฐานข้อมูลอนุญาตให้ใช้แค่ Degree = 3 เราจะได้ผลจากการคิวรี V_$PX_SESSION ดังนี้
QCSID SID SERVER_GROUP SERVER_SET DEGREE REQ_DEGREE
------ ------- ---------- ---------- ---------- --------
390 390 0
390 351 1 1 3 4
390 342 1 1 3 4
390 330 1 1 3 4
390 332 1 2 3 4
390 353 1 2 3 4
390 339 1 2 3 4

แม้ว่าคำสั่งจะขอ Degree = 4 (REQ_DEGREE) จะเห็นว่ามีแค่ 3 (DEGREE) ซึ่งอธิบายว่าทำไมเราจึงมีแค่ 3 Process ในแต่ละ Server Set

ตรวจสอบ Process โดยดูที่การอ่านของฮาร์ดดิสก์
การจะดู Process ของ Parallel Query ถ้าเรารู้ว่าข้อมูลอยู่บนฮาร์ดดิสก์ลูกไหน เราสามารถตรวจสอบ Parallel Queryได้จากการอ่านข้อมูลของฮาร์ดดิสก์ ซึ่งการตรวจสอบในลักษณะนี้ทำให้เราสามารถกะเวลาที่คิวรีจะเสร็จได้ นอกจากนี้การอ่านข้อมูลของฮาร์ดดิสก์จะเป็นตัวบอกด้วยหากผลจากคิวรีที่ได้ไม่ใช่สิ่งที่เราต้องการ

select a.qcsid,
a.sid,
a.server_group,
a.server_set,
substr(b.name,1,20) operation,
a.value
from V$PX_SESSTAT a,
V$STATNAME b
where a.statistic# = b.statistic#
and upper(b.name) = 'PHYSICAL READS'
order by a.qcsid,
a.server_group,
a.server_set;
คิวรีข้างบนนี้จะให้ผลดังข้างล่างก็ต่อเมื่อเรากำลังรันคิวรีที่เป็น Parallel อยู่ โดยผลที่ได้นี้เกิดจากคิวรีที่มี Degree = 2 และมีการ Join แบบซับซ้อน และมีการ Sort ร่วมกับการทำ Full Table Scan

QCSID SID SERVER_GROUP SERVER_SET OPERATION VALUE
------ ------- ---------- ---------- ---------- --------
332 330 1 1 physical reads 4632
332 351 1 1 physical reads 4697
332 333 1 2 physical reads 4554
332 339 1 2 physical reads 4605
332 332 physical reads 168005

คิวรีนี้แสดงให้เห็นว่าตอนที่กำลังรันคิวรี Parallel Server Set ทั้งคู่ (Producer และ Consumer) จะทำงานโดยแตกออกเป็น 2 Process ตาม DEGREE ที่กำหนด SID 332 เป็น Session ที่ทำหน้าที่เป็นตัวเริ่มต้นและคอยประสานงานของ Session ย่อยอื่น ๆ (Query Coordination Session) ค่าของ Physical Reads (การอ่านค่าจากฮาร์ดดิสก์) จะเพิ่มขึ้นเรื่อย ๆ จนกว่า Session นี้จะ Disconnect ไป

การตรวจสอบการทำงานภายในที่ใช้เวลานาน (Long Operations) โดยใช้ V$SESSION_LONGOPS
V$SESSION_LONGOPS เป็นที่เก็บสถานะของการทำงานซึ่งจะระบบกะว่าใช้เวลาไม่ต่ำกว่า 6 วินาที ตัวอย่างของ Long Operation ได้แก่ Full Table Scan, Hash Join และ Sort Merge ที่อาจจะสร้าง Log การทำงานของตัวมันในระหว่างทำงานไว้ในตารางนี้ (และหายไปเมื่อทำงานเสร็จ) ข้อดีของตารางนี้คือมีคอลัมน์ที่แสดงเวลาที่งานจะเสร็จโดยประมาณเป็นวินาที สำหรับ Parallel Query ที่จะเก็บ Log ในตารางนี้ จะสร้างเรคคอร์ดตามจำนวนของ Parallel Server ที่กำหนดไว้

select sl.sid,
sl.serial#,
substr(sl.opname,1,30),
sl.sofar,
to_char(sl.start_time,'DD-MON-YYYY:HH24:MI:SS') start_time,
sl.elapsed_seconds elapsed,
sl.time_remaining remaining
from V$SESSION_LONGOPS sl,
V$SESSION s
where s.sid = sl.sid
and s.serial# = sl.serial#
and s.username = '&username'
order by sl.start_time desc,
sl.time_remaining asc;

ระหว่างการทำงานของ Parallel คิวรีทีใช้เวลานาน เราอาจจะได้ผลของคิวรีดังนี้

SID SERIAL# OPERATION START_TIME ELAPSED REMAINING
----- ------------- ------------------ -------------------- --------------- -------------------
332 18956 Table Scan 03-MAY-2007:14:26:11 42 18
333 18957 Table Scan 03-MAY-2007:14:26:13 40 20

โปรดสังเกตว่า V$SESSION_LONGOPS ไม่ได้เอาไว้ใช้ในการตรวจสอบ Parallel Query โดยตรง แต่สามารถใช้ได้กับการทำงานใด ๆ ที่ใช้เวลานานเช่นการทำ Full Table Scan

แปลจาก Monitoring Oracle Parallel Queries: PARALLEL Hint Part2 Published in Complex SQL, Hints, SQL, Tuning by gpike วันพฤหัสบดีที่ 3 พฤษภาคม 2007

บทความที่เกี่ยวเนื่องกัน
1. การใช้ Parallel Hint (ตอนที่ 1)

Thursday, November 5, 2009

การกำหนดให้ Developer ไม่สามารถแก้ไขโครงสร้างตาราง

ขอบคุณคุณ Somritt Thiendej สำหรับคำถาม
"โปรแกรมเมอร์ใช้ toad ต่อ oracle db ทำอย่างไรจะไม่ให้สามารถแก้ไข ฟิล เพิ่มฟิล ในตารางได้?"

ปกติ User บนฐานข้อมูลจะมีสิทธิ์สร้างตาราง และเปลี่ยนแปลงตารางที่เขาสร้างได้ ถ้าเราต้องการให้ User หรือ Developer มีสิทธิ์เฉพาะการจัดการข้อมูลในตาราง แต่ไม่มีสิทธิ์ในการแก้ไขโครงสร้าง เราจะต้องให้ User หรือ Developer ล็อกอินด้วย User อื่น แล้วกำหนดสิทธิ์เฉพาะการจัดการข้อมูลในตารางให้เขาดังตัวอย่างข้างล่างนี้ครับ

สมมติว่าตารางที่ต้องให้ Developer ใช้อยู่ใน Schema "account" สังเกตว่าถ้าเราล็อกอินด้วย User "account" เราจะสามารถสร้างตาราง,เปลี่ยนแปลงโครงสร้างตารางที่สร้าง และเปลี่ยนแปลงข้อมูลในตารางได้

===========================================================
Listing1: User "account" สามารถสร้างและจัดการโครงสร้างตารางที่ตนเองสร้างขึ้นได้เสมอ เราไม่สามารถห้ามได้

SQL> conn system
Connected.

SQL> create user account2 identified by account;

User created.

SQL> grant connect,resource to account2;

Grant succeeded.

SQL> create table table1 (id number,name varchar2(20));

Table created.

SQL> alter table table1 add address varchar2(50);

Table altered.

SQL> desc table1;
Name Null? Type
----------------------------------------- -------- ----------------------------
ID NUMBER
NAME VARCHAR2(20)
ADDRESS VARCHAR2(50)

SQL> drop table table1;

Table dropped.

SQL> create table table2(id number,name varchar2(30));

Table created.

SQL> insert into table2 values (1,'Somchai');

1 row created.

SQL> insert into table2 values (2,'Somkid');

1 row created.

SQL> commit;

Commit complete.

SQL> select * from table2;

ID NAME
---------- ------------------------------
1 Somchai
2 Somkid

ต่อมาเราสร้าง User ชื่อ accapp ที่จะให้ Developer ใช้ในการ Access ฐานข้อมูล จากนั้นเรา Grant สิทธิ์ให้ Connect ได้ และสามารถใช้ทรัพยากรของระบบได้ (เช่นสามารถสร้างตารางของตัวเองได้) Resource เป็นสิทธิ์ที่ให้พื้นที่กับ User แบบไม่จำกัด จากนั้นเราก็ ล็อกอินด้วย User ที่มีสิทธิ์ DBA หรือล็อกอินด้วย User "account" แล้ว Grant สิทธิ์ทุกอย่างบนตาราง Table2 ให้กับ accapp เมื่อเราล็อกอินเป็น accapp แล้วคิวรีดูจาก USER_TAB_PRIVS ซึ่งแสดงสิทธิ์ของ User ที่ล็อกอินอยู่ว่ามีสิทธิ์อะไรบนตารางไหนบ้าง เนื่องจากเราได้ Grant All ไว้เราจะเห็นว่า accapp ได้สิทธิ์ทุกสิทธิ์บนตาราง Table2 ของ "account" เราสามารถจะลบสิทธิ์บางตัวที่ไม่ต้องการออก โดยการใช้คำสั่ง Revoke หรือจะใช้วิธีการ Grant เฉพาะสิทธิ์ที่ต้องการให้กับ Developer ก็ได้ เช่นถ้าต้องการให้ Developer จัดการกับข้อมูลในตารางได้เท่านั้นก็ใช้คำสั่ง "grant insert,update,delete on table2 to accapp" แทนที่จะใช้คำสั่ง grant all เป็นต้น ข้อสำคัญอย่าไป grant สิทธิ์ DBA ให้กับ User ก็แล้วกันนะครับ :)

===========================================================
Listing2: User "accapp" จะสามารถจัดการตารางของ User (หรือ Schema) อื่น ได้เท่าที่ได้รับสิทธิ์

SQL> conn system
Connected.
SQL> create user accapp identified by accapp;

User created.

SQL> grant connect,resource to accapp;

Grant succeeded.

SQL> grant all on account.table2 to accapp;

Grant succeeded.

SQL> conn accapp
Connected.
SQL> column owner format a15
SQL> column table_name format a15
SQL> column privilege format a30
SQL> select owner ,table_name ,privilege from user_tab_privs;

OWNER TABLE_NAME PRIVILEGE
--------------- --------------- ------------------------------
ACCOUNT TABLE2 FLASHBACK
ACCOUNT TABLE2 DEBUG
ACCOUNT TABLE2 QUERY REWRITE
ACCOUNT TABLE2 ON COMMIT REFRESH
ACCOUNT TABLE2 REFERENCES
ACCOUNT TABLE2 UPDATE
ACCOUNT TABLE2 SELECT
ACCOUNT TABLE2 INSERT
ACCOUNT TABLE2 INDEX
ACCOUNT TABLE2 DELETE
ACCOUNT TABLE2 ALTER

11 rows selected.

SQL> select * from account.table2;

ID NAME
---------- ------------------------------
1 Somchai
2 Somkid

-- คำสั่งข้างล่างแสดงให้เห็นว่าตอนนี้ accapp สามารถเปลี่ยนโครงสร้างของ table2 ได้
SQL> alter table account.table2 add test number;

Table altered.

SQL> conn account
Connected.
SQL> revoke alter on table2 from accapp;

Revoke succeeded.

SQL> conn accapp
Connected.
--หลังจากที่เรา Revoke สิทธิ์ในการแก้ไขตารางออกไปแล้ว accapp ไม่สามารถแก้ไขตารางได้
SQL> alter table account.table2 drop column test;
alter table account.table2 drop column test
*
ERROR at line 1:
ORA-01031: insufficient privileges

SQL> alter table account.table2 add testcol varchar2(30);
alter table account.table2 add testcol varchar2(30)
*
ERROR at line 1:
ORA-01031: insufficient privileges

SQL> drop table account.table2;
drop table account.table2
*
ERROR at line 1:
ORA-01031: insufficient privileges

SQL> conn accapp
Connected.

SQL> desc account.table2;
Name Null? Type
----------------------------------------- -------- ----------------------------
ID NUMBER
NAME VARCHAR2(30)
TEST NUMBER

SQL> show user
USER is "ACCAPP"
SQL> conn system
Connected.
SQL> alter table account.table2 drop column TEST;

Table altered.

SQL> conn accapp
Connected.
SQL> select * from account.table2;

ID NAME
---------- ------------------------------
1 Somchai
2 Somkid

SQL> insert into account.table2 values (3,'Somkiat');

1 row created.

SQL> select * from account.table2 ;

ID NAME
---------- ------------------------------
1 Somchai
2 Somkid
3 Somkiat

SQL> update account.table2 set name = 'XXXX' where id = 2;

1 row updated.

SQL> commit;

Commit complete.

SQL> select * from account.table2;

ID NAME
---------- ------------------------------
1 Somchai
2 XXXX
3 Somkiat

SQL> delete account.table2 where id = 3;

1 row deleted.

SQL> commit;

Commit complete.

คุณอาจจะพบว่าการ Grant สิทธิ์ทีละตารางนี้เป็นเรื่องยากถ้ามีเป็นจำนวนมาก เราอาจจะใช้วิธีการใช้ SQL สร้างคำสั่งเป็นสคริปต์แล้วไปรันอีกทีเช่น

SQL> conn account
connected.

SQL> set heading off
SQL> select 'grant all on ' || table_name || ' to accapp;' from user_tables;