Posted by: siamnobita | 06/17/2010

Finding continuous number

หัวข้อในวันนี้ได้รับการอนุเคราะห์จากคำถามของคุณ Artelrooy ที่เว็บบอร์ด pantip

ทางผมต้องการ นับ ข้อมูล เฉพาะ down โดยแต่ละรอบจะมีข้อมูลเข้ามาทุก 5 นาที ครับ เช่น รอ 12.00  ได้ down ก็จะนับ 1  แต่ต่อมา รอบ 12.05 ส่ง down มาอีกจะไม่นับ  จนกว่า ข้อมูล จะเป็น up  แล้ว ส่ง down มาให้อีกทีจะนับ เป็น 2 ครับ เงื่อนไขคือ ถ้ามี down ส่งมาติดต่อ กัน จะนับ เพียง 1 ครั้ง ครับ  ไม่ทราบว่าต้องเขียน query อย่างไรครับ

เห็นว่าคำถามนี้น่าสนใจดี สามารถใช้เป็นตัวอย่างให้เห็นประโยชน์ที่บางคนคิดไม่ถึงของ analytic functions ได้

สำหรับผู้อ่านที่คุ้นเคยกับ analytic functions ของ Oracle ดีอยู่แล้วอ่านคำถามครั้งแรกก็น่าจะนึกถึง lead, lag functions ซึ่งสามารถใช้ในการเปรียบเทียบข้อมูลในแถวปัจจุบันกับข้อมูลในแถวก่อนหน้าได้ อย่างไรก็ดีสำหรับ database ยี่ห้ออื่น เท่าที่ผมรู้ยังไม่มี function 2 ตัวนี้ ดังนั้น วันนี้ผมจะแสดงตัวอย่างของการนำ function row_number มาประยุกต์ใช้ในกรณีนี้ครับ

SQL> select tt1, status
  2  from down_time
  3  /
TT1      STAT
-------- ----
00:00:00 UP
...
09:05:00 UP
09:10:00 DOWN
09:15:00 UP
...
19:40:00 UP
19:45:00 DOWN
...
20:30:00 DOWN
20:35:00 UP
...
21:55:00 UP
22:00:00 DOWN
...
22:15:00 DOWN
22:20:00 UP
...
23:55:00 UP
288 rows selected.

SQL> select count(distinct (rn1-rn2)) cnt_down
  2  from (
  3  select status
  4    , row_number () over (order by tt1) rn1
  5    , row_number () over (partition by status order by tt1) rn2
  6  from down_time
  7  )
  8  where status = 'DOWN'
  9  /
  CNT_DOWN
----------
         3

tips นี้ผมได้ต้นแบบมาจาก otn forum โดยคุณ Rod West และ Vadim Tropashkoโดย query ต้นฉบับนั้นใช้ตอบคำถามว่าเราจะยุบแถวหลาย ๆ แถว ให้เหลือเป็นแต่ละช่วงที่ต่อเนื่องกันได้อย่างไร เช่น 1,2,3,5,6,8,9,10 ก็เหลือเพียง 3 แถว คือ 1-3, 5-6 และ 8-10 เป็นต้น

สำหรับคำถามที่ต้องการยุบข้อมูลมากกว่าหนึ่งแถวให้เหลือเพียงหนึ่งแถวนั้น ส่วนใหญ่เราต้องตอบด้วย group by และ aggregate functions ปัญหาคือเราจะ group by ด้วยอะไร ผมจะแสดงวิธีแก้ปัญหาของเขาโดยใช้ rownum

SQL> select rownum, n1, n1-rownum diff
  2  from t1
  3  order by n1 ;
    ROWNUM         N1       DIFF
---------- ---------- ----------
         1          1          0
         2          2          0
         3          3          0
         4          5          1
         5          6          1
         6          8          2
         7          9          2
         8         10          2
8 rows selected.

ดูจากตัวอย่างข้างบนจะเห็นได้ว่าทุกค่าที่อยู่ติดกัน จะมีผลต่างระหว่างค่ากับ rownum ที่เท่ากัน ผลต่างนี้คือสิ่งที่เราจะใช้ในการ group by

SQL> select min(n1) n_from, max(n1) n_to
  2  from ( select rownum rn, n1 from t1 order by n1 )
  3  group by n1-rn
  4  order by n_from
  5  /
    N_FROM       N_TO
---------- ----------
         1          3
         5          6
         8         10

กลับมาที่คำถามของเราจะเห็นว่ายากขึ้นอีกนิด แต่ก็เป็นคำถามที่ไม่ต่างจากคำถามนี้ในแง่ที่ว่าต้องการยุบ status ที่มีค่า ‘DOWN’ ที่อยู่ติดกันให้เหลือเพียงหนึ่ง สิ่งที่ผมต้องทำเพิ่มก็คือสร้าง running number ขึ้นมาสองตัว ตัวหนึ่งนับเฉพาะค่าที่ เป็น ‘DOWN’ (ซึ่งคือ row_number ที่มี partition by clause ) อีกตัวนับทั้งหมด (ซึ่งคือ row_number ที่มีแต่ order by clause)  จากนั้นเมื่อกรองข้อมูลมาดูเฉพาะที่เราสนใจคือ ‘DOWN’ เราก็จะได้ตัวเลขสองชุด ชุดที่หนึ่งเรียงกันแบบต่อเนื่อง ส่วนชุดที่สองเป็นตัวเลขที่มี gap ซึ่งเกิดจากข้อมูลที่ไม่ใช่ down เพียงเท่านี้ เราก็จะได้คำถามในลักษณะเดียวกันแล้ว แต่ไม่ได้ต้องการค่าสูงสุด ต่ำสุดในแต่ละช่วง แต่ต้องการนับว่ามีกี่ช่วง ซึ่งสามารถตอบได้ด้วย function count (distinct ..)

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

SQL is fun!


Responses

  1. ขอบคุณครับ มีประโยชน์กับผมมากเลย


ใส่ความเห็น

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / เปลี่ยนแปลง )

Twitter picture

You are commenting using your Twitter account. Log Out / เปลี่ยนแปลง )

Facebook photo

You are commenting using your Facebook account. Log Out / เปลี่ยนแปลง )

Google+ photo

You are commenting using your Google+ account. Log Out / เปลี่ยนแปลง )

Connecting to %s

หมวดหมู่

%d bloggers like this: