This is part 1 of a series of blogposts entitled Things in APEX I’ve used for years, but not fully understood how they work – catchy right? 😎 The article will have a slightly different structure than my previous pieces: I’ll point out questions I asked myself – often ones that may come to your mind while reading – and then provide answers.
Let’s start by creating a Classic Report on a SQL Statement using the wizard.
select EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO from EMP
I’m going to give it a static ID of EMP. You’ll see why later.
It looks like this:
Note that the sorting has been defaulted to (the first column) ENAME. And look! It has an icon:
Now, let’s take a deeper look at this report and consider the – both big and small – questions that come to mind.
Well, at the column level, these 4 columns have been given the Default Sequence of 1, Ascending.
I’m not sure. If you want me to make a guess there are two reasons:
I’ve tested a few tables and this seems to be the case.
Note: The other 3 columns are sortable, however, they do not have a default sequence.
The default is the column with the lowest Default Sequence. If there is a tie, it’s the lowest Sequence.
Look at the picture above again and notice that there is a 4-way tie for the lowest Default Sequence, however, ENAME wins as it’s the first in the list, i.e., it has the lowest Sequence.
This one:
select i.* from (select "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" from(select /*+ qb_name(apex$inner) */d."EMPNO",d."ENAME",d."JOB",d."MGR",d."HIREDATE",d."SAL",d."COMM",d."DEPTNO" from(select x.* from "EMP" x )d )i )i where 1=1 order by "ENAME" asc nulls last,"JOB" asc nulls last,(select "ENAME"from (select "ENAME","EMPNO" from(select x.* from "EMP" x )i )where "EMPNO"=i."MGR") asc nulls last,(select "DNAME"from (select "DNAME","DEPTNO" from(select x.* from "DEPT" x )i )where "DEPTNO"=i."DEPTNO") asc nulls last
Note: In the Order By clause, the columns appear in the same order as I previously described: ENAME, JOB, MGR, DEPTNO. It’s just that EMPNO gets to claim that up arrow icon.
Let’s see.
select i.* from (select "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" from(select /*+ qb_name(apex$inner) */d."EMPNO",d."ENAME",d."JOB",d."MGR",d."HIREDATE",d."SAL",d."COMM",d."DEPTNO" from(select x.* from "EMP" x )d )i )i where 1=1 order by "SAL" asc nulls last,"ENAME" asc nulls last,"JOB" asc nulls last,(select "ENAME"from (select "ENAME","EMPNO" from(select x.* from "EMP" x )i )where "EMPNO"=i."MGR") asc nulls last,(select "DNAME"from (select "DNAME","DEPTNO" from(select x.* from "DEPT" x )i )where "DEPTNO"=i."DEPTNO") asc nulls last
Right. So, now, the order is like this: SAL, ENAME, JOB, MGR, DEPTNO. I.e., it’s the chosen column followed by the famous four.
Let’s check.
select i.* from (select "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" from(select /*+ qb_name(apex$inner) */d."EMPNO",d."ENAME",d."JOB",d."MGR",d."HIREDATE",d."SAL",d."COMM",d."DEPTNO" from(select x.* from "EMP" x )d )i )i where 1=1 order by (select "ENAME"from (select "ENAME","EMPNO" from(select x.* from "EMP" x )i )where "EMPNO"=i."MGR") asc nulls last,"ENAME" asc nulls last,"JOB" asc nulls last,(select "DNAME"from (select "DNAME","DEPTNO" from(select x.* from "DEPT" x )i )where "DEPTNO"=i."DEPTNO") asc nulls last
Now the order is like this: MGR, ENAME, JOB, DEPTNO. MGR jumps the queue and the remaining order is preserved – clever, right?
These can be found in the Attributes tab.
Note: If you are following along at home, first click to sort Employee Name Ascending before continuing for maximum effect.
Yes. You can change ENAME to sort Descending.
Give it a run and… it looks exactly the same. 😕 No change at all. It’s not sorting Descending like I asked for.
Because the sorting is controlled by a permanent user preference that already has this set to Ascending. To see this preference, create a Dynamic Region Content with this SQL:
DECLARE l_region_static_id_c CONSTANT VARCHAR2(512) DEFAULT 'EMP'; l_preference_template_c CONSTANT VARCHAR2(512) DEFAULT 'FSP&APP_ID._P&APP_PAGE_ID._R%0_SORT'; l_preference_name VARCHAR2(512) DEFAULT NULL; l_preference_value VARCHAR2(512) DEFAULT NULL; BEGIN FOR x IN ( SELECT region_id FROM apex_application_page_regions WHERE application_id = :APP_ID AND page_id = :APP_PAGE_ID AND static_id = l_region_static_id_c ) LOOP l_preference_name := apex_plugin_util.replace_substitutions( apex_string.format(l_preference_template_c, x.region_id ) ); l_preference_value := apex_util.get_preference( p_preference => l_preference_name, p_user => :APP_USER); RETURN '<p><span class="u-bold"> Preference Name: </span>' ||l_preference_name || '</p>' || '<p><span class="u-bold"> Preference Value: </span>' ||l_preference_value || '</p>'; END LOOP; END;
There we go:
Note that column 1 is the Hidden Column EMPNO, therefore column 2 (i.e., ENAME) is stored instead.
Yes, you can freely order the Column Sequence (i.e., Column Name > Layout > Sequence) without affecting the user’s default sort at all. 👌
The number in the User Preference Value, i.e., sort_2_asc, references the Column Order defined in the Region SQL Statement. Therefore, if I were to change the SQL statement to this:
select EMPNO, JOB, -- JOB is now column 2. This used to be column 3 ENAME, -- ENAME is now column 3. This used to be column 2 MGR, HIREDATE, SAL, COMM, DEPTNO from EMP
… then my users would start complaining to me because their preference is still to sort on column 2, however, column 2 is now JOB and not ENAME.
To the users, I’ve broken their sorting.
Let’s try.
alter table "EMP" add ("HIGH_JUMP_PERSONAL_BEST" NUMBER);
Note. My High Jump best is 1.68m which I obtained when I was 15 years old. I have come to terms that I will never beat it. 😢
Now, I adjust my Region Query (to add the column) and add a Sort for my user.
You can see the preference value is now sort_9_asc.
I drop the column:
alter table "EMP" drop column "HIGH_JUMP_PERSONAL_BEST";
When I adjust my Region Query (to remove the column), the result looks like this:
Well, no Blue Screen of Death, however the sort has now been placed on the final column. The same thing happens when I set it sky-high:
BEGIN APEX_UTIL.SET_PREFERENCE( p_preference => 'FSP192_P14_R40495703897447889_SORT', p_value => 'sort_1919_desc', p_user => :APP_USER); END;
Interestingly, even though column 1919 doesn’t exist, APEX still honors the sort direction (i.e., Asc or Desc) on the final column.
If you use an invalid sort direction like this one:
BEGIN APEX_UTIL.SET_PREFERENCE( p_preference => 'FSP192_P14_R40495703897447889_SORT', p_value => 'sort_5_lufc', p_user => :APP_USER); END;
… then it defaults the sort order to Ascending.
If you specify an invalid column number:
BEGIN APEX_UTIL.SET_PREFERENCE( p_preference => 'FSP192_P14_R40495703897447889_SORT', p_value => 'sort_leeds_asc', p_user => :APP_USER); END;
… the user gets the standard sorting, however, no icon is placed next to any column. Funnily enough, the absolute values of column numbers are used.
BEGIN APEX_UTIL.SET_PREFERENCE( p_preference => 'FSP192_P14_R40495703897447889_SORT', p_value => 'sort_-5_asc', p_user => :APP_USER); END;
In the above example, this has the same effect as column 5 (Hired – remember there is one hidden column) being set as the default sort.
Yep. Stick this in a Before Header Process:
BEGIN APEX_UTIL.REMOVE_SORT_PREFERENCES(:APP_USER); END;
Warning! ⚠️ This command deletes all sorts of preferences for the current user for all regions, not only on the current page but for all application pages across all workspace applications. Wild, right? Well, not that wild because it removes the preference from a User – not the application.
Yep. Use something like this to pick them and remove them (this code identifies a region to clear by its Static ID):
DECLARE l_region_static_id_c CONSTANT VARCHAR2(512) DEFAULT 'EMP'; l_preference_template_c CONSTANT VARCHAR2(512) DEFAULT 'FSP&APP_ID._P&APP_PAGE_ID._R%0_SORT'; l_preference_name VARCHAR2(512) DEFAULT NULL; l_preference_value VARCHAR2(512) DEFAULT NULL; BEGIN FOR x IN ( SELECT region_id FROM apex_application_page_regions WHERE application_id = :APP_ID AND page_id = :APP_PAGE_ID AND static_id = l_region_static_id_c ) LOOP l_preference_name := apex_plugin_util.replace_substitutions( apex_string.format(l_preference_template_c, x.region_id ) ); apex_util.remove_preference( p_preference => l_preference_name, p_user => :APP_USER); END LOOP; END;
No. But you could try this as an Application Process running On Load: Before Header to automatically remove the user preference for all regions before the page loads. This technique simulates the User Preferences never being used.
DECLARE l_preference_template_c CONSTANT VARCHAR2(512) DEFAULT 'FSP&APP_ID._P&APP_PAGE_ID._R%0_SORT'; l_preference_name VARCHAR2(512) DEFAULT NULL; l_preference_value VARCHAR2(512) DEFAULT NULL; BEGIN FOR x IN ( SELECT region_id FROM apex_application_page_regions WHERE application_id = :APP_ID AND page_id IN ( :APP_PAGE_ID, 0 ) ) LOOP l_preference_name := apex_plugin_util.replace_substitutions( apex_string.format(l_preference_template_c, x.region_id ) ); apex_util.remove_preference( p_preference => l_preference_name, p_user => :APP_USER); END LOOP; END;
Do you mean this?
No, as you can see, it doesn’t work.
I just looked in Administration > Manage Service > Session State > Preferences by User and I found examples like this one.
First, here is what doesn’t work:
Here are 3 options that do work:
1. Create a new user and log in using their account
2. Duplicate the Report Region in Page Designer
3. Use one of the code samples I provided in this blog with the Build Option (as desired)
Let’s try:
select EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO from EMP ORDER BY ENAME
When I give it a run, I see this:
Amazing. It’s now sorting by Employee Name… However, not so amazing, as now the user cannot sort the columns. 😠
Let’s try adding them back:
Oh dear, my favorite thing – errors.
There is a very good explanation for this that is definitely worth reading. However, for now, I’ll put the attributes back manually and remove the Order By clause.
You mean this:
Which opens this:
What’s this P6_ORDER_BY? I don’t have a P6_ORDER_BY Page Item! Oh man, the Advisor is going to go nuts when it sees this…
Or… not? Wait, what? It seems it doesn’t even care! 🥀
Okay, seriously now – the fact is, so far, we’ve been working with a Classic Report Region Type with a Theme Standard Template. This provides users with the ability to sort columns, as described above, using column headers (see below).
However, what Source > Order By Item does, is limit the Order By Controls to a Single Page Item, usually a Select List. You may want to do this in some circumstances.
Let’s do it now:
By the way, the Key has to be unique in this popup.
Fabulous. Now every time you use the Select List, it applies the Clause, e.g., ENAME ASC.
Now, the Select List can only be used to apply predefined sorts. Be aware that column header sorting is now disabled.
Personally, no, I don’t think it is. I think column heading sorting provides much more functionality to the user.
The answer is that not all Classic Reports use the Theme > Standard template. Some use the Theme > Cards template, others the Theme > Comments template.
Some use the Cards Region Type (this is a better choice as APEX doesn’t insist on specifically naming column aliases).
All these types don’t have column headings and therefore are not sortable by the user. However, users may still require some type of sorting and developers would also like a declarative option. And the Source > Order by Item feature is the answer to all these dilemmas.
Look how useful it is on a Cards Region. The Order By Select List was created automatically by the wizard – it’s not the most beautiful of card lists… but you get the idea.
APEX does not create a User Preference for Source > Order By Item. However, it does submit the Value to Session, meaning that this setting persists for the duration of the user session. If the user logs out and logs back in again, then it’s back to the item’s initial default value – not any user preference.
I hope that’s given you a huge deep dive into Classic Report column sorting. I’ve learned a lot by writing this. And it was strangely fun to interview myself! One last tip: if you are experiencing Classic Report or Card Sorting that isn’t responding to your settings try the Selective Preference Delete Code in a Before Header Page Process.
And if you’re interested in Oracle APEX content, check out some of the other publications on the Pretius blog: