WITH is now one of my favorite keywords in SQL.
That’s the keyword that lets us use modular design with SQL and also helps us avoid redundant logic in SQL statements.
The WITH clause (first introduced in Oracle Database 9i, also called “subquery factoring” and “common table expression”), to quote Tim Hall of ORACLE-BASE, “may be processed as an inline view or resolved as a temporary table. The advantage of the latter is that repeated references to the subquery may be more efficient as the data is easily retrieved from the temporary table, rather than being requeried by each reference.”
And even more exciting (to PL/SQL fanatics like myself), Oracle Database 12c added the WITH function and WITH procedure variations, allowing you to define PL/SQL subprograms right inside a DML statement. There are two primary reasons to do this:
- Reduce the cost of context switching from SQL to PL/SQL.
- Avoid the repetition of logic in your SQL statement that can be expressed in PL/SQL.
I offer a couple of links below to make it easy for you to learn so much more about this feature. I’d like to focus in this post on a refactoring I just completed on a long SQL statement, leveraging both forms of the WITH clause. It was fun, and I learned a few lessons I thought I’d pass on to you.
with cte_users as ( select dept_id, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = parent_dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE1' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end approver, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = parent_dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE2' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end adm, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = parent_dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE3' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end hr, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE4' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end s_approver, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE5' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end s_adm, case when exists ( select 1 from user_departments ud, user_roles ur, app_roles ar where ud.department_id = dept_id and ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and role_code='CODE6' and app_id=:APP_ID) then 'fa-check-circle' else 'fa fa-times-circle' end s_hr from all_departments ) select dept_desc department, b.adm Admins, b.approver Approvers, b.hr HR, b.s_approver S_Approvers, b.s_adm S_Admins, b.s_hr S_HR, DECODE(NVL(b.adm, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') adm_color, DECODE(NVL(b.approver, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') approver_color, DECODE(NVL(b.hr, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') hr_color, DECODE(NVL(b.s_adm, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') s_adm_color, DECODE(NVL(b.s_approver, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') s_approver_color, DECODE(NVL(b.s_hr, 'fa fa-times-circle'), 'fa-check-circle', 'green', 'red') s_hr_color from all_departments v, cte_users b where b.dept_id = v.dept_id
Anything catch your eye? 🙂
I know what catches my eye: repetition. As in the repeats of those case expressions and the six lines of decode.
Oh, and also: the six lines of decode, not in terms of repetition but in terms of decode. I don’t believe in decode anymore. Not now that we’ve got case.
The first thing I decided to do was reduce the code redundancy in the case expression. My first pass moved the four-way join query in each case expression out to its subquery:
with cte_role_check as ( select role_code from user_departments ud, user_roles ur, app_roles ar where ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and app_id = :APP_ID), cte_users as ( select dept_id, case when exists ( select 1 from cte_role_check where role_code = 'CODE1' ) then 'fa-check-circle' else 'fa fa-times-circle' end approver, ...
That certainly cut down on the code within each case expression, but there was still quite a lot there. Then I thought to myself: the case expression returns a value. Like a function. How about if I move all that logic into a function right inside the query?
Which changed the entire WITH clause in the original query into the following (note: I had to pass in :APP_ID to the function because it turns out you cannot reference a placeholder inside a function declared inside the query):
with function set_circle ( p_app_id in varchar2, p_role_code in varchar2) return varchar2 is l_dummy integer; begin select 1 into l_dummy from user_departments ud, user_roles ur, app_roles ar where ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and ar.role_code = p_role_code and hap.app_id = p_app_id; return 'fa-check-circle'; exception when no_data_found then return 'fa fa-times-circle'; end; cte_users as ( select dept_id, set_circle(:APP_ID, 'CODE1') approver, set_circle(:APP_ID, 'CODE2') adm, set_circle(:APP_ID, 'CODE3') hr, set_circle(:APP_ID, 'CODE4') s_approver, set_circle(:APP_ID, 'CODE5') s_adm, set_circle(:APP_ID, 'CODE6') s_hr from all_departments )
Oh, I like that! Now, time to take a look at the decodes. I could simply switch it to using case, but there would still be a lot of repetition within each case expression. I think it’s time for another with!
with function set_color (p_value in varchar2) return varchar2 is begin return case NVL(p_value, 'fa fa-times-circle') when 'fa-check-circle' then 'green' else 'red' end; end;
Which then leads to a refactored, much more concise, and utterly transformed query:
with function set_color (p_value in varchar2) return varchar2 is begin return case NVL(p_value, 'fa fa-times-circle') when 'fa-check-circle' then 'green' else 'red' end; end; function set_circle ( p_app_id in varchar2, p_role_code in varchar2 ) return varchar2 is l_dummy integer; begin select 1 into l_dummy from user_departments ud, app_roles ur, app_roles ar where ud.ur_id = ur.ur_id and ur.role_id = ar.role_id and hap.app_id = p_app_id; return 'fa-check-circle'; exception when no_data_found then return 'fa fa-times-circle'; end; cte_users as ( select dept_id, set_circle(:APP_ID, 'CODE1') approver, set_circle(:APP_ID, 'CODE2') adm, set_circle(:APP_ID, 'CODE3') hr, set_circle(:APP_ID, 'CODE4') s_approver, set_circle(:APP_ID, 'CODE5') s_adm, set_circle(:APP_ID, 'CODE6') s_hr from all_departments ) select dept_desc department, b.adm Admins, b.approver Approvers, b.hr HR, b.s_approver S_Approvers, b.s_adm S_Admins, b.s_hr S_HR, set_color(b.adm) adm_color, set_color(b.approver) approver_color, set_color(b.hr) hr_color, set_color(b.s_adm) s_adm_color, set_color(b.s_approver) s_approver_color, set_color(b.s_hr) s_hr_color from all_departments v, cte_users b where b.dept_id = v.dept_id
But then….but then…I put this query into my interactive report and tried to run it. All I saw on the screen was…
“unsupported use of WITH clause”? Nooooooooo! How can this be? APEX is so tightly integrated with SQL and PL/SQL and the database and…
Well, it is not an APEX issue, folks. It’s the way this feature works. See, if you want to use the WITH clause either in a non-query DML statement (insert, update, delete, merge) or in a sub-query of a select statement, then you have to include the with_plsql hint, as in this example from Connor McDonald’s post (see “Further exploration” below):
SQL> insert /*+ WITH_PLSQL */ into TARGET_TABLE 2 WITH 3 function my_initcap(p_string varchar2) 4 return varchar2 is 5 l_string varchar2(1000) := p_string; 6 begin ... 20 end; 21 select my_initcap(surname) 22 from names; 23 /
You might be thinking: but the WITH clause is already at the top of the select, not in a sub-query. So what’s the problem? The “problem” is that the query you write for an interactive report is not the query that is executed when the interactive report runs. APEX has a very smart report engine that transforms your query into something that can be used to provide all the wonderful features of an interactive report.
That’s why we get the “unsupported use” error. How do we fix it? APEX to the rescue! When you define a report, you can also provide an optimizer hint right in the properties for that report. This hint (or hints) will then be applied by APEX to the top level of the generated query, and then the report will work fine!
Summing it all up
Here are my take-aways from my “fun with WITH” this week:
- Use the WITH clause to make your SQL more readable and concise.
- WITH function still involves a context switch, though the overhead is reduced compared to a “normal” function stored in the database. So be careful how you use the function. It has the most impact when executed in the where clause.
- You can’t put references to APEX placeholders inside the functions.
- You need to add the WITH_PLSQL hint to your APEX reports.
- You cannot use the WITH clause with queries executed inside a PL/SQL block.
Further exploration
Oracle documentation on WITH clause: https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6
Oracle-Base.com on WITH clause: https://oracle-base.com/articles/misc/with-clause#subquery-factoring
With and without WITH_PLSQL within a WITH SQL statement by Connor McDonald: https://connor-mcdonald.com/2019/05/31/with-and-without-with_plsql-within-a-with-sql-statement/