MySQL generates a sequence of numeric values

หลังจากได้อ่านและลอง Sequence ใน MariaDB 10.3 แล้ว มันช่างง่ายเหลือเกินให้ตายเถอะโรบิน (แนะนำให้ไปอ่านยังไงก็ได้ใช้แน่ ๆ ) แต่บล๊อกนี้เราจะไม่ได้เขียนเรื่องนี้ 5555 เราจะใช้วิธีที่เรียกว่า CTE (Common Table Expression) สำหรับการสร้างชุดข้อมูล CTE ซึ่งเริ่มใช้ได้ใน MySQL 8.0, MariaDB 10.2 เป็นต้นมาซึ่งเอาจริง ๆ ก็ง่ายพอ ๆ กันแหล่ะ เราเคยเขียนในบทความเก่า ๆ ลองไปหาอ่านกันได้ หรือของ อ.ประเสริฐ

ลักษณของ CTE ใช้การเรียกโดยวิธี Recursive รูปแบบใน MySQL จะเป็นแบบนี้

with_clause:
    WITH [RECURSIVE]
        cte_name [(col_name [, col_name] ...)] AS (subquery)
        [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...

ลักษณะของคำสั่งนี้คือ

สมมุติเรามีโจทย์ตามข้างต้น คือการสร้างตารางชั่วคราวที่เก็บ sequence number จาก 1 ถึง 100 เราก็เขียนได้ดังนี้

WITH RECURSIVE numbers AS (
    SELECT 1 AS value
    UNION ALL
    SELECT value + 1 AS value
    FROM numbers
    WHERE numbers.value < 100
)
SELECT *
FROM numbers;

ผลลัพธ์ก็ตามนี้ ดูได้ใน dbfiddle

สั้น ๆ และง่ายใช่ไหม อ่านจบตามไปอ่าน  sequence ต่อ แนะนำเลย
หลับฝันดีมีแฮง ^__^

Split comma separated string to multiple rows

สำหรับใครที่เคยใช้ฟังก์ชั่น GROUP_CONCAT() ใน  MySQL มาก่อนก็พอจะเดาผลลัพธ์จากฟังก์ชั่นนี้ได้ว่า ผลลัพธ์จะเป็นค่าตามคอลัมภ์ที่ถูกกรุ๊ป (GROUP BY) และนำมาต่อกันด้วยเครื่องหมายที่ระบุ ปกติค่าดีฟอลต์จะเป็น comma ‘,’ รูปแบบคำสั่งก็จะประมาณนี้

GROUP_CONCAT([DISTINCT] expr [,expr ...]
             [ORDER BY {unsigned_integer | col_name | expr}
                 [ASC | DESC] [,col_name ...]]
             [SEPARATOR str_val])

ตัวอย่าง


**ภาพจาก mysqltutorial

ข้างบนนี่คือต้นเหตุ มักจะมีกรณีที่เราได้ผลลัพธ์มาแล้ว นั่นคือ “A,B,C” และเราต้องการแยกข้อความที่ได้มาออกเป็นแต่ละแถว (ตาราง t ก่อนที่จะผ่านฟังก์ชั่น GROUP_CONCAT() นั่นแหล่ะ) ดูตัวอย่างกัน

CREATE TABLE DEMO (
    ID INTEGER PRIMARY KEY AUTO_INCREMENT,
    PID VARCHAR(20) NOT NULL,
    ICD10LIST VARCHAR(255) DEFAULT NULL
);
 
INSERT INTO DEMO(ID, PID, ICD10LIST) VALUES(NULL, '101', 'E119,E112,I10'), (NULL, '102', 'E119,E112'), (NULL, '103', 'E119,I10');

ข้อมูลในตารางเป็นแบบนี้ (คุ้น ๆ กันไหม 5555)

**แนวคิดของคำสั่งก็คือทำการสร้างตารางค่า Index ที่อยู่ในชุด/เซตข้อความเพื่อแยกแต่ละไอเท็มออกมา

SELECT
  DEMO.ID,
  DEMO.PID,
  SUBSTRING_INDEX(SUBSTRING_INDEX(DEMO.ICD10LIST, ',', numbers.n), ',', -1) AS ICD10
FROM
  (SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) numbers INNER JOIN DEMO
  ON CHAR_LENGTH(DEMO.ICD10LIST)-CHAR_LENGTH(REPLACE(DEMO.ICD10LIST, ',', '')) >= numbers.n-1
ORDER BY
  PID, n

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

จบปิ๊ง !!!

ป.ล.

  • ตาราง numbers จะสร้างไว้ก่อนก็ได้ ปกติก็ใช้บ่อย ๆ นะครับจะด้วย RECURSIVE CTE ก็ได้หรือตัวนี้
  • ตัว Split String จะทำเป็นฟังก์ชั่นไว้ก็ได้นะ ^_^
  • ข้อมูลเป็นข้อมูลสมมุติจาก Data Exchange สมมุติแห่งหนึ่ง

 

 

Hierarchical and recursive queries in MySQL

Hierarchical query คืออะไร ?

A hierarchical query is a type of SQL query that handles hierarchical model data. They are special cases of more general recursive fixpoint queries, which compute transitive closures.

อยากบอก(บ่น) ว่าก่อนที่ MySQL 8 จะออกนี่การดึงข้อมูลแบบลำดับชั้นลักษณะต้นไม้ (Tree) แบบนี้โคตรเปลืองพลังงาน แต่ตอนนี้ดีขึ้นมาก ต้องขอบคุณจริง ๆ ที่ MySQL อิมพลีเมนต์ Common Table Expression (CTE) ให้ใช้ได้ซะที เขียนคำสั่งสั้นลง ประสิทธิภาพดีขึ้น (ก็แหงสิเจ้าของเค้าทำออกมาเอง)

ยกตัวอย่างข้อมูลลักษณะแบบลำดับชั้น เช่น โครงสร้างองค์กร เธรดในคอมเมนต์ต่าง ๆ ข้อมูล Categories and sub-categories ที่เห็นภาพชัด ๆ  เลยคือ โครงสร้างองค์กร (Organization charts ) นี่ง่ายสุดละ มีหัวหน้าเป็นรูทและมีลูกน้องในความดูแลและบางทีลูกน้องก็มีเบ้ต่อลงไปอีกที ลักษณะข้อมูลแบบรูปนี้เลย


ว่าแล้วก็ติดตั้ง MySQL 8 สำหรับใช้งานกัน

docker run -d -p 3306:3306 --name mysqltest --restart always -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=demo mysql:8

โดยธรรมเนียมปฏิบัติทั่วไป ของตารางที่เก็บข้อมูลในลักษณะนี้ก็จะออกแบบคล้าย ๆ แบบนี้ โดยให้คอลัมภ์ parent เก็บข้อมูลหัวหน้า

DROP TABLE IF EXISTS category;
CREATE TABLE IF NOT EXISTS category(
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        parent INT DEFAULT NULL
);

INSERT INTO category VALUES
(1,'Root A', NULL),
(2,'Item 1', 1),
(3,'Item 2', 1),
(4,'Item 3', 3),
(5,'Root B', NULL),
(6,'Item 4', 5),
(7,'Item 5', 6),
(8,'Root C', NULL),
(9,'Item 6', 8);

การดึงข้อมูลในลักษณะ Recursive MySQL ได้เตรียมคำสั่งให้แล้ว รูปแบบการใช้งาน

with_clause:
    WITH [RECURSIVE]
        cte_name [(col_name [, col_name] ...)] AS (subquery)
        [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...

ลักษณะของคำสั่งนี้คือ


*ภาพจาก mysqltutorial : A Definitive Guide To MySQL Recursive CTE

เรามาดูผลลัพธ์กัน

WITH RECURSIVE categorypath(id, name, dept, breadcrumbs) AS
(
  SELECT id, name, 0, CAST(name AS CHAR(1000))
  FROM category 
  WHERE parent IS NULL
  UNION ALL
  SELECT c.id, c.name, cp.dept + 1, CONCAT_WS('/', cp.breadcrumbs, c.name) 
  FROM categorypath cp 
    JOIN category c ON cp.id = c.parent
)
SELECT * FROM categorypath ORDER BY breadcrumbs;

ไปอ่านเพิ่มเติมกันได้ที่

  • https://mysqlserverteam.com/mysql-8-0-labs-recursive-common-table-expressions-in-mysql-ctes/
  • https://www.percona.com/live/17/sites/default/files/slides/Recursive%20Query%20Throwdown.pdf
  • https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL

จบปิ๊ง ^__^
ป.ล.1 วันหยุด วันฝนตกฉันควรนอนกกใครสักคนอยู่ใต้ผ้าห่มนี่นา
ป.ล.2 เศร้าาาวันทำงาน

Get random 2 rows from each group

การสุ่มตัวอย่าง (Sampling) หมายถึง กระบวนการเลือก “ตัวอย่าง” จาก “ประชากร” เพื่อให้กลุ่มตัวอย่างเป็นตัวแทนของประชากรในการให้ข้อมูล การที่จะเลือกตัวอย่างให้เป็นตัวแทนที่ดีของประชากรได้นั้น จะต้องทำการเลือกแบบสุ่ม (random) หรือเลือกอย่างไม่ลำเอียง (unbias)

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

ในกลุ่มมีคำถามนึงน่าสนใจ ถามขึ้นในกลุ่มตามนี้

คร่าว ๆ ก็คืออยากได้กลุ่มข้อมูลมาจำนวนนึงแยกตามรหัส/กลุ่มการวินิจฉัยโรค มาดูอีก 1 วิธีกัน

  1. สร้างตารางทดสอบกัน (เอาตารางเกี่ยวกับผลไม้เนี่ยแหล่ะ เมื่อเช้าจัดมา //แพคเกจแบบนี้โคตรตอบโจทย์เอาจริง ๆ คือกินวันนึงก็ 1-2 ลูกเอง เยอะหน่อยก็ช่วงเล่นกีฬา แล้วกล้วยหอมหวีนึงนี่ก็ใช่ว่าหวีเล็ก ซื้อมาเป็นหวีทีก็กินไม่หมด T_T)

    CREATE TABLE fruits
        (`type` varchar(50), `variety` varchar(50), `price` float)
    ;
        
    INSERT INTO fruits
        (`type`, `variety`, `price`)
    VALUES
     ('apple', 'gala', 2.79),
     ('apple', 'fuji', 0.24),
     ('apple', 'limbertwig', 2.87),
     ('orange', 'valencia', 3.59),
     ('orange', 'navel', 9.36),
     ('pear', 'bradford', 6.05),
     ('pear', 'bartlett', 2.14),
     ('cherry', 'bing', 2.55),
     ('cherry', 'chelan', 6.33);

     

  2. ใช้ฟังก์ชั่น RAND() ใน MySQL เพื่อให้ได้ค่าตัวเลขที่ได้จากการสุ่มขึ้นมาและทำการจัดเรียงตามกลุ่ม (กำหนดเงื่อนไข) ในที่นี้เรียงตาม `type`

    RAND() – Returns a random floating-point value v in the range 0 <= v < 1.0

    SELECT
        type,
        variety,
        price,
        RAND()
    FROM fruits
    ORDER BY type, 4

  3. ใส่ลำดับที่ของแต่ละกลุ่มไว้
    SET @num:=0, @type:='';
    SELECT
        @num:=IF(@type = A.type, @num + 1, 1) as number,
        @type:=A.type as dummy,
        A.*
    FROM (
      SELECT
        type,
        variety,
        price,
        RAND()
      FROM fruits
      ORDER BY type, 4
    ) A

  4. เลือกจำนวนกลุ่มตัวอย่างตามที่ต้องการ (ในคำถามเลือก 2 ตัวอย่าง)
    ...
    WHERE number <= 2

    ผลลัพธ์ที่ได้คือรายการที่ได้จากการสุ่มกลุ่มละ 2 ตัวอย่าง

  5. ขอให้สนุกกับเดือนแห่ง QOF จร้าาา

ป.ล.
ลองรันทดสอบได้ที่ http://sqlfiddle.com/#!9/350bc7/6

Dynamic pivoting in MySQL

คิดถึงรายงานคิดถึง Excel 555 … เรื่องความตั้งใจในการเรียน Microsoft Excel นี่มีมาสักพักแล้ว เริ่มด้วยการซื้อหนังสือมาก่อนแต่น่าจะหลายเดือนละ (จนลืมไปเลยว่าซื้อมาตอนไหน บาปมาก) แต่ก็นะความขี้เกียจครอบงำ ช่วงนี้ก็เลยเป็นช่วงอ่านหนังสือ Microsoft Excel แบบจริงจัง (แอบตั้งใจ)
เปิดดูคร่าว ๆ มีบทนึงพูดถึงเรื่องการทำ Pivot Table ในโปรแกรมตระกูลตารางคำนวณเรื่องนี้ทุกตัวในตลาดก็สามารถทำได้ ถือเป็นหนึ่งใน Killer Feature แต่เรื่องนี้แค่ประเด็นนิดหน่อย ^_^

สำหรับคนที่เขียนคำสั่งเรียกดูข้อมูล (SQL) ก็ทำเรื่องนี้อยู่เนือง ๆ หลายครั้งก็ทำแบบฮาร์ดคอร์ ณ ตอนนั้นเลย ตัวอย่างคร่าว ๆ สมมุติเรามีข้อมูลค่าใช้จ่ายภายในบ้านสรุปประมาณนี้ ในฐานข้อมูล (ด้านซ้าย) และต้องการแสดงผลเป็นอีกรูปด้านขวา

SQL แสดงรายงานแบบนี้ อนุมานจากข้อมูลดิบที่เก็บในฐานข้อมูล ก่อนที่เราจะสรุปได้ก็น่าจะมีประมาณนี้ (เราใช้บริการของ generatedata.com) เอาตัวอย่างข้อมูลสัก 100 รายการหล่ะกัน

DROP TABLE IF EXISTS `ExpenseCategory`;
CREATE TABLE `ExpenseCategory` (
  `CategoryId` varchar(255) NOT NULL,
  `CategoryName` varchar(255) default NULL,
  PRIMARY KEY (`CategoryId`)
);

INSERT INTO `ExpenseCategory` (`CategoryId`,`CategoryName`) VALUES ("01","ความบันเทิง"),("02","ของชำ"),("03","ครอบครัว"),("04","การคมนาคมขนส่ง");

DROP TABLE IF EXISTS `Expense`;
CREATE TABLE `Expense` (
  `id` mediumint(8) unsigned NOT NULL auto_increment,
  `CategoryId` varchar(255) default NULL,
  `ExpenseDate` varchar(255),
  `Amount` varchar(100) default NULL,
  PRIMARY KEY (`id`)
) AUTO_INCREMENT=1;

INSERT INTO `Expense` VALUES (1,'04','2018-07-19 02:55:09','83369'),(2,'01','2018-01-17 19:18:21','28001'),(3,'04','2018-06-29 03:32:27','17058'),(4,'04','2018-07-04 17:44:03','64619'),(5,'04','2018-07-22 17:38:56','24050'),(6,'04','2018-08-15 03:43:50','80585'),(7,'03','2018-06-11 23:52:59','79427'),(8,'02','2018-04-29 04:03:38','63177'),(9,'01','2018-09-03 04:02:46','77975'),(10,'04','2018-07-04 21:49:15','61752'),(11,'01','2018-02-21 11:39:34','59245'),(12,'04','2018-01-05 05:42:14','15695'),(13,'02','2018-09-13 09:07:51','22132'),(14,'02','2018-08-12 13:24:09','58156'),(15,'01','2018-08-14 18:34:16','7802'),(16,'01','2018-04-04 04:26:02','44733'),(17,'03','2018-11-03 02:59:01','73158'),(18,'04','2018-01-23 09:59:45','69724'),(19,'02','2018-11-16 07:39:50','98963'),(20,'03','2018-01-04 11:20:59','93659'),(21,'02','2018-08-25 22:01:10','13945'),(22,'04','2018-04-08 22:46:07','90735'),(23,'01','2018-09-13 10:20:59','76856'),(24,'01','2018-09-02 08:45:17','54668'),(25,'03','2018-12-06 12:19:30','45571'),(26,'02','2018-05-27 09:48:40','24694'),(27,'01','2018-04-15 03:01:29','73961'),(28,'01','2018-03-11 06:05:34','53546'),(29,'02','2018-12-27 06:57:17','91375'),(30,'04','2018-02-05 05:14:19','44997'),(31,'02','2018-12-15 02:35:52','54144'),(32,'02','2018-08-01 02:06:37','95898'),(33,'02','2018-02-18 08:50:43','79249'),(34,'02','2018-03-26 04:39:35','66184'),(35,'01','2018-07-26 13:17:03','44670'),(36,'02','2018-12-26 13:50:39','88500'),(37,'04','2018-04-19 20:26:25','31414'),(38,'01','2018-07-28 13:53:24','56802'),(39,'02','2018-04-27 06:18:15','67682'),(40,'01','2018-10-11 12:25:37','98233'),(41,'03','2018-09-28 12:36:05','43118'),(42,'02','2018-08-17 16:06:34','94942'),(43,'02','2018-12-16 16:28:16','25039'),(44,'04','2018-04-14 15:47:06','87368'),(45,'03','2018-11-21 16:53:01','49355'),(46,'04','2018-01-06 17:08:54','63354'),(47,'03','2018-02-02 11:06:33','26540'),(48,'01','2018-10-19 03:50:49','32534'),(49,'01','2018-05-15 23:40:11','58038'),(50,'03','2018-04-28 23:53:24','2915'),(51,'01','2018-03-30 18:57:12','10229'),(52,'03','2018-04-22 07:47:45','20004'),(53,'03','2018-08-16 23:42:08','69185'),(54,'02','2018-03-09 20:38:52','92533'),(55,'02','2018-12-25 23:51:10','66589'),(56,'04','2018-09-22 05:40:21','94759'),(57,'01','2018-06-03 13:24:32','76588'),(58,'03','2018-04-21 07:27:01','66573'),(59,'02','2018-09-25 03:49:02','76607'),(60,'01','2018-08-11 10:48:24','47755'),(61,'04','2018-11-02 02:37:54','69570'),(62,'02','2018-08-31 14:44:15','25560'),(63,'02','2018-03-23 07:24:59','78550'),(64,'04','2018-06-07 17:39:12','18455'),(65,'02','2018-10-31 11:14:30','71227'),(66,'01','2018-10-31 14:23:16','2454'),(67,'04','2018-08-14 21:16:17','54700'),(68,'04','2018-06-05 01:04:59','59554'),(69,'01','2018-01-23 10:33:50','37798'),(70,'04','2018-06-09 12:00:56','59983'),(71,'04','2018-02-19 15:56:33','56754'),(72,'04','2018-12-24 13:14:33','54684'),(73,'02','2018-05-15 23:08:16','66531'),(74,'02','2018-06-25 19:08:32','25407'),(75,'02','2018-11-25 18:53:14','96119'),(76,'01','2018-05-28 02:43:26','30991'),(77,'04','2018-03-18 02:03:29','81845'),(78,'02','2018-02-18 20:23:03','1057'),(79,'01','2018-11-15 16:32:26','52151'),(80,'01','2018-07-25 19:54:37','91069'),(81,'04','2018-04-18 14:02:13','51134'),(82,'02','2018-06-04 07:27:50','4609'),(83,'04','2018-07-07 20:22:38','62669'),(84,'02','2018-08-13 10:40:36','93161'),(85,'03','2018-03-29 12:51:47','75717'),(86,'04','2018-04-03 15:35:12','67327'),(87,'04','2018-02-06 15:56:58','87785'),(88,'01','2018-12-16 04:38:52','34503'),(89,'03','2018-07-25 02:52:09','87144'),(90,'04','2018-08-21 21:48:58','37756'),(91,'01','2018-07-25 20:38:00','5070'),(92,'01','2018-05-23 17:56:31','40138'),(93,'01','2018-01-10 22:47:36','35626'),(94,'01','2018-03-20 22:01:19','94446'),(95,'01','2018-05-20 23:55:32','65411'),(96,'02','2018-08-06 07:52:03','48145'),(97,'02','2018-05-26 22:38:07','17564'),(98,'01','2018-05-31 21:40:52','59694'),(99,'03','2018-03-03 16:47:55','18672'),(100,'01','2018-04-29 06:15:51','52622');

ปกติ(เรา)เขียน SQL เพื่อสรุปค่าใช้จ่ายแยกตามประเภทและรายเดือนตามตารางรูปขวา

SELECT
    CategoryName, 
    SUM(IF(M = 1, TotalAmount, 0 )) AS '1',
    SUM(IF(M = 2, TotalAmount, 0 )) AS '2',
    SUM(IF(M = 3, TotalAmount, 0 )) AS '3',
    SUM(IF(M = 4, TotalAmount, 0 )) AS '4',
    SUM(IF(M = 5, TotalAmount, 0 )) AS '5',
    SUM(IF(M = 6, TotalAmount, 0 )) AS '6',
    SUM(IF(M = 7, TotalAmount, 0 )) AS '7',
    SUM(IF(M = 8, TotalAmount, 0 )) AS '8',
    SUM(IF(M = 9, TotalAmount, 0 )) AS '9',
    SUM(IF(M = 10, TotalAmount, 0 )) AS '10',
    SUM(IF(M = 11, TotalAmount, 0 )) AS '11',
    SUM(IF(M = 12, TotalAmount, 0 )) AS '12',
    SUM(TotalAmount) AS Total
FROM (
    SELECT
        MONTH(Expense.ExpenseDate) AS M,
        MONTHNAME(Expense.ExpenseDate) AS MN,
        Expense.CategoryId, 
        ExpenseCategory.CategoryName, 
        SUM(Amount) AS TotalAmount
    FROM Expense INNER JOIN ExpenseCategory ON Expense.CategoryId = ExpenseCategory.CategoryId
    GROUP BY Expense.CategoryId, MONTH(Expense.ExpenseDate)
) T 
GROUP BY T.CategoryId 
ORDER BY T.CategoryId ASC;

ผลลัพธ์ก็จะประมาณนี้

  • แถวเป็นประเภทค่าใช้จ่าย
  • คอลัมภ์เป็นยอดรวมรายเดือน

แบบนี้บ่อย ๆ ก็เหนื่อยเหมือนกันนะ  (อาจมีคนเขียน SQL สั้นและง่ายกว่านี้แหล่ะ  ก็ช่วยแนะนำด้วยหล่ะกันครับ) มันต้องมีคนเคยเหนื่อยเหมือนเรานะ และก็มีจริงด้วย คุณ Rick James เค้าก็ทำ Function เพื่อทำ Pivot Table ไว้เหมือนกัน (Blog นี้แนะนำให้อ่านเลย)

DELIMITER //
DROP   PROCEDURE IF EXISTS PIVOTTABLE //
CREATE PROCEDURE PIVOTTABLE(
    IN tbl_name VARCHAR(99),       -- table name (or db.tbl)
    IN base_cols VARCHAR(99),      -- column(s) on the left, separated by commas
    IN pivot_col VARCHAR(64),      -- name of column to put across the top
    IN tally_col VARCHAR(64),      -- name of column to SUM up
    IN where_clause VARCHAR(99),   -- empty string or "WHERE ..."
    IN order_by VARCHAR(99)        -- empty string or "ORDER BY ..."; usually the base_cols
    )
    DETERMINISTIC
    SQL SECURITY INVOKER
BEGIN
    -- Find the distinct values
    -- Build the SUM()s
    SET @subq = CONCAT('SELECT DISTINCT ', pivot_col, ' AS val ',
                    ' FROM ', tbl_name, ' ', where_clause, ' ORDER BY 1');
    -- select @subq;

    SET @cc1 = "CONCAT('SUM(IF(&p = ', &v, ', &t, 0)) AS ', &v)";
    SET @cc2 = REPLACE(@cc1, '&p', pivot_col);
    SET @cc3 = REPLACE(@cc2, '&t', tally_col);
    -- select @cc2, @cc3;
    SET @qval = CONCAT("'\"', val, '\"'");
    -- select @qval;
    SET @cc4 = REPLACE(@cc3, '&v', @qval);
    -- select @cc4;

    SET SESSION group_concat_max_len = 10000;   -- just in case
    SET @stmt = CONCAT(
            'SELECT  GROUP_CONCAT(', @cc4, ' SEPARATOR ",\n")  INTO @sums',
            ' FROM ( ', @subq, ' ) AS top');
     select @stmt;
    PREPARE _sql FROM @stmt;
    EXECUTE _sql;                      -- Intermediate step: build SQL for columns
    DEALLOCATE PREPARE _sql;
    -- Construct the query and perform it
    SET @stmt2 = CONCAT(
            'SELECT ',
                base_cols, ',\n',
                @sums,
                ',\n SUM(', tally_col, ') AS Total'
            '\n FROM ', tbl_name, ' ',
            where_clause,
            ' GROUP BY ', base_cols,
            '\n WITH ROLLUP',
            '\n', order_by
        );
    select @stmt2;                    -- The statement that generates the result
    PREPARE _sql FROM @stmt2;
    EXECUTE _sql;                     -- The resulting pivot table ouput
    DEALLOCATE PREPARE _sql;
    -- For debugging / tweaking, SELECT the various @variables after CALLing.
END;
//
DELIMITER ;

วิธีเรียกใช้งานก็ง่าย ๆ เลยแบบนี้

CALL PIVOTTABLE('TEMP', 'CategoryName', 'M', 'TotalAmount', '', ' ORDER BY CategoryId');

ตาราง TEMP ก็สร้างขึ้นมาจากสรุป (ตารางฝั่งซ้ายจากรูปแรก)

DROP TABLE IF EXISTS TEMP;
CREATE TABLE TEMP (
    SELECT
        MONTH(Expense.ExpenseDate) AS M,
        MONTHNAME(Expense.ExpenseDate) AS MN,
        Expense.CategoryId AS CategoryId, 
        ExpenseCategory.CategoryName AS CategoryName, 
        SUM(Amount) AS TotalAmount
    FROM Expense INNER JOIN ExpenseCategory ON Expense.CategoryId = ExpenseCategory.CategoryId
    GROUP BY Expense.CategoryId, MONTH(Expense.ExpenseDate)
);

SELECT * FROM TEMP;

หลังจากเรียกฟังก์ชั่นเสร็จผลลัพธ์ก็เหมือนที่เราเขียนเองนั่นแหล่ะ ^_^ แต่ลดแรงงานได้อีก

CALL PIVOTTABLE('TEMP', 'CategoryName', 'M', 'TotalAmount', '', ' ORDER BY CategoryId');

ป.ล.

การคำนวณระยะห่างระหว่างจุดสองจุด (Latitude/Longitude points) ใน MySQL

วันนี้เขียนบันทึกสั้น ๆ หล่ะกัน  ^_^

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

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

อย่ากระนั้นเลยบางทีสิ่งที่เราต้องการก็รอโปรแกรมหรือทูลมาทำงานได้ไม่ทันการณ์ ตัวอย่างเช่น “ขอรายชื่อ/บ้านของชาวบ้านที่อยู่ห่างจากบ้านที่พบผู้ป่วยไข้เลือดออกรัศมี 100 เมตร” แล้วสิ่งที่เรามี (พื้นฐาน) หล่ะมีอะไรบ้าง
– ข้อมูลพิกัดจาก HIS (Latitude/Longitude) มีเก็บ
– ทางเลือก โปรแกรม/ทูลก็ดีไปคลิก ๆ ค้นหา (มีเว็บ gisjhcis ให้ใช้นะครับ ตัวนี้ก็ใช้บริการ/API แผนที่จาก Google อีกต่อนึง) หรือคำนวณจากพิกัดที่เรามี
เรามันสายฮาร์ดคอร์อยู่แล้ว ทำเรื่องง่ายให้เป็นเรื่องยากสิ 5555 งั้นก็เริ่มกันเลย

  1. สร้างฟังก์ชั่นสำหรับการคำนวณระยะห่างจุดสองจุด (Latitude/Longitude points) ใน MySQL โดยใช้ Haversine formula รายละเอียดก็ไปอ่านต่อได้เลย
    DELIMITER $$
    DROP FUNCTION IF EXISTS haversine $$
    CREATE FUNCTION haversine (
            lat1 FLOAT, lon1 FLOAT,
            lat2 FLOAT, lon2 FLOAT
    ) RETURNS FLOAT
    NO SQL DETERMINISTIC
    BEGIN
        DECLARE r FLOAT unsigned DEFAULT 6372.8;
        DECLARE dLat FLOAT unsigned;
        DECLARE dLon FLOAT unsigned;
        DECLARE a FLOAT unsigned;
        DECLARE c FLOAT unsigned;
     
        SET dLat = RADIANS(lat2 - lat1);
        SET dLon = RADIANS(lon2 - lon1);
        SET lat1 = RADIANS(lat1);
        SET lat2 = RADIANS(lat2);
     
        SET a = POW(SIN(dLat / 2), 2) + COS(lat1) * COS(lat2) * POW(SIN(dLon / 2), 2);
        SET c = 2 * ASIN(SQRT(a));
        
        SET r = IF(htype = 'km', r, 3959);
        
        RETURN (r * c);
    END$$
     
    DELIMITER ;
  2. ลองทดสอบเรียกใช้งานฟังก์ชั่นดูจากพิกัดที่เรามี
    -- Ex
    SET @origin_lat0=15.829187;
    SET @origin_long0=105.262752;
    SET @origin_lat=15.828063;
    SET @origin_long=105.380678;
    
    SELECT ROUND(haversine(@origin_lat0, @origin_long0, @origin_lat, @origin_long), 2) AS distance_km;
    +-------------+
    | distance_km |
    +-------------+
    |       12.62 |
    +-------------+
    1 row in set (0.00 sec)
    
  3. ตรวจสอบว่าคลาดเคลื่อนมากน้อยแค่ไหน
  4. ทีนี้เราก็สามารถใช้ข้อมูลพิกัดจากตาราง house (ในที่นี้ใช้ HIS เป็น JHCIS ของกระทรวงฯ นะครับ) ได้เลย
  5. จบปิ๊งงงง ^_^

เพิ่มเติม
Fastest Way to Find Distance Between Two Lat/Long Points
Haversine formula
GISFORJHCIS

ป.ล.
เอาไปคำนวณระยะห่างระหว่างเราน่าจะไม่ได้

 

ปรับปรุงข้อมูลวันหยุดราชการ (Holiday) กัน

สำหรับประชาชนคนไทยแล้วคำว่า “ในเวลาราชการ” นี่น่าจะคุ้นเคยกันเป็นอย่างดี เรามักเห็นคำนี้ติดตามหน่วยงานราชการ แล้วคำว่าในเวลาราชการนี่มันยังไง เอาแบบกำปั้นทุบดิน ก็คือวันที่ไม่ใช่วันหยุดราชการนั่นแหล่ะ 😛

แล้ววันหยุดราชการนี่เราก็เริ่มยึดตาม “ประกาศกำหนดวันหยุดราชการนักขัตฤกษ์ พระพุทธศักราช ๒๔๕๖” ซึ่งพระบาทสมเด็จพระมงกุฎเกล้าเจ้าอยู่หัว รัชกาลที่ ๖ ทรงออกประกาศดังกล่าว ถ้าใครจะศึกษารายละเอียดบล็อกนี้ก็อธิบายไว้เป็นอย่างดี

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

และอีกเว็บไซต์ก็ myhora ซึ่งมีบริการ iCal บริการ

จะเห็นได้ว่ามีไฟล์ CSV (Comma-separated values) ให้เราได้ใช้ด้วย งั้นเราก็มาปรับปรุงวันหยุดราชการด้วยไฟล์ตัวนี้กัน

  1. เริ่มต้นก็ดาวน์โหลดมาซะ
    wget -O th_holiday.csv http://www.myhora.com/calendar/ical/holiday.aspx?2561.csv

    หน้าตาข้อมูลไฟล์คร่าว ๆ ก็ประมาณนี้

  2. นำเข้าข้อมูลข้างต้นลงในฐานข้อมูลของเราด้วยคำสั่ง LOAD DATA INFILE (สร้างตารางชั่วคราว th_holiday ไว้รอรับด้วย )
    DROP TABLE IF EXISTS th_holiday;
    CREATE TABLE IF NOT EXISTS th_holiday(
        Subject       	VARCHAR(255) NULL
        ,Start_Date    	DATE NOT NULL
        ,Start_Time    	TIME NULL
        ,End_Date      	DATE NULL
        ,End_Time      	TIME NULL
        ,All_day_event 	VARCHAR(4) NOT NULL
        ,Description   	VARCHAR(255) NOT NULL
        ,Show_time_as  	INTEGER  NOT NULL
        ,Location      	VARCHAR(30)
    );
    
    TRUNCATE th_holiday;
    LOAD DATA LOCAL INFILE 'th_holiday.csv' 
    INTO TABLE th_holiday
    FIELDS TERMINATED BY ',' 
    ENCLOSED BY '"'
    ESCAPED BY '"'
    LINES TERMINATED BY '\r\n'
    (`Subject`, @start_date, `Start_Time`, `End_Date`, `End_Time`, `All_day_event`, `Description`, `Show_time_as`, `Location`)
    SET start_date = STR_TO_DATE(@start_date, '%d/%m/%Y');
  3. เป็นอันเสร็จสิ้น ทีนี้ก็นำข้อมูลนี้ไปปรับปรุงในตารางของแต่ละ HIS

 

 

Automatically backup MySQL database to Google Drive

ประเด็นหลัก ๆ ของบล็อกนี้ก็เรื่องของการสำรองฐานข้อมูลนั่นแหล่ะ ต่างกันนิดหน่อยกับคำว่าตัวสำรอง เราสำรองเพราะเห็นว่าสิ่งนั้นสำคัญแต่ตัวสำรองคือยังไม่สำคัญนะให้รอ ก็ได้แค่นั้นแค่ตัวสำรอง

เดี๋ยวลองไล่ลำดับ ขั้นตอนในกระบวนการนี้ด้วยกัน

  1. เริ่มสำรองข้อมูล ในที่นี้คือฐานข้อมูลของ MySQL ด้วย Tool มาตรฐานคือ mysqldump (ส่วนเรื่องพารามิเตอร์ในคำสั่ง อะไรยังไง จะเอาอะไร ไม่เอาอะไร อ่านต่อได้ที่นี่)
  2. บีบอัดไฟล์ให้ขนาดเล็กลง
  3. เก็บไฟล์ไว้ที่เครื่องโลคอลนิดหน่อยอีก 30 วันค่อยมาลบหล่ะกัน (ทั้งนี้ทั้งนั้นก็ขึ้นกับความต้องการด้วยนะว่าเหมาะสมแค่ไหน)
  4. ทำการอัพโหลดข้อมูลที่สำรองได้ขึ้น Google Drive ผ่าน Google Drive CLI Client ตัวนึงที่ชื่อว่า gdrive (จุดประสงค์หลักคือเพื่อให้มีแหล่งเก็บอื่นเพิ่ม) ก่อนการใช้งานจำเป็นต้องอนุญาตให้ gdrive สามารถเข้าถึง Google Drive ของเราได้ก่อน
    1. อนุญาตสิจ๊ะ
    2. คัดลอก Token ที่ได้ เพื่อมาใช้ในโปรแกรม
  5. ทั้งหมดทั้งมวลให้ทำงานอัตโนมัติ โดยการใช้ crontab (สงสัยวิธีใช้งานก็ตามอ่านกันที่นี่นะ #เหมือนจะขี้เกียจ #55555)

โดยขั้นตอนข้างต้นเราเขียนเชลล์สคริปท์สั้น ๆ สำหรับทำงานแบบรูทีนตามนี้

ผลลัพธ์ที่ได้ก็ตามนี้

ป.ล. 1
การสำรองข้อมูลเป็นเรื่องที่สำคัญพอ ๆ กับการทดสอบไฟล์ที่สำรองได้นั้นสามารถใช้งานได้หรือไม่

ป.ล 2
gdrive – gdrive is a command line utility for interacting with Google Drive.
pv – monitor the progress of data through a pipe.

 

การหาปีงบประมาณแบบไทยสำหรับ MySQL

ปีงบประมาณ หรือ ปีงบการเงิน (อังกฤษ: fiscal year, financial year หรือ budget year) เป็นช่วงเวลาที่ใช้สำหรับคำนวณงบการเงินประจำปีในธุรกิจและองค์กรอื่น ๆ ในหลายเขตอำนาจตามกฎหมาย กฎข้อบังคับเกี่ยวกับการบัญชีและการเก็บภาษี จะต้องอาศัยรายงานเหล่านี้อย่างน้อยครั้งหนึ่งต่อสิบสองเดือน แต่ไม่จำเป็นว่าช่วงเวลาที่รายงานนี้จะต้องตรงกับปีตามปฏิทิน ซึงปีงบประมาณของไทยเริ่มตั้งแต่เดือนตุลาคมถึงเดือนกันยายนของปีถัดไป

เราสามารถเขียนฟังก์ชั่น (Function) เพื่อคำนวณหาปีงบประมาณจากฐานข้อมูล MySQL ตามนี้

DELIMITER $$

DROP FUNCTION IF EXISTS THGOVYEAR$$
CREATE FUNCTION THGOVYEAR(
    xdate DATETIME,
    th BIT)
RETURNS INT 
DETERMINISTIC
BEGIN
    RETURN IF(MONTH(xdate) < 10, YEAR(xdate), YEAR(DATE_ADD(xdate, INTERVAL 1 YEAR))) + (543*th);
END$$

DELIMITER ;

โดยตัวแปรที่รับในฟังก์ชั่นเองจะมี 2 ตัว คือ

  • xdate : วันที่ที่ต้องการคำนวณ
  • th : ต้องการแสดงผลแบบปี พ.ศ หรือไม่ (1 = ใช่, 0 = ไม่)

ที่นี้มาลองทดสอบดู โดยการสร้างตารางเก็บวันที่กัน

DROP TABLE IF EXISTS DemoDate;
CREATE TABLE DemoDate (
    fielddate DATETIME NOT NULL
);
INSERT INTO DemoDate (fielddate) VALUES
    ('2008-09-01 00:00:00'),
    ('2008-04-04 00:00:00'),
    ('2009-05-25 00:00:00'),
    ('2010-06-30 00:00:00'),
    ('2011-07-01 00:00:00'), 
    ('2010-10-01 00:00:00'),
    ('2011-10-01 00:00:00'), 
    ('2012-09-30 00:00:00'), 
    ('2012-10-01 00:00:00'),
    ('2013-09-30 00:00:00'),
    ('2012-10-01 00:00:00'),
    ('2013-09-30 00:00:00'),
    ('2013-10-01 00:00:00'),
    ('2014-09-30 00:00:00'),
    ('2014-10-01 00:00:00'),
    ('2015-09-30 00:00:00'),
    ('2015-10-01 00:00:00'),
    ('2016-09-30 00:00:00'),
    ('2016-10-01 00:00:00'),
    (NOW()),
    ('0000-00-00 00:00:00');

แล้วก็มาทดสอบกัน

ขอให้มีความสุขกับการทำงานกัน แด่วันแรงานนนนนนน

**ช่วงนี้ไม่ค่อยได้เขียนบล็อคเลย แต่เขียน ๆ หน่อยเหอะ เดือนละเรื่องก็ยังดี T_T …
**งานช่วงนี้เหรอ ถามว่าท้อไหมตอบเลยว่ามากกกกก (แต่ไม่เคยถอยนะ หันหลังแม่มมเลย 555)

ไตรมาสใน MySQL แบบปีงบประมาณของไทย

ไตรมาส เป็นช่วงระยะเวลาสามเดือน ซึ่งแบ่งช่วงเวลา หนึ่งปี ออกเป็น สี่ ไตรมาส ในเชิงธุรกิจการพิจารณาผลประกอบการนิยมใช้ช่วงเวลาไตรมาส ในการประเมินผล กรณีที่เป็นตามปีปฏิทินจะแบ่งได้

  • ไตรมาสที่ 1 หมายถึงช่วงเดือนมกราคมถึงมีนาคม
  • ไตรมาสที่ 2 หมายถึงช่วงเดือนเมษายนถึงมิถุนายน
  • ไตรมาสที่ 3 หมายถึงช่วงเดือนกรกฎาคมถึงกันยายน
  • ไตรมาสที่ 4 หมายถึงช่วงเดือนตุลาคมถึงธันวาคม

แต่ระบบราชการของไทย ระบบรายงานส่วนใหญ่ขึ้นกับปีงบประมาณ ^_^

ปีงบประมาณ หรือ ปีงบการเงิน (อังกฤษ: fiscal year, financial year หรือ budget year) เป็นช่วงเวลาที่ใช้สำหรับคำนวณงบการเงินประจำปีในธุรกิจและองค์กรอื่น ๆ ในหลายเขตอำนาจตามกฎหมาย กฎข้อบังคับเกี่ยวกับการบัญชีและการเก็บภาษี จะต้องอาศัยรายงานเหล่านี้อย่างน้อยครั้งหนึ่งต่อสิบสองเดือน แต่ไม่จำเป็นว่าช่วงเวลาที่รายงานนี้จะต้องตรงกับปีตามปฏิทิน ซึงปีงบประมาณของไทยเริ่มตั้งแต่เดือนตุลาคมถึงเดือนกันยายนของปีถัดไป

ดังนั้นระบบไตรมาสจึงแบ่งออกเป็น

  • ไตรมาสที่ 1 หมายถึงช่วงเดือนตุลาคมถึงธันวาคม
  • ไตรมาสที่ 2 หมายถึงช่วงเดือนมกราคมถึงมีนาคม
  • ไตรมาสที่ 3 หมายถึงช่วงเดือนเมษายนถึงมิถุนายน
  • ไตรมาสที่ 4 หมายถึงช่วงเดือนกรกฎาคมถึงกันยายน

ซึ่งใน MySQL เองก็มีฟังก์ชั่นที่คำนวณหาไตรมาสชื่อ QUARTER อยู่แล้วซึ่งอิงจากปีปฏิทิน

QUARTER(date) : Returns the quarter of the year for date, in the range 1 to 4.

mysql> SELECT QUARTER('2016-10-18');
+-----------------------+
| QUARTER('2016-10-18') |
+-----------------------+
|                     4 | 
+-----------------------+

ผลอิงจากปีปฏิทินก็ตามนั้น ^_^ อ้าวว งั้นเรามาสร้างฟังก์ชั่นหาไตรมาสจากปีงบประมาณกัน

CREATE DEFINER=`root`@`localhost` FUNCTION `THQUARTER`(
    xdate DATETIME,
    th BIT) RETURNS int(11)
DETERMINISTIC
BEGIN
    DECLARE qt INT;
    
    SET qt = QUARTER(xdate) + th;
    
    RETURN IF(qt > 4, qt - 4, qt);
END

-- Test
mysql> SELECT THQUARTER('2016-10-18');
+-----------------------+
| THQUARTER('2016-10-18') |
+-----------------------+
|                     1 | 
+-----------------------+

 

มาลองเรียกใช้งานกัน สมมุติโจทย์ต้องการหารายงานการรับบริการของผู้ป่วยในสถานบริการแยกรายไตรมาส (ครั้ง)

-- Generate dummy data
CREATE TABLE `visit` (
  `visitno` mediumint(8) unsigned NOT NULL auto_increment,
  `visitdate` varchar(255),
  `pid` varchar(255) default NULL,
  `pcucode` varchar(255) default NULL,
  PRIMARY KEY (`visitno`)
) AUTO_INCREMENT=1;

INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-01-09","5","yellow"),("2017-06-11","9","orange"),("2016-11-28","9","yellow"),("2017-06-02","2","red"),("2017-01-09","10","orange"),("2017-05-03","10","orange"),("2017-07-06","8","red"),("2017-06-25","10","yellow"),("2017-04-19","3","orange"),("2017-06-12","3","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2016-11-12","10","orange"),("2017-06-18","5","red"),("2017-05-20","9","yellow"),("2017-04-01","2","red"),("2016-12-31","6","red"),("2017-02-05","8","orange"),("2017-02-19","3","red"),("2017-03-13","5","red"),("2017-03-27","3","yellow"),("2017-06-22","8","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-01-17","6","red"),("2016-10-15","5","yellow"),("2017-05-30","8","orange"),("2017-04-29","5","red"),("2017-01-02","3","orange"),("2017-09-04","4","red"),("2017-01-09","3","orange"),("2016-12-15","7","yellow"),("2017-01-31","8","orange"),("2016-11-08","6","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-01-10","5","orange"),("2016-11-20","3","yellow"),("2017-07-09","8","red"),("2017-01-20","8","red"),("2017-05-17","8","red"),("2017-08-05","5","yellow"),("2017-07-19","7","red"),("2017-05-06","10","yellow"),("2017-06-01","6","orange"),("2016-11-06","9","red");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-09-20","5","orange"),("2017-01-28","8","yellow"),("2017-02-20","7","red"),("2016-11-24","5","orange"),("2016-12-27","5","orange"),("2017-05-20","6","yellow"),("2017-07-03","1","red"),("2017-09-09","8","yellow"),("2017-06-04","9","red"),("2017-03-04","9","orange");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-09-12","6","orange"),("2017-07-24","1","yellow"),("2017-02-27","5","red"),("2017-08-21","3","yellow"),("2017-01-26","2","orange"),("2017-05-08","8","orange"),("2016-12-26","2","red"),("2017-06-24","7","red"),("2017-02-20","6","red"),("2017-04-18","10","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-03-01","6","yellow"),("2017-07-15","1","yellow"),("2016-11-11","4","yellow"),("2017-06-27","10","orange"),("2017-01-02","10","orange"),("2017-05-08","7","orange"),("2017-03-16","2","red"),("2017-02-27","4","orange"),("2017-02-14","4","orange"),("2017-04-23","9","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2016-12-02","1","orange"),("2017-04-15","9","yellow"),("2017-01-25","6","yellow"),("2017-05-06","7","yellow"),("2017-07-31","8","orange"),("2017-07-23","4","yellow"),("2017-06-02","7","red"),("2017-02-06","7","yellow"),("2017-02-14","6","red"),("2017-07-27","3","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2016-10-21","1","red"),("2017-01-13","5","red"),("2017-06-06","1","red"),("2017-01-16","7","yellow"),("2017-01-29","9","orange"),("2016-11-06","7","red"),("2017-01-06","2","red"),("2017-01-12","6","yellow"),("2017-06-29","5","red"),("2017-04-04","10","yellow");
INSERT INTO `visit` (`visitdate`,`pid`,`pcucode`) VALUES ("2017-03-11","10","red"),("2017-05-22","2","red"),("2017-03-21","2","yellow"),("2017-02-19","3","orange"),("2016-11-30","3","red"),("2017-06-30","2","yellow"),("2017-09-17","2","red"),("2017-04-21","7","orange"),("2017-02-12","1","yellow"),("2017-05-12","7","red");

ผลลัพธ์

mysql> SELECT
    ->     pcucode,
    ->     SUM(IF(THQUARTER(visitdate, 1) = 1, 1, 0)) AS Q1,
    ->     SUM(IF(THQUARTER(visitdate, 1) = 2, 1, 0)) AS Q2,
    ->     SUM(IF(THQUARTER(visitdate, 1) = 3, 1, 0)) AS Q3,
    ->     SUM(IF(THQUARTER(visitdate, 1) = 4, 1, 0)) AS Q4,
    ->     COUNT(visitno) AS Total
    -> FROM visit
    -> GROUP BY pcucode
    -> WITH ROLLUP;
+---------+------+------+------+------+-------+
| pcucode | Q1   | Q2   | Q3   | Q4   | Total |
+---------+------+------+------+------+-------+
| orange  |    4 |   13 |    9 |    3 |    29 |
| red     |    6 |   12 |   12 |    6 |    36 |
| yellow  |    6 |   10 |   12 |    7 |    35 |
| NULL    |   16 |   35 |   33 |   16 |   100 |
+---------+------+------+------+------+-------+
4 rows in set (0.02 sec)

ปั่นรายงานกันต่อ T_T