Mine

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

วันศุกร์ที่ 5 พฤศจิกายน พ.ศ. 2553

จำนวนผู้ป่วยเบาหวาน(DM)และความดัน(HT)ของสิทธิUC

สวัสดีค่ะ วันก่อนได้มีโอกาสเขียน SQL ตัวนึงค่ะ ก็เลยอยากเอามาฝากกันทุกคน
รายละเอียด คือ จำนวนผู้ป่วยนอกเบาหวานที่มีความดันร่วมด้วย โดยมีความดัน <= 130/80 เฉพาะสิทธิ UC เท่านั้น ในช่วงปีงบประมาณ 2553
มาตีความของรายละเอียดที่ต้องการก่อนดีกว่าค่ะ ว่าเขาต้องการอะไรกันแน่
1. ผู้ป่วยนอกเบาหวานที่มีความดันร่วมด้วย หมายถึง ผู้ที่มารับบริการที่ไม่มีการ Admit ด้วยโรคเบาหวาน และมีโรคความดัน จากการวินิจฉัยของแพทย์ และผู้ให้รหัส ซึ่งเราสามารถตรวจสอบจากการให้รหัส ICD-10 ได้ นั่นคือ ผู้รับบริการจะได้รับ ICD-10 ในช่วง E10-E14 (เบาหวาน) และ I10-I15 (ความดัน)
2. ความดัน <= 130/80 หมายถึง ผู้ที่มารับบริการได้วัดความดัน และความดันช่วงบนมีค่าน้อยกว่าหรือเท่ากับ 130 และ ความดันช่วงล่างต้องมีค่าน้องกว่าหรือเท่ากับ 80
3. สิทธิ UC หมายถึง ผู้มารับบริการที่ใช้สิทธิของหลักประกันสุขภาพ โดยสามารถตรวจสอบได้จาก maininscl = "WEL", "UCS", ""
4. ปีงบประมาณ 2553 หมายถึง ผู้มารับบริการในช่วงวันที่ 01 ตุลาคม 2552 ถึง 30 กันยายน 2553
สรุปตารางที่เราต้องใช้ มีดังนี้
- t_visit บอกถึงข้อมูลรับบริการในแต่ละครั้ง
- t_diag_icd10 บอกถึง รหัสที่ได้รับวินิจฉัยในการรับบริการแต่ละครั้ง
- t_visit_vital_sign บอกถึงค่าความดันที่วัดในการรับบริการ
- t_visit_payment บอกถึงสิทธิที่ผู้รับบริการใช้ในการมารับบริการแต่ละครั้ง
- b_contract_plans บอกถึงสิทธิของโรงพยาบาลว่ามีสิทธิอะไรบ้าง และเชื่อมโยงไปยังสิทธิของหลักประกันสุขภาพในสิทธิอะไร
- r_rp1853_instype บอกถึงสิทธิของหลักประกันสุขภาพ
งั้นเรามาเริ่มเขียน Query กันทีละขั้นเลยนะคะ เพื่อให้ได้ผลตามที่เราต้องการ
1. บอกว่าได้รับการวินิจฉัยเบาหานหรือความดัน
select t_visit.t_visit_id, t_visit.visit_vn, substring(t_visit.visit_begin_visit_time,1,10) ,t_diag_icd10.diag_icd10_number
from t_visit inner join t_diag_icd10
    on t_visit.t_visit_id = t_diag_icd10.diag_icd10_vn
where t_diag_icd10.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
and t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก
and (upper(substring(t_diag_icd10.diag_icd10_number,1,3)) between 'E10' and 'E14' --เบาหวาน
        or
        upper(substring(t_diag_icd10.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';
2. แต่เราต้องการการวินิจฉัยเบาหวาน และความดัน จากข้อ 1 คงบอกไม่ได้เพราะอยู่คนละบรรทัด อีกทางที่เราจะทำได้ คือ มองตาราง t_diag_icd10 เป็น 2 ตารางก็ได้ ดังนี้
select t_visit.t_visit_id, t_visit.visit_vn, substring(t_visit.visit_begin_visit_time,1,10) ,icd_dm.diag_icd10_number,icd_ht.diag_icd10_number
from t_visit inner join t_diag_icd10 as icd_dm --แทนตารางเบาหวาน
    on (t_visit.t_visit_id = icd_dm.diag_icd10_vn
    and icd_dm.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_dm.diag_icd10_number,1,3)) between 'E10' and 'E14' ) --เบาหวาน
 inner join t_diag_icd10 as icd_ht --แทนตารางความดัน
    on (t_visit.t_visit_id = icd_ht.diag_icd10_vn
    and icd_ht.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_ht.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน
where t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';
3. เมื่อเราได้ผู้ป่วยเบาหวานและมีความดันร่วมด้วยแล้ว เราก็มา Focus ที่สิทธิที่รับบริการเพิ่มเติม ดังนี้

select t_visit.t_visit_id, t_visit.visit_vn, substring(t_visit.visit_begin_visit_time,1,10) ,icd_dm.diag_icd10_number,icd_ht.diag_icd10_number,r_rp1853_instype.maininscl
from t_visit inner join t_diag_icd10 as icd_dm --แทนตารางเบาหวาน
    on (t_visit.t_visit_id = icd_dm.diag_icd10_vn
    and icd_dm.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_dm.diag_icd10_number,1,3)) between 'E10' and 'E14' ) --เบาหวาน
 inner join t_diag_icd10 as icd_ht --แทนตารางความดัน
    on (t_visit.t_visit_id = icd_ht.diag_icd10_vn
    and icd_ht.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_ht.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน
inner join t_visit_payment --สิทธิที่ผู้รับบริการใช้ในการรับบริการ
   
on t_visit.t_visit_id = t_visit_payment.t_visit_id
   
and t_visit_payment.visit_payment_active = '1' --จะต้องไม่ถูกยกเลิก
inner join b_contract_plans
   
on t_visit_payment.b_contract_plans_id = b_contract_plans.b_contract_plans_id
inner join r_rp1853_instype
  
  on b_contract_plans.r_rp1853_instype_id = r_rp1853_instype.id
   
and r_rp1853_instype.maininscl in ('WEL','UCS',''--สิทธิ UC
where t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';
4. และจากโจทย์จะต้องมีความดันเช้ามาเกี่ยวข้อง เราก็ Join ตาราง t_visit_vital_sign เพิ่มเติมได้เลยค่ะ แต่ต้องวัดความดันนะคะ
select t_visit.t_visit_id, t_visit.visit_vn, substring(t_visit.visit_begin_visit_time,1,10) ,icd_dm.diag_icd10_number,icd_ht.diag_icd10_number,r_rp1853_instype.maininscl, t_visit_vital_sign.visit_vital_sign_blood_presure
from t_visit inner join t_diag_icd10 as icd_dm --แทนตารางเบาหวาน
    on (t_visit.t_visit_id = icd_dm.diag_icd10_vn
    and icd_dm.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_dm.diag_icd10_number,1,3)) between 'E10' and 'E14' ) --เบาหวาน
 inner join t_diag_icd10 as icd_ht --แทนตารางความดัน
    on (t_visit.t_visit_id = icd_ht.diag_icd10_vn
    and icd_ht.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_ht.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน
inner join t_visit_payment --สิทธิที่ผู้รับบริการใช้ในการรับบริการ
   
on t_visit.t_visit_id = t_visit_payment.t_visit_id
   
and t_visit_payment.visit_payment_active = '1' --จะต้องไม่ถูกยกเลิก
inner join b_contract_plans
   
on t_visit_payment.b_contract_plans_id = b_contract_plans.b_contract_plans_id
inner join r_rp1853_instype
  
  on b_contract_plans.r_rp1853_instype_id = r_rp1853_instype.id
   
and r_rp1853_instype.maininscl in ('WEL','UCS',''--สิทธิ UC
inner join t_visit_vital_sign
   
on t_visit.t_visit_id = t_visit_vital_sign.t_visit_id
   
and t_visit_vital_sign.visit_vital_sign_active = '1' --รายการไม่ถูกยกเลิก
   
and t_visit_vital_sign.visit_vital_sign_blood_presure <> '' --ต้องมีการวัดความดันเท่านั้น
where t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';
5. แต่ผลที่ต้องการ เราต้องการเฉพาะที่มีความดัน <= 130/80 งั้นก็มาตรวจสอบเพิ่มเติมเลยค่ะ 
select t_visit.t_visit_id, t_visit.visit_vn, substring(t_visit.visit_begin_visit_time,1,10) ,icd_dm.diag_icd10_number,icd_ht.diag_icd10_number,r_rp1853_instype.maininscl, t_visit_vital_sign.visit_vital_sign_blood_presure,cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,1,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)-1)) as numeric) as bp1,cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)+1)) as numeric) as bp2
from t_visit inner join t_diag_icd10 as icd_dm --แทนตารางเบาหวาน
    on (t_visit.t_visit_id = icd_dm.diag_icd10_vn
    and icd_dm.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_dm.diag_icd10_number,1,3)) between 'E10' and 'E14' ) --เบาหวาน
 inner join t_diag_icd10 as icd_ht --แทนตารางความดัน
    on (t_visit.t_visit_id = icd_ht.diag_icd10_vn
    and icd_ht.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_ht.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน 
inner join t_visit_payment --สิทธิที่ผู้รับบริการใช้ในการรับบริการ
   
on t_visit.t_visit_id = t_visit_payment.t_visit_id
   
and t_visit_payment.visit_payment_active = '1' จะต้องไม่ถูกยกเลิก
inner join b_contract_plans
   
on t_visit_payment.b_contract_plans_id = b_contract_plans.b_contract_plans_id
inner join r_rp1853_instype
  
  on b_contract_plans.r_rp1853_instype_id = r_rp1853_instype.id
   
and r_rp1853_instype.maininscl in ('WEL','UCS',''--สิทธิ UC
inner join t_visit_vital_sign
   
on t_visit.t_visit_id = t_visit_vital_sign.t_visit_id  
    and t_visit_vital_sign.visit_vital_sign_active = '1' --รายการไม่ถูกยกเลิก  
    and t_visit_vital_sign.visit_vital_sign_blood_presure <> '' --ต้องมีการวัดความดันเท่านั้น
where t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก 
and cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,1,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)-1)) as numeric) <= 130 --ช่วงบนน้องกว่าเท่ากับ 130
and cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)+1)) as numeric) <= 80 --ช่วงล่างน้อยกว่าเท่ากับ 80
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';
6. หลังจากที่ได้ตามเงื่อนไขโจทย์ที่กำหนดมาแล้ว เราก็ทำส่วนสุดท้าย คือการนับยอดซึ่งตรงนี้หากนับคน เราต้องนับตาม t_patient_id และ หากต้องการนับครั้ง เราต้องนับตาม t_visit_id ซึ่งผลลัพธ์จากที่เราได้ทำ Query มานั้นอาจจะเกิดเหตุการณ์เบิ้ล Record ได้ เราสามารถนำ Distinct เข้ามาช่วยในการนับแบบไม่ซ้ำได้ ดังนี้
select count(distinct t_visit.t_patient_id) as count_pat, count(distinct t_visit.t_visit_id) as count_visit
from t_visit inner join t_diag_icd10 as icd_dm --แทนตารางเบาหวาน
    on (t_visit.t_visit_id = icd_dm.diag_icd10_vn
    and icd_dm.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_dm.diag_icd10_number,1,3)) between 'E10' and 'E14' ) --เบาหวาน
 inner join t_diag_icd10 as icd_ht --แทนตารางความดัน
    on (t_visit.t_visit_id = icd_ht.diag_icd10_vn
    and icd_ht.diag_icd10_active = '1' --รายการ ICD-10 ไม่ถูกยกเลิก
    and upper(substring(icd_ht.diag_icd10_number,1,3)) between 'I10' and 'I15') --ความดัน 
inner join t_visit_payment --สิทธิที่ผู้รับบริการใช้ในการรับบริการ
   
on t_visit.t_visit_id = t_visit_payment.t_visit_id
   
and t_visit_payment.visit_payment_active = '1' จะต้องไม่ถูกยกเลิก
inner join b_contract_plans
   
on t_visit_payment.b_contract_plans_id = b_contract_plans.b_contract_plans_id
inner join r_rp1853_instype
  
  on b_contract_plans.r_rp1853_instype_id = r_rp1853_instype.id
   
and r_rp1853_instype.maininscl in ('WEL','UCS',''--สิทธิ UC
inner join t_visit_vital_sign
   
on t_visit.t_visit_id = t_visit_vital_sign.t_visit_id  
    and t_visit_vital_sign.visit_vital_sign_active = '1' --รายการไม่ถูกยกเลิก  
    and t_visit_vital_sign.visit_vital_sign_blood_presure <> '' --ต้องมีการวัดความดันเท่านั้น
where t_visit.f_visit_type_id = '0' --เฉพาะผู้ป่วยนอก
and t_visit.f_visit_status_id <> '4' --รายการ visit ไม่ถูกยกเลิก 
and cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,1,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)-1)) as numeric) <= 130 --ช่วงบนน้องกว่าเท่ากับ 130
and cast(substring(t_visit_vital_sign.visit_vital_sign_blood_presure,(position('/' in t_visit_vital_sign.visit_vital_sign_blood_presure)+1)) as numeric) <= 80 --ช่วงล่างน้อยกว่าเท่ากับ 80
and substring(t_visit.visit_begin_visit_time,1,10) between '2552-10-01' and '2553-09-30';


วันศุกร์ที่ 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

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

วันอังคารที่ 28 กันยายน พ.ศ. 2553

มาดูความสามารถของ Group By กับการ Count

สวัสดีค่ะ ผ่านไปไม่ถึงครึ่งวันก็มาต่อกันอีกแล้วนะคะ ก็เนื่องจากเป็นเรื่องเดียวกันค่ะ แต่จะซับซ้อนขึ้นมาอีกนิดนึง เพื่อให้เราสามารถเข้าใจและนำไปใช้ประโยชน์ได้จริง

เราก็คงพอเข้าใจกับการทำ Group By มาแล้วนะคะ สำหรับใครที่ยังไม่เข้าใจว่า Group By คิดยังไงขอให้กลับไปทำความเข้าใจกันก่อนค่ะ ที่ มาทำความเข้าใจคำว่า Group By ขึ้นพื้นฐาน

งั้นเรามาต่อกันเลยค่ะ ... ส่วนใหญ่การ Group By เราจะนำความสามารถของมัน มาใช้ร่วมกับ Function ต่างๆ เช่น การนับ(Count), การรวม(Sum), หาค่าสูงสุด(Max), หาค่าต่ำสุด(Min), หาค่ากลาง (Avg) เป็นต้น
โดยที่ทำงานกับ Record ที่ทำ Group By กันได้ เช่นการ Count คือ การนับจำนวน Record หาก เรา Group By ได้ 3 Record ผลที่จะได้จากการใช้ Count คือ 3  ... คนเขียนก็ งง คนอ่านไม่รู้ งง หม้าย 

ความเดิมจาก ตาราง test_table

field_a field_b field_c
123ทดสอบ1
123 ทดสอบ 2
123ทดสอบ2
123 ทดสอบ 2
123ทดสอบ3

หากเราต้องการนับจำนวน Record ที่มีค่าเหมือนกัน
ใช้คำสั่ง
SELECT field_a, field_b, field_c, COUNT(*) as count_rec
FROM
test_table
GROUP BY field_a, field_b, field_c;

ผลลัพธ์ที่ได้ คือ

field_afield_bfield_ccount_rec
123ทดสอบ11
123ทดสอบ23
123ทดสอบ31

จะเห็นว่าจำนวน Record จะถูกนับใน Field count_rec โดยจะนับจำนวน Record ตามที่ Group By ได้
ในส่วนนี้จะขอยกตัวอย่างจากการ Query ข้อมูลของ Hospital OS ประกอบเพื่อจะได้เห็นภาพ การทำงานที่ชัดเจนยิ่งขึ้น(หรือเปล่า) มาทำพร้อมๆกันนะคะ
เริ่มจาก
โจทย์ : ต้องการหารายชื่อ และ จำนวนครั้งที่มารับบริการ ใน 1 เดือนของผู้ป่วยที่มารับบริการเกิน 4 ครั้ง
ตีโจทย์ :
- หากต้องการายชื่อของผู้ป่วย สามารถหามาได้จากตาราง t_patient
- หากต้องการหากข้อมูลรับบริการ สามารถหามาได้จากตาราง t_visit
- สรุปว่าข้อมูล 2 ตารางนี้ก็น่าจะเพียงพอกับ โจทย์ ที่ต้องการนะคะ
ลองทำดู ทำเป็นขั้นเป็นตอน ดูผลเป็นขั้นเป็นตอนเพื่อทำความเข้าใจ ดังนี้

ขั้นตอนที่ 1 : นำข้อมูลมาเชื่อมโยงกันเพื่อให้ได้ข้อมูลชื่อและรายการที่รับบริการออกมาก่อน โดยเลือกข้อมูลที่คิดว่าจำเป็นในการนำมาใช้
คำสั่ง
SELECT t_patient.t_patient_id as patient_id --ใช้ as เพื่อตั้งชื่อใหม่
,t_patient.patient_hn as hn
,t_patient.patient_firstname as fname
,t_patient.patient_lastname as lname
,t_patient.patient_birthday as dob
,t_patient.patient_pid as pid
,t_visit.t_visit_id as visit_id --Field นี้มาจากตาราง t_visit เป็นข้อมูลรับบริการ เราเอามานับได้ค่ะ
FROM t_patient INNER JOIN t_visit
ON t_patient.t_patient_id = t_visit.t_patient_id
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'; --ข้อมูลรับบริการเดือนเมษายน

ผลลัพธ์ คือ ข้อมูลรับบริการในเดือนเมษายน 2553 ตาม Field ที่เรา Select (ลองเอาไป "รัน" ดูนะคะ) ก็ประมาณนี้ค่ะ ขึ้นอยู่กับข้อมูลของเรา
patient_id hn fname lname dob pid visit_id
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11 2554063684111
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11
2554063684202
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11 2554063684330
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11
2554063684890
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11 2554063684999
206000037798 330000001 ทดสอบชื่อ ทดสอบนามสกุล 2534-04-11
2554063685000
... ... ... ... ... ... ...

ขั้นตอนที่ 2 :จากผลลัพธ์ของครั้งแรกจะเห็นว่าข้อมูลที่ซ้ำๆ กัน จะมาจากตาราง t_patient(ข้อมูลผู้ป่วย) ส่วนข้อมูลที่เป็นการรับบริการจะไม่ซ้ำกัน สังเกตุใน field visit_id เป็นเลขลำดับไม่ซ้ำกัน ซึ่งข้อมูลมาจากตาราง t_visit(ข้อมูลรับบริการ)
ดังนั้น เราจะต้องนับจำนวนรับบริการตามรายละเอียดของผู้ป่วยที่เหมือนกัน(ที่มาจากตาราง t_patient ทั้งหมด) แสดงว่าข้อมูลที่มาจากตาราง t_patient เราจะต้องนำไปทำ Group By เพื่อรวมเป็น Record เดียวกัน และนับจำนวน Record ที่ Group By กันได้(ข้อมูลเหมือนกันในทุก Field ที่กำหนดให้ Group By) ...อ่านแล้ว งง เองอีกแระ มาดูคำสั่งและผลลัพธ์ดีกว่า
คำสั่ง
SELECT t_patient.t_patient_id as patient_id --ใช้ as เพื่อตั้งชื่อใหม่
,t_patient.patient_hn as hn
,t_patient.patient_firstname as fname
,t_patient.patient_lastname as lname
,t_patient.patient_birthday as dob
,t_patient.patient_pid as pid
,count(t_visit.t_visit_id) as count_visit
FROM t_patient INNER JOIN t_visit
ON t_patient.t_patient_id = t_visit.t_patient_id
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 patient_id,hn,fname,lname,dob,pid; --ตรงนี้จะบอกว่าเอาชื่อ Field ที่เราทำ as มาใช้เลยก็ได้ค่ะ  แต่ถ้าอยากให้อ่านแล้วเข้าใจเลยก็เอาชื่อ Field มาอ้างได้ค่ะ ได้ผลเหมือนกัน
ผลลัพธ์ คือ ข้อมูลจำนวนครั้งของผู้ป่วยที่มารับบริการในเดือนเมษายน (ลองเอาไป "รัน" ดูนะคะ) เหมือนเดิม ข้อมูลก็อาจจะเป็น ขึ้นอยู่กับข้อมูลของเรา
patient_idhnfnamelnamedobpidcount_visit
206000037798330000001ทดสอบชื่อทดสอบนามสกุล2534-04-11
6
206000037799330000002ทดสอบชื่อ2ทดสอบนามสกุล22524-09-10
2
206000037800330000003ทดสอบชื่อ3ทดสอบนามสกุล32521-04-19
5

ขั้นตอนที่ 3 : จากผลลัพธ์ในข้อสองนั้น เราก็ยังไม่ได้ตามที่โจทย์ต้องการย้อนกลับไปอ่านโจทย์ เรายังขาดเรื่องของจำนวนครั้งที่มารับบริการมากกว่า 4 ครั้ง ดังนั้นเราก็มาทำตามที่ควรจะได้กันดีกว่า
การที่เราจะกำหนดเงื่อนไขจากผลลัพธ์ของ Function Count (หรือ Function อะไรก็แล้วแต่ที่ใช้ร่วมกับ Group By) เราจะต้องรู้จักคำนี้ Having -> นั่นคือตัวช่วยของเราค่ะ
Having
- จำเป็นต้องใช้ร่วมกับการทำ Group By ค่ะ จู่ๆ จะมา Having ไม่ได้ (หรือได้ งง)
- จะต้องอยู่หลัง Group By
- จะต้องยกคำสั่งมาเป็นเงื่อนไข หมายถึง ยกการทำงานของ Function มาเป็นเงื่อนไข เช่น count(t_visit.t_visit_id) > 4 
งั้นเรามาดูคำสั่งแบบเต็มๆ กันเลยนะคะ
คำสั่ง
SELECT t_patient.t_patient_id as patient_id --ใช้ as เพื่อตั้งชื่อใหม่
,t_patient.patient_hn as hn
,t_patient.patient_firstname as fname
,t_patient.patient_lastname as lname
,t_patient.patient_birthday as dob
,t_patient.patient_pid as pid
,count(t_visit.t_visit_id) as count_visit
FROM t_patient INNER JOIN t_visit
ON t_patient.t_patient_id = t_visit.t_patient_id
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 patient_id,hn,fname,lname,dob,pid
HAVING count(t_visit.t_visit_id) > 4; --มารับบริการมากกว่า 4 ครั้งในช่วงเดือนที่กำหนด ในที่นี้คือ เมษายน
ผลลัพธ์ คือ ข้อมูลจำนวนครั้งของผู้ป่วยที่มารับบริการในเดือนเมษายนที่มารับบริการมากกว่า 4 (ลองเอาไป "รัน" ดูนะคะ) เหมือนเดิม ข้อมูลก็อาจจะเป็น ขึ้นอยู่กับข้อมูลของเรา
patient_idhnfnamelnamedobpidcount_visit
206000037798330000001ทดสอบชื่อทดสอบนามสกุล2534-04-11
6
206000037800330000003ทดสอบชื่อ3ทดสอบนามสกุล32521-04-195

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

งั้น ขอแถมอีกนิดนะคะ เผื่อมีประโยชน์ ถ้าเราต้องการเรียงลำดับจากมากไปหาน้องของโทจทย์นี้ก็เพิ่ม Order By ไปท้ายสุดเลยค่ะ

คำสั่ง
SELECT t_patient.t_patient_id as patient_id --ใช้ as เพื่อตั้งชื่อใหม่
,t_patient.patient_hn as hn
,t_patient.patient_firstname as fname
,t_patient.patient_lastname as lname
,t_patient.patient_birthday as dob
,t_patient.patient_pid as pid
,count(t_visit.t_visit_id) as count_visit
FROM t_patient INNER JOIN t_visit
ON t_patient.t_patient_id = t_visit.t_patient_id
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 patient_id,hn,fname,lname,dob,pid
HAVING count(t_visit.t_visit_id) > 4 --มารับบริการมากกว่า 4 ครั้งในช่วงเดือนที่กำหนด ในที่นี้คือ เมษายน
ORDER BY count_visit DESC;

เพิ่มเติมสำหรับ Order By ที่ควรรู้
- Order By ต้องอยู่ท้ายสุดของการเขียน Query
- หากเราไม่เติมคำสั่งบังคับหลัง Order By จะถือว่าเรียงจากน้อยไปหามาก โดยปริยาย เช่น ORDER BY field_a
- หากต้องการกำกับให้เรียงจากมากไปหาน้อย ให้ใส่ DESC ตามหลัง Field ที่กำหนดให้เรียง เช่น ORDER BY field_a DESC
- หากต้องการกำกับให้เรียงจากน้อยไปหามาก ให้ใส่ ASC ตามหลัง Field ที่กำหนดให้เรียง เช่น ORDER BY field_a ASC
- สามารถเรียงลำดับมากกว่า 1 Field ได้ โดยจะทำงานจากซ้าย(ติดกับ Order By) โดยมี comma คั้นระหว่าง Field และมีตัวกำกับหลัง Field แตกต่างกับได้ (DESC,ASCเช่น ORDER BY field_a DESC, field_b ASC, field_c DESC


อันนี้ก็ต้องลองถึงจะรู้นะคะ ... สำหรับวันนี้คงพอก่อนเอาไว้มาต่อเรื่อง Group By , Distinct กันอีกนะคะ ...to be continue