oracle-databasepivotdynamic-pivot

How do I pivot date column as the first column?


I have a table using oracle SQL with hospital departments with daily input rows and this is the table columns

        select IDNO,
               DEPT_ID,
               SAMPLES_MORNING as SM,
               TESTS_MORNING   as TM,
               SAMPLES_EVENING as SE,
               TESTS_EVENING   as TE,
               sent_out        as SO,
               STAT_DATE,
               HOSPITAL_NO,
               USER_ID
          from DEPARTMENT_STATISTICS

I need to make statistics sheet get SUM of samples and tests for each hospital daily and using pivot function and the output I need like this (some columns omitted) :

STAT_DATE 01/06/2025 02/06/2025
HOSPITAL_NO 2 3 2 3
DEPT_ID SM TM SM TM SM TM SM TM
1 100 200 250 300 55 80 90 120
2 50 65 100 90 100 100 200 250
3 99 45 20 22 33 55 65 89

I tried this query and pivot:

    SELECT * FROM
    (
      SELECT dept_id , hospital_no,STAT_DATE, SAMPLES_MORNING , TESTS_MORNING , samples_evening , TESTS_EVENING , SENT_OUT FROM DEPARTMENT_STATISTICS
    )
    PIVOT
    (
      sum(SAMPLES_MORNING) as SM , sum(TESTS_MORNING) as TM, sum(samples_evening) AS SE , sum(TESTS_EVENING) as TE , SUM(SENT_OUT) AS OUT
      FOR hospital_no in (2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,21,22,41,61,81,107,108,109,110)
    );

but STAT_DATE show in the rows ,

how can I pivot STAT_DATE as the first column up the query result and up the HOSPITAL_NO also ?


Solution

  • Making one column from date and hospital

    The most straightforward method to me would have been to simply concatenate your pivoting columns:

    SELECT * FROM
    (
      SELECT
        dept_id,
        to_char(stat_date, 'DD/MM/YYYY')||' - '||hospital_no AS date_and_hospital,
        SAMPLES_MORNING , TESTS_MORNING , samples_evening , TESTS_EVENING , SENT_OUT
      FROM DEPARTMENT_STATISTICS
    )
    PIVOT
    (
      sum(SAMPLES_MORNING) as SM , sum(TESTS_MORNING) as TM, sum(samples_evening) AS SE , sum(TESTS_EVENING) as TE , SUM(SENT_OUT) AS OUT
      FOR date_and_hospital in
      (
        '01/06/2025 - 2',
        '01/06/2025 - 3',
        -- …,
        '02/06/2025 - 2',
        '02/06/2025 - 3'
        -- , …
      )
    );
    
    DEPT_ID '01/06/2025 - 2'_SM '01/06/2025 - 2'_TM '01/06/2025 - 2'_SE '01/06/2025 - 2'_TE '01/06/2025 - 2'_OUT '01/06/2025 - 3'_SM '01/06/2025 - 3'_TM '01/06/2025 - 3'_SE '01/06/2025 - 3'_TE '01/06/2025 - 3'_OUT '02/06/2025 - 2'_SM '02/06/2025 - 2'_TM '02/06/2025 - 2'_SE '02/06/2025 - 2'_TE '02/06/2025 - 2'_OUT '02/06/2025 - 3'_SM '02/06/2025 - 3'_TM '02/06/2025 - 3'_SE '02/06/2025 - 3'_TE '02/06/2025 - 3'_OUT
    1 100 200 0 0 0 250 300 0 0 0 55 80 0 0 0 90 120 0 0 0
    2 50 65 0 0 0 100 90 0 0 0 100 100 0 0 0 200 250 0 0 0
    3 99 45 0 0 0 20 22 0 0 0 33 55 0 0 0 65 89 0 0 0

    (as can be seen in the first SELECT of that fiddle)

    Clean multiple columns handling

    … But, as I discovered in the last article about Dynamic pivoting (see next §), even since Oracle 11 you can avoid concatenating your columns from the start;
    still you will have to enumerate all cases, like with the concatenation above:

    SELECT * FROM
    (
      SELECT dept_id , hospital_no,STAT_DATE, SAMPLES_MORNING , TESTS_MORNING , samples_evening , TESTS_EVENING , SENT_OUT FROM DEPARTMENT_STATISTICS
    )
    PIVOT
    (
      sum(SAMPLES_MORNING) as SM , sum(TESTS_MORNING) as TM, sum(samples_evening) AS SE , sum(TESTS_EVENING) as TE , SUM(SENT_OUT) AS OUT
      FOR (STAT_DATE, hospital_no) in
      (
        ('01/06/2025', 2) AS "01/06/2025 - 2",
        ('01/06/2025', 3) AS "01/06/2025 - 3",
        -- …,
        ('02/06/2025', 2) AS "02/06/2025 - 2",
        ('02/06/2025', 3) AS "02/06/2025 - 3"
        -- , …
      )
    );
    

    (second SELECT of that fiddle)

    Dynamic pivoting!

    Of course, given the complexity of the problem, you've probably considered a way of dynamic pivoting,
    which as seen in this SO question, this Oracle forum post, or with a more satisfying PL/SQL implementation in this article,
    will require some high-level language wrapping.

    In PL/SQL, your EXECUTE IMMEDIATE 'SELECT * FROM …' won't return rows, but you can EXECUTE IMMEDIATE 'CREATE TABLE report …'; EXECUTE IMMEDIATE 'INSERT INTO report SELECT * FROM …', then SELECT from this populated results table:

    DECLARE
      mysql varchar2(4000);
      cols varchar2(4000);
    BEGIN
      -- Create a table with the column names we will want.
      
      SELECT listagg('"'||STAT_DATE||' - '||hospital_no||' - '||type||'" int', ', ') WITHIN GROUP (ORDER BY STAT_DATE, hospital_no, colpos)
      INTO cols
      FROM
        (SELECT DISTINCT STAT_DATE, hospital_no FROM DEPARTMENT_STATISTICS) s,
        (SELECT 1 colpos, 'SM' type from dual union all select 2, 'TM' from dual union all select 3, 'SE' from dual union all select 4, 'TE' from dual union all select 5, 'OUT' from dual) types
      ;
      mysql := 'CREATE TABLE report (DEPT_ID int, '||cols||')';
      EXECUTE IMMEDIATE mysql;
      
      -- Create our PIVOT.
      SELECT listagg('('''||STAT_DATE||''', '||hospital_no||')', ', ') WITHIN GROUP (ORDER BY STAT_DATE, hospital_no)
      INTO cols
      FROM
        (SELECT DISTINCT STAT_DATE, hospital_no FROM DEPARTMENT_STATISTICS) s
      ;
      mysql :=
      '
        INSERT INTO report
        SELECT * FROM
        (
          SELECT dept_id , hospital_no,STAT_DATE, SAMPLES_MORNING , TESTS_MORNING , samples_evening , TESTS_EVENING , SENT_OUT FROM DEPARTMENT_STATISTICS
        )
        PIVOT
        (
          sum(SAMPLES_MORNING) as SM , sum(TESTS_MORNING) as TM, sum(samples_evening) AS SE , sum(TESTS_EVENING) as TE , SUM(SENT_OUT) AS OUT
          FOR (STAT_DATE, hospital_no) in
          (
            '||cols||'
          )
        )
      ';
      EXECUTE IMMEDIATE mysql;
    END;
    /
    SELECT * FROM report;
    

    With the same result (with pretty column names as we could name them in the CREATE TABLE):

    DEPT_ID 01/06/2025 - 2 - SM 01/06/2025 - 2 - TM 01/06/2025 - 2 - SE 01/06/2025 - 2 - TE 01/06/2025 - 2 - OUT 01/06/2025 - 3 - SM 01/06/2025 - 3 - TM 01/06/2025 - 3 - SE 01/06/2025 - 3 - TE 01/06/2025 - 3 - OUT 02/06/2025 - 2 - SM 02/06/2025 - 2 - TM 02/06/2025 - 2 - SE 02/06/2025 - 2 - TE 02/06/2025 - 2 - OUT 02/06/2025 - 3 - SM 02/06/2025 - 3 - TM 02/06/2025 - 3 - SE 02/06/2025 - 3 - TE 02/06/2025 - 3 - OUT
    1 100 200 0 0 0 250 300 0 0 0 55 80 0 0 0 90 120 0 0 0
    2 50 65 0 0 0 100 90 0 0 0 100 100 0 0 0 200 250 0 0 0
    3 99 45 0 0 0 20 22 0 0 0 33 55 0 0 0 65 89 0 0 0

    (and you've got it running as the last block of our fiddle)