Mine

รูปภาพของฉัน
หาดใหญ่, ภาคใต้, Thailand
ชอบเอาใจ แต่ก็เอาแต่ใจ

วันศุกร์ที่ 1 ตุลาคม พ.ศ. 2553

เมื่อใช้งานจริง count มันจะยากหรือไม่น๊ะ

จากคราวที่แล้วเราก็พอทราบแล้วว่าเวลาเราจะนับนั้นใช้ Function Count และต้องใช้คู่กับการ Group By นะคะ นั่นคือ นับตามข้อมูลอะไร แต่บางทีเราก็ไม่จำเป็นต้อง Group By ก็ได้ค่ะถ้าเราไม่แสดงข้อมูลจาก Field อื่น ถ้าเช่นนั้นเรามารู้จัก Count ให้ลึกซึ้งกันดีกว่าเพื่อจะได้นำไปใช้งานได้จริง

count คือ Function สำหรับนับจำนวน record หากเรา Query ข้อมูลแล้วต้องการทราบว่า จำนวนที่เรา Query ได้มีกี่ record ก็สามารถใช้ Function นี้ได้ค่ะ แต่การใช้ count มันพิเศษกว่า นั้นค่ะ เรามาดูต่อเลยนะคะ

ควรรู้
- สามารถนับจำนวน Record ข้อมูลทั้งหมดได้
- สามารถนับจำนวน Record ข้อมูลเฉพาะที่ไม่ซ้ำกันได้
- ไม่สามารถนับค่าที่เป็น NULL ได้
- หากการ Select ข้อมูล มี filed อื่นๆ ที่ไม่ได้ใช้ count หรือ Function ประเภทเดียวกับ count ให้นำมาทำ Group By เสมอ

รูปแบบการใช้คำสั่ง
แบบที่ 1
count([ชื่อ filed]) ใช้เมื่อ ต้องการนับจำนวน record โดยที่จะต้องมีข้อมูลของ Filed ที่ใช้นับ (หากเป็น NULL จะไม่สนใจ)
แบบที่ 2
count(distinct [ชื่อ Field]) ใช้เมื่อ ต้องการนับจำนวน record โดยที่ Filed ที่ใช้นับจะต้องไม่ซ้ำกัน (โดยที่ซ้ำกันนับแต่ 1)

แนะนำตารางที่ใช้อ้างถึง
t_patient คือ ตารางเก็บข้อมูลทั่วไปของผู้ป่วยที่เคยมารับบริการ 1 คนจะมี 1 record 
t_visit คือ ตารางเก็บข้อมูลรับบริการของผู้ป่วยที่มารับบริการแต่ละครั้ง 1 ครั้งจะมี 1 Record ผู้ป่วยสามารถรับบริการได้มากกว่า 1 ครั้ง

ตัวอย่างการใช้คำสั่ง
- นับจำนวนข้อมูล visit ทั้งหมด
SELECT count(t_visit.t_visit_id) FROM t_visit WHERE t_visit.f_visit_status_id <> '4';

- นับจำนวน visit ของแต่ละวัน
SELECT substring(t_visit.visit_financial_discharge_time,1,10) as dateserv ,count(t_visit.t_visit_id) as count_visit
FROM t_visit
WHERE t_visit.f_visit_status_id <> '4'
and substring(t_visit.visit_financial_discharge_time,1,10) Between '2553-04-01' And '2553-04-30'
GROUP BY dateserv;

- นับจำนวนผู้ป่วยมารับบริการในเดือน ว่ามีกี่คน (ใน 1 เดือนอาจจะรับบริการมากกว่า 1 ครั้ง ดังนั้น Record ที่ได้จากการ Select อาจจะมากกว่าจำนวนคน)
SELECT count(distinct t_visit.t_patient_id) --ใช้ distinct เพื่อนับเป็นรายบุคคล  
FROM
t_visit
WHERE t_visit.f_visit_status_id <> '4'
and substring(t_visit.visit_financial_discharge_time,1,10) Between '2553-04-01' And '2553-04-30';

- นับจำนวนคน จำนวนครั้ง ของการมารับบริการใน Query เดียวกัน
SELECT count(distinct t_visit.t_patient_id) as count_pat --ใช้ distinct เพื่อนับเป็นรายบุคคล  
,
count(t_visit.t_visit_id) as count_visit --นับครั้งของการรับบริการ 
FROM
t_visit
WHERE t_visit.f_visit_status_id <> '4'
and substring(t_visit.visit_financial_discharge_time,1,10) Between '2553-04-01' And '2553-04-30';

มาถึงจุดเปลี่ยน ... สิ่งที่กล่าวมาข้างต้นเรื่องรูปแบบการใช้งาน นั่นคือ "ทฤษฎี" แต่เวลา "ปฏิบัติ" มันย่อมซับซ้อนกว่าเป็นธรรมดาค่ะ เรามาดูตัวอย่างที่ซับซ้อนขึ้นอีกนิดนะคะ
SELECT
count(case when t_patient.f_sex_id = '1'
    then t_patient.t_patient_id
    else NULL
 end) as man --นับเฉพาะเพศชายเท่านั้น
,count(case when t_patient.f_sex_id = '2'
    then t_patient.t_patient_id
    else NULL
 end) as women --นับเฉพาะเพศหญิงเท่านั้น
FROM t_patient;

จากตัวอย่างข้างต้นจะเห็นว่าเราไม่สามารถนับได้เลยนะคะ เราต้องมีเงื่อนไขก่อนที่จะนับซึ่งตรงนี้เราทราบคุณสมบัติของการ Count ว่าจะไม่นับค่า NULL ดังนั้น เราก็ทำให้เกิดค่า NULL ใน Record ที่เราไม่ต้องการนับโดยการขอความช่วยเหลือจาก CASE WHEN เพื่อให้สามารถนับข้อมูลได้ตรงตามจริง

งั้นมาทำความรู้จักกับ CASE WHEN คร่าวๆกันก่อนะคะ สำหรับคำสั่ง CASE WHEN จะช่วยให้เราสร้างเงื่อนไขกับข้อมูลได้ และได้มากกว่า 1 เงื่อนไข ค่ะ
รูปแบบ
CASE WHEN [ผลลัพธ์ของเงื่อนไข จริง หรือ เท็จ
         THEN [ให้แสดงหรือทำอะไรถ้า when เป็นจริง]
         WHEN [ผลลัพธ์ของเงื่อนไข จริง หรือ เท็จ]
         THEN [ให้แสดงหรือทำอะไรถ้า when เป็นจริง]
         --when คู่กับ then เสมอ แต่จะมีกี่เงื่อนไขก็ได้ มี when then กี่ตู่ก็ได้

         ELSE [ให้แสดงหรือทำอะไรถ้า when ทั้งหมดเป็นเท็จ] --จะมีหรือไม่มี else ก็ได้
END --จบ case ด้วย end เสมอ

การทำงาน
จะเริ่มจาก WHEN จะตรวจสอบเงื่อนไขเพื่อบอกว่าเราต้องการข้อมูลอะไร ซึ่งหาก WHEN ได้ข้อสรุปจากเงื่อนไขว่าเป็นจริง ก็จะทำงานตามที่ THEN สั่งการแล้วถ้าเป็น เท็จ ก็จะทำให้ ELSE แทน

งั้นเรามาทำความเข้าใจกับคำสั่งข้างต้นกันนะคะที่เอา CASE WHEN เข้ามาเป็นเงื่อนไขการนับ
โจทย์ : ต้องการนับจำนวนผู้ป่วยเพศชาย และจำนวนผู้ป่วยเพศหญิง แยกกัน 2 Field
จากโจทย์ เราก็ต้องกลับไปดูที่ข้อมูลของเราก่อนนะคะว่าจัดเก็บข้อมูลอย่างไร
t_patient คือ ตารางเก็บข้อมูลทั่วไปของผู้ป่วยที่เคยมารับบริการ 1 คนจะมี 1 record 
t_patient.f_sex_id คือ Field ที่เก็บข้อมูลรหัสเพศ โดยที่ 1=ชาย, 2=หญิง

จากข้อมูลที่เรามีลองทำทีละขั้นตอนให้เห็นที่มาที่ไปก่อนจะ Count กันนะคะ
ขั้นตอนที่ 1 : เราลอง Query ข้อมูลจากตารางนี้ดูก่อนนะคะ
คำสั่ง
SELECT t_patient.f_sex_id
FROM t_patient;
ผลลัพธ์ที่ได้
f_sex_id
1
1
2
1
2

ขั้นตอนที่ 2 : เราลองนำ CASE WHEN เข้ามาเป็นเงื่อนไขเพื่อแยกข้อมูลของเราค่ะ
คำสั่ง

SELECT
t_patient.f_sex_id
,case when t_patient.f_sex_id = '1'
    then t_patient.t_patient_id
    else NULL
 end as man --เพศชายเท่านั้น
,case when t_patient.f_sex_id = '2'
    then t_patient.t_patient_id
    else NULL
 end as women --เพศหญิงเท่านั้น
FROM t_patient;
ผลลัพธ์ที่ได้
f_sex_idmanwomen
12063096106908 NULL
12063096106910NULL
2NULL2063096104000
12063096108000NULL
2NULL2063096106911

ขั้นตอนที่ 3
: จากข้อมูลเราจะเห็นว่าจะมีค่า NULL เกิดขึ้นในกรณีที่เงื่อนไขเป็นเท็จ งั้นเราลองนับจำนวนกันดูนะคะว่าจะได้ตามที่เราต้องการหรือไม่ ชาย = 3 คน และ หญิง = 2 คน (เราต้องเอา Field
t_patient.f_sex_id ออกนะคะ เพราะไม่เช่นนั้นเราจะต้องนำมาทำ Group By ซึ่ง Output ไม่ตรงตามที่เราต้องการ เพราะตอนนี้เราต้องการให้แยก Field กันน่ะ)
คำสั่ง

SELECT
count(case when t_patient.f_sex_id = '1'
    then t_patient.t_patient_id
    else NULL
 end) as man --นับเฉพาะเพศชายเท่านั้น
,count(case when t_patient.f_sex_id = '2'
    then t_patient.t_patient_id
    else NULL
 end) as women --นับเฉพาะเพศหญิงเท่านั้น
FROM t_patient;
ผลลัพธ์ที่ได้
manwomen
32

ในทางกลับกันบางทีมันก็ง่ายกว่าที่เราทำอยู่หากโจทย์ไม่ได้ Fixed ว่าต้องการข้อมูลแบบไหนแต่ต้องการทราบว่าจำนวนเท่าไหร่ ซึ่งบางข้อมูลเราอาจจะไม่จำเป็นต้องใช้ CASE WHEN เขา้ามาช่วยเลย เพราะในข้อมูลของมันเองสามารถทำ GROUP BY และได้ผลลัพธ์เช่นเดียวกันได้ ดูตัวอย่างนับตามเพศนะคะเราก็ใช้รหัสเพศมาเป็นตัวแปรของการนับเลยค่ะ (นับตามรหัสเพศ)
คำสั่ง
SELECT
t_patient.f_sex_id
,count(t_patient.t_patient_id) as count_by_gender
FROM t_patient
GROUB BY t_patient.f_sex_id;
ผลลัพธ์ที่ได้
f_sex_idcount_by_gender
13
22

คำถามต่อไป แล้วทำไมเราต้อง COUNT(distinct ...) ด้วยล่ะ ?

อันนี้ก็เนื่องจากเราไม่รู้ว่าข้อมูลที่ JOIN มาเนี๊ยะ มีข้อมูลอะไรบ้างยังไงล่ะ หากเอา visit ไป join กับ ICD10 หรือ Order ก็อาจจะพบ record มากกว่า 1 เป็นแน่แต่โจทย์กลับบอกว่าให้นับจำนวนคน...เมื่อ Record เบิ้ล ก็จะทำให้การนับมีความผิดพลาดได้ค่ะ

งั้นเราลองมาทำโจทย์นี้ดูค่ะ แล้วจะรู้ว่าเราจำเป็นต้องใช้ Count เมื่อเรานับ "คน" "ครั้ง" ก็ด้วย
โจทย์ : ต้องการทราบจำนวนคน ครั้ง ของผู้ที่มารัับบริการ ในแต่ละเดือนของปีงบประมาณ 2553

ขั้นตอนที่ 1
: ออกแบบผลลัพธ์ที่โจทย์ต้องการ ได้ดังนี้ 
month_visitcount_patcount_visit
ตุลาคม 255220004500
พฤศจิกายน 255223405600
.........
.........
กันยายน 255322004999

ขั้นตอนที่ 2
: ตรวจสอบที่มาของข้อมูลที่จะสามารทำให้เกิดผลลัพธ์ได้ ก็ต้องดูโทย์ว่าต้องการอะไรและจาก Output เราออกแบบไว้อย่างไร ก็คงไม่พ้นตาราง t_visit แหละค่ะ เป็นตารางข้อมูลรับบริการน่ะ จะมีทั้งวันที่รับบริการ และ รหัสผู้ป่วย(t_visit.t_patient_id)ที่นับ "คน" ได้ และ รหัสการเข้ารับบริการ(t_visit.t_visit_id)ที่นับ "ครั้ง" ได้

ขั้นตอนที่ 3 : ลอง Query ข้อมูลรับบริการของปังบประมาณ 2553 ดูนะคะ
คำสั่ง
SELECT
t_visit.visit_financial_discharge_time --วันที่จำหน่ายทางการเงิน
,t_visit.t_patient_id --รหัสผู้ป่วย
,t_visit.t_visit_id --รหัสการเข้ารับบริการ
FROM t_visitWHERE t_visit.f_visit_status_id <> '4' --ข้อมูลการรับบริการไม่ถูกยกเลิก
And substring(t_visit.visit_financial_discharge_time,1,10) Between '2552-10-01' And '2553-09-30';
ผลลัพธ์ที่ได้
visit_financial_discharge_timet_patient_idt_visit_id
2553-01-19,16:49:3820600018418762555003061312
2553-01-20,17:38:0020600018418762552391103429
2553-01-22,10:30:5320600056216212554092156596
2553-01-30,13:40:5520600116668782554250372159
2553-01-30,15:30:5920600380426392553850962391

ขั้นตอนที่ 4 : จากผลลัพธ์ของขั้นตอนที่ 3 นั้นเห็นว่าข้อมูลของรหัสผู้ป่วย(t_patient_id) เป็นข้อมูลที่ซ้ำกันได้ในเดือนเดียวกัน เนื่องจาก 1 คนสามารถเข้ารับบริการได้มากกว่า 1 ครั้งนั้นเองและจากความรู้ของการ Count หากเรานับเลยจะทำให้จำนวน "คน" และ "ครั้ง" เพราะต้างก็มีข้อมูล ดังนั้น เราใช้โอกาสนี้นับแบบ Distinct คือ นับค่าที่ซ้ำกันแค่ 1 เท่านั้น และอีกส่วนหนึ่งที่เราจะสนใจ คือวันที่ค่ะ จากโจทย์เราต้องการนับเป็นเดือน จากข้อมูลของวันที่นั้นเราก็ต้องใช้วิชากันหน่อยค่ะ
คำสั่ง
SELECT
(case when substring(t_visit.visit_financial_discharge_time,6,2) = '10' --ตัดวันที่ตั้งแต่ตำแหน่งที่ 6 มา 2 ตัวจะได้เลขเดือน 2 หลัก และเทียบว่าเป็น 10 หรือไม่
        then 'ตุลาคม'  --หาก when เป็นจริง คือ เดือน 10 จะให้แสดงเป็นข้อความว่า "ตุลาคม"
        when substring(t_visit.visit_financial_discharge_time,6,2) = '11'
        then 'พฤศจิกายน'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '12'
        then 'ธันวาคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '01'
        then 'มกราคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '02'
        then 'กุมภาพันธ์'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '03'
        then 'มีนาคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '04'
        then 'เมษายน'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '05'
        then 'พฤษภาคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '06'
        then 'มิถุนายน'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '07'
        then 'กรกฎาคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '08'
        then 'สิงหาคม'
        when substring(t_visit.visit_financial_discharge_time,6,2) = '09'
        then 'กันยายน'
 end) || ' ' || substring(t_visit.visit_financial_discharge_time,1,4) as month_visit --เมื่อได้เดือนที่เป็นข้อความแล้วให้นำมาต่อกับ ช่องว่าง และต่อด้วยเลขปีซึ่งเราตัดวันที่ในตำแหน่งที่ 1 มา 4 ตัวจะได้เลขปี พ.ศ.
,count(distinct t_visit.t_patient_id) as count_pat --นับคน
,count(t_visit.t_visit_id) as count_visit --นับครั้ง
FROM t_visitWHERE t_visit.f_visit_status_id <> '4' --ข้อมูลการรับบริการไม่ถูกยกเลิก
And substring(t_visit.visit_financial_discharge_time,1,10) Between '2552-10-01' And '2553-09-30'
GROUP BY month_visit
,substring(t_visit.visit_financial_discharge_time,1,7) --หากทำ Order By ก็ต้องนำมา Group By ด้วยนะคะ ถึงแม้จะไม่ได้แสดงก็ตาม
ORDER BY substring(t_visit.visit_financial_discharge_time,1,7); --ใช้วิชามารนิดหน่อยเพื่อให้ได้ข้อมูลที่สวยงาม

ผลลัพธ์ที่ได้ 
(ลองนำไป Query ดูนะคะ แต่ละที่จะได้ข้อมูลไม่เท่ากันค่ะ)
month_visitcount_patcount_visit
ตุลาคม 255220004500
พฤศจิกายน 255223405600
.........
.........
กันยายน 255322004999

* เท่านี้เราก็ได้ตามโจทย์ที่เราต้องการแล้วค่ะ วันนี้พอก่อนแล้วกันค่ะเด็กไม่ยอมนอนกล่อมเด็กก่อนค่ะ "ไว้ติดตามกันตอนต่อไปนะคะ"