סיבוב טבלה על ציר

בעיה נפוצה שלא היה לה פתרון פשוט על ידי פקודה עד גרסה 11 היא סיבוב טבלה על ציר. בגרסה 11 ישנו פתרון יעיל ומהיר לבעיה על ידי צמד פקודות: Pivot ו-Unpivot שלא מצריך ניפנופי ידיים והסברים על מימוש שליפה שתבצע את זה.

כדי שהעניין יהיה ברור, אני אציג דוגמה שתסביר מה זאת אומרת סיבוב טבלה על ציר ואציג שני פתרונות שהיו בשימוש לפני גרסה 11. הפתרון הראשון הוא באמצעות SQL פשוט שיכול במקרים מסויימים להפוך למפלצת של ביצועים מבחינת פעילות IO. הפתרון השני הוא פתרון מורכב יחסית שמשתמש בפונקציה האנליטית המתוסבכת אך שימושית model שחוסך בפעולות IO אבל במקרים של טבלאות גדולות הוא הופך לצרכן זכרון משמעותי.

כמובן שסיבוב על ציר הוא סימטרי וניתן להפוך שורות לעמודות (שזה יחסית פשוט) ועמודות לשורות (שזה כבר יותר מסובך). בוא נראה איך פותרים את כל הבלאגן הזה…

לצורך הדוגמה, נניח שיש לנו טבלה השומרת לכל עובד את נתוני המכירה שלו למוצר כאשר יש לנו בחברה שלושה מוצרים שונים.
הטבלה שלנו תיראה ככה:

EMP_NAME             PROD_NAME            AMOUNT
-------------------- -------------------- ----------------------
emp1                 prod1                11
emp1                 prod2                22
emp1                 prod3                33
emp2                 prod1                111
emp2                 prod2                222
emp2                 prod3                333
emp3                 prod1                1111
emp3                 prod2                2222
emp3                 prod3                3333

PIVOT

כדי להסביר מה זה PIVOT, נניח שאנחנו רוצים לראות את הנתונים ברמת העובד כלומר לכל עובד תהיה שורה נפרדת ובה יהיו לו שלוש עמודות עם סך המכירות של כל אחד מהמוצרים.

כאמור, עד לגרסה 11 היו לנו כמה פתרונות שהעיקרי שבינהם היה שימוש ב-case כדי להזיז את הנתונים מרמת שורה לעמודה:

select emp_name,
sum(case when prod_name ='prod1' then
                        amount else null end) prod1,
sum(case when prod_name ='prod2' then
                        amount else null end) prod2,
sum(case when prod_name ='prod3' then
                        amount else null end) prod3
 from zelkayam2.test_model_pivot
group by emp_name;

פתרון אחר ומסובך לכתיבה והבנה אך מהיר יותר בביצועים היה שימוש בפקודת model:

select emp_name, prod1_sales_sum, prod2_sales_sum, prod3_sales_sum
from zelkayam2.test_model_pivot
model return updated rows
dimension by (emp_name, prod_name)
measures ( 0 prod1_sales_sum,
           0 prod2_sales_sum,
           0 prod3_sales_sum,
           AMOUNT)
rules upsert all
          (
            prod1_sales_sum[any,'na'] = amount[cv(),'prod1'],
            prod2_sales_sum[any,'na'] = amount[cv(),'prod2'],
            prod3_sales_sum[any,'na'] = amount[cv(),'prod3']
          );

החל מגרסה 11 אנו יכולים להשתמש בפקודה החדשה PIVOT כדי לבצע את אותה פונקציונליות בצורה פשוטה (וקצרה) בהרבה:

SELECT *
  FROM zelkayam2.test_model_pivot
  PIVOT (SUM (amount)
             FOR prod_name IN  ('prod1' AS prod1_sales_sum,
                                            'prod2' AS prod2_sales_sum,
                                            'prod3' AS prod3_sales_sum));

----------------------------------------------------------------------------------------
| Id  | Operation           | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                  |     9 |   333 |     4  (25)| 00:00:01 |
|   1 |  HASH GROUP BY PIVOT|                  |     9 |   333 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL | TEST_MODEL_PIVOT |     9 |   333 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

שורה 3 מגדירה מה יהיה הערך שאותו נשים בעמודות.
שורה 4 מגדירה לפי איזה עמודה נבצע את הפירוק לעמודות.
שורות 4-6 מגדירות את הערכים שלפיהם נשים את הערכים בעמודות החדשות.

כמובן ששלושת המקרים מחזירים את אותה התוצאה:

EMP_NAME             PROD1_SALES_SUM        PROD2_SALES_SUM        PROD3_SALES_SUM
-------------------- ---------------------- ---------------------- ----------------------
emp3                 1111                   2222                   3333
emp1                 11                     22                     33
emp2                 111                    222                    333

UNPIVOT

כל זה טוב ויפה אבל מה קורה כשיש לנו את הבעיה ההפוכה: יש לנו טבלה עם מספר עובדים ושלוש עמודות שאותן אנחנו רוצים להפוך לשורות? במקרה כזה נראה שנצטרך להכפיל את הטבלה כמספר העמודות כדי לשטח את הנתונים.

בגרסאות קודמות היינו סורקים את הטבלה שלוש פעמים:

SELECT *
FROM
  ( 
  SELECT emp_name, 'prod1' AS prod_name, prod1_sales_sum FROM zelkayam2.test_model_unpivot
  UNION ALL
  SELECT emp_name, 'prod2' AS prod_name, prod2_sales_sum FROM zelkayam2.test_model_unpivot
  UNION ALL
  SELECT emp_name, 'prod3' AS prod_name, prod3_sales_sum FROM zelkayam2.test_model_unpivot
  );

------------------------------------------------------------------------------------------
| Id  | Operation           | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                    |     9 |   288 |     9   (0)| 00:00:01 |
|   1 |  VIEW               |                    |     9 |   288 |     9   (0)| 00:00:01 |
|   2 |   UNION-ALL         |                    |       |       |            |          |
|   3 |    TABLE ACCESS FULL| TEST_MODEL_UNPIVOT |     3 |    75 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| TEST_MODEL_UNPIVOT |     3 |    75 |     3   (0)| 00:00:01 |
|   5 |    TABLE ACCESS FULL| TEST_MODEL_UNPIVOT |     3 |    75 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

או משתמשים במכפלה קרטזית כדי לחסוך סריקות (אבל לא זכרון):

select emp_name,
case 
  when b.lv = 1 then 'prod1'
  when b.lv = 2 then 'prod2'
  when b.lv = 3 then 'prod3' end prod_name,
case 
  when b.lv = 1 then a.prod1_sales_sum 
  when b.lv = 2 then a.prod2_sales_sum 
  when b.lv = 3 then a.prod3_sales_sum end prod_sales_sum
from zelkayam2.test_model_unpivot a, (select level lv from dual connect by level <= 3) b;

-----------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                    |     3 |   192 |     5   (0)| 00:00:01 |
|   1 |  MERGE JOIN CARTESIAN          |                    |     3 |   192 |     5   (0)| 00:00:01 |
|   2 |   VIEW                         |                    |     1 |    13 |     2   (0)| 00:00:01 |
|*  3 |    CONNECT BY WITHOUT FILTERING|                    |       |       |            |          |
|   4 |     FAST DUAL                  |                    |     1 |       |     2   (0)| 00:00:01 |
|   5 |   BUFFER SORT                  |                    |     3 |   153 |     5   (0)| 00:00:01 |
|   6 |    TABLE ACCESS FULL           | TEST_MODEL_UNPIVOT |     3 |   153 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter(LEVEL<=3)


אפשרות אחרת הייתה להשתמש בפקודת model שתסרוק את הטבלה פעם אחת אבל עלולה לצרוך הרבה מאוד זכרון כדי לבנות את התוצאה:

select emp_name ,prod_name, amount
from zelkayam2.test_model_unpivot 
model 
return updated rows
dimension by (emp_name, 'prod_name' prod_name)
measures (prod1_sales_sum, prod2_sales_sum, prod3_sales_sum, 0 amount)
rules upsert all
(
    amount[any,'prod1'] = prod1_sales_sum[cv(),'prod_name'],
    amount[any,'prod2'] = prod2_sales_sum[cv(),'prod_name'],
    amount[any,'prod3'] = prod3_sales_sum[cv(),'prod_name']
);

-----------------------------------------------------------------------------------------
| Id  | Operation          | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                    |     3 |   153 |     3   (0)| 00:00:01 |
|   1 |  SQL MODEL ORDERED |                    |     3 |   153 |            |          |
|   2 |   TABLE ACCESS FULL| TEST_MODEL_UNPIVOT |     3 |   153 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

החל מגרסה 11 אנחנו יכולים להשתמש בפקודת unpivot הפשוטה בצורה הבאה:

SELECT *
  FROM zelkayam2.test_model_unpivot 
  UNPIVOT (amount 
                             FOR prod_name IN (prod1_sales_sum AS 'prod1',
							   prod2_sales_sum AS 'prod2',
							   prod3_sales_sum AS 'prod3'));

------------------------------------------------------------------------------------------
| Id  | Operation           | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                    |     9 |   288 |     9   (0)| 00:00:01 |
|*  1 |  VIEW               |                    |     9 |   288 |     9   (0)| 00:00:01 |
|   2 |   UNPIVOT           |                    |       |       |            |          |
|   3 |    TABLE ACCESS FULL| TEST_MODEL_UNPIVOT |     3 |   153 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("unpivot_view_006"."AMOUNT" IS NOT NULL)

שורה 3 מגדירה את העמודה החדשה שבה יהיו הנתונים (amount).
שורה 4 מגדירה את העמודה החדשה שבה יהיו הנתונים החותכים (prod_name).
שורות 4-6 מגדירות את הערכים שיחליפו כל אחת מהעמודות שמסתובבות על הציר.

בעזרת שימוש בפקודות האלה קל הרבה יותר לסובב טבלאות על ציר – הן מבחינת הכתיבה והן מבחינת הביצועים.
כרגיל, אם יש שאלות או הערות – אני אשמח לשמוע… 🙂

1 תגובה

Trackbacks & Pingbacks

  1. […] לרשומה המקורית בבלוג של זהר: סיבוב טבלה על ציר. […]

השאירו תגובה

Want to join the discussion?
Feel free to contribute!

השאר תגובה

אתר זה עושה שימוש באקיזמט למניעת הודעות זבל. לחצו כאן כדי ללמוד איך נתוני התגובה שלכם מעובדים.