Blog Post

WITH clause gets a 26ai boost!

,

By way of background, a while back I did video called “My New Favourite Join” on the LATERAL clause.

The reason I like it is that it lets you solve a problem by composing little pieces together rather than writing one enormous query.

Now this post is not about LATERAL, but I’ll show a quick LATERAL example to show how it leads to a cool new 26ai SQL enhancement. I’ll start with a simple example.


SQL> select d.deptno, d.dname
  2  from   dept d;

    DEPTNO DNAME
---------- --------------
        10 ACCOUNTING
        20 RESEARCH
        30 SALES
        40 OPERATIONS

4 rows selected.



Suppose we decide we also want the salary of the highest paid employee in each department. The traditional solution would be to build an aggregate query and join it back.


SQL> select d.deptno, d.dname, m.maxsal
  2  from   dept d,
  3    ( select deptno, max(sal) maxsal
  4      from   emp
  5      group by deptno
  6    ) m
  7  where m.deptno = d.deptno;

    DEPTNO DNAME              MAXSAL
---------- -------------- ----------
        10 ACCOUNTING           5000
        20 RESEARCH             3000
        30 SALES                2850

3 rows selected.



It’s not particular intuitive that this is how the problem should have been solved, but it’s not too hard to throw together. That is until someone says…

“Actually, I’d like the employee number as well.”

Now you have to ramp your SQL skills up a notch. We have to join the aggregate back to EMP, deal with ties, group again…


SQL> select d.deptno, d.dname, m.maxsal, max(e.empno)
  2  from   dept d,
  3    ( select deptno, max(sal) maxsal
  4      from   emp
  5      group by deptno
  6    ) m,
  7    emp e
  8  where m.deptno = d.deptno
  9  and e.sal = m.maxsal
 10  group by d.deptno, d.dname, m.maxsal;

    DEPTNO DNAME              MAXSAL MAX(E.EMPNO)
---------- -------------- ---------- ------------
        30 SALES                2850         7698
        20 RESEARCH             3000         7902
        10 ACCOUNTING           5000         7839

3 rows selected.



Nothing is technically bad with this approach (or solution) but it doesn’t really reflect the way humans typically tackle a coding problem. Typically we build a solution in components and knit them together. Repeating the example from that mindset, our approach would be something like.

“OK, I can get a list of departments easily enough…”


SQL> select d.deptno, d.dname
  2  from   dept d;

    DEPTNO DNAME
---------- --------------
        10 ACCOUNTING
        20 RESEARCH
        30 SALES
        40 OPERATIONS

4 rows selected.



“…and for a known department, I can easily write a SQL to get the employee details I need”


SQL> select e.empno, e.sal
  2  from   emp e
  3  where  e.deptno = 10
  4  order by sal desc
  5  fetch first 1 row only;

     EMPNO        SAL
---------- ----------
      7839       5000



LATERAL gives us a simple way to piece those two “standalone” queries together.


SQL> select d.deptno, d.dname, m.empno, m.sal
  2  from   dept d,
  3     lateral(
  4       select e.empno, e.sal
  5       from   emp e
  6       where  e.deptno = d.deptno
  7       order by sal desc
  8       fetch first 1 row only ) m;

    DEPTNO DNAME               EMPNO        SAL
---------- -------------- ---------- ----------
        10 ACCOUNTING           7839       5000
        20 RESEARCH             7788       3000
        30 SALES                7698       2850

3 rows selected.



I like this style because we’re using a “human” step-by-step process to still come up with a relational set-based solution. (It becomes the database engine’s job to convert to a nice standard join/aggregation if appropriate)

The WITH clause is similar

Using WITH has always encouraged the same style. Common Table Expressions have let use use modular SQL building blocks to compose a larger final SQL solution.

Instead of writing one giant statement, we give meaningful names to intermediate steps.



SQL> with avgsal as
  2   ( select deptno, avg(sal) asal
  3     from   emp
  4     group by deptno )
  5  select *
  6  from avgsal;

    DEPTNO       ASAL
---------- ----------
        20       2175
        30 1566.66667
        10 2916.66667



That’s much easier to read than burying everything inside nested inline views.

However, if I wanted to apply the same approach as LATERAL, ie, grab an existing query (that might contain a WITH) and then utilise in another SQL that contains a WITH, then I strike problems.



SQL> with my_outer_query as
  2  (
  3    with avgsal as
  4     ( select deptno, avg(sal) asal
  5       from   emp
  6       group by deptno )
  7    select *
  8    from avgsal
  9  )
 10  select * from my_outer_query;
  with avgsal as
  *
ERROR at line 3:
ORA-32034: unsupported use of WITH clause



New WITH in 26ai

Starting in Oracle Database 26ai, those restrictions disappear. Nested WITH clauses now work throughout SQL.

The previous example works exactly as you’d hope.



SQL> with w1 as
  2   ( with w2 as
  3       (select deptno, dname
  4        from   dept
  5        where  dname = 'SALES'
  6       )
  7    select dname, emp.*
  8    from emp, w2
  9    where emp.deptno = w2.deptno
 10  )
 11  select dname, empno, ename, job
 12  from w1
 13  order by empno;

DNAME               EMPNO ENAME      JOB
-------------- ---------- ---------- ---------
SALES                7499 ALLEN      SALESMAN
SALES                7521 WARD       SALESMAN
SALES                7654 MARTIN     SALESMAN
SALES                7698 BLAKE      MANAGER
SALES                7844 TURNER     SALESMAN
SALES                7900 JAMES      CLERK



Even more deeply nested examples become legal.



SQL> with w1 as
  2    (with w2 as
  3      (with w3 as (
  4         select emp.*
  5         from emp, dept
  6         where emp.deptno = dept.deptno
  7         and dname = 'SALES')
  8       select *
  9       from w3
 10       where job = 'SALESMAN'
 11        )
 12    select empno, ename, job, mgr, hiredate, sal, comm, sal+comm as income
 13    from w2
 14    order by income desc
 15    fetch first 2 rows with ties
 16  )
 17  select *
 18  from w1
 19  order by income desc;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     INCOME
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7844 TURNER     SALESMAN        7698 08-SEP-81       1500
      7654 MARTIN     SALESMAN        7698 28-SEP-81       1250       1400       2650



Pretty much anywhere you could have SQL query, that query can now contain a WITH clause



SQL> with w1 as
  2    (select dname, emp.*
  3     from emp, ( with w2 as
  4                   (select deptno, dname
  5                    from dept
  6                    where dname = 'SALES' )
  7                 select *
  8                 from w2 ) dept_v
  9     where emp.deptno = dept_v.deptno
 10     )
 11  select dname, empno, ename, job
 12  from w1
 13  order by empno;

DNAME               EMPNO ENAME      JOB
-------------- ---------- ---------- ---------
SALES                7499 ALLEN      SALESMAN
SALES                7521 WARD       SALESMAN
SALES                7654 MARTIN     SALESMAN
SALES                7698 BLAKE      MANAGER
SALES                7844 TURNER     SALESMAN
SALES                7900 JAMES      CLERK



A bonus for SQL macros

One pleasant side effect appears with SQL macros. Whilst not directly related to the nested WITH enhancement, there has historically been issues when you start combining SQL macros and a WITH clause.

In 19c, SQL macros couldn’t be referenced inside a WITH clause.



SQL> create or replace function my_emp_with(p_empno number)
  2  return varchar2 sql_macro is
  3  begin
  4    return 'select * from emp where empno = p_empno';
  5  end;
  6  /

Function created.

SQL>
SQL> with my_query as
  2    ( select * from my_emp_with(7369) )
  3  select *
  4  from my_query;
with my_query as
*
ERROR at line 1:
ORA-64630: unsupported use of SQL macro: use of SQL macro inside WITH clause is not supported



In an upcoming release update of 26ai, the same code works without any issues.



SQL> create or replace function my_emp_with(p_empno number)
  2  return varchar2 sql_macro is
  3  begin
  4    return 'select * from emp where empno = p_empno';
  5  end;
  6  /

Function created.

SQL>
SQL> with my_query as
  2    ( select * from my_emp_with(7369) )
  3  select *
  4  from my_query;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7369 SMITH      CLERK           7902 17-DEC-80        800                    20

1 row selected.



Building for the next developer

The real benefit here comes in terms of maintainability. As much as we all like to write giant complex monolithic statements to show how smart we are ?? , it’s generally going to be better for the next developer that comes along to look after your code, if you can build solutions from small, understandable SQL blocks. It’s easier to write, much easier to review, and much easier to maintain.

Original post (opens in new tab)
View comments in original post (opens in new tab)

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating