In this blog I'm going to Deep Dive in to Classic Report sorting, what it is, how it works including all the options for both users and developers.
This is part 1 of series of blogs entitled Things in APEX I've used for years, but not fully understood how they work - catchy right? ๐
Let's stat 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..
The sort has been defaulted to (the first column) ENAME. Look! it has an icon
Why? Well at column level these 4 columns have been given the Default Sequence of 1, Ascending
Why specifically these 4 columns? I'm not sure. If you want me to make a guess... its
All Strings
All Foreign Keys
... 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.
Next question
Q. If 4 columns share the same Sort Sequence, how has APEX decided to specifically place the sort on ENAME Asc?
The default is the column with the lowest Default Sequence. If there is a tie, its the lowest Sequence.
If you look at this picture again ...
... There is a 4 way tie for lowest Default Sequence, however ENAME wins as its the first in the list i.e. lowest Sequence.
Q. What SQL did it actually run?
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. Its just that EMPNO gets to claim that up arrow icon.
Q. So what happens when the user tries to sort on another column like Salary (outside of the 4)
Lets 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 it order like this SAL, ENAME, JOB, MGR, DEPTNO. I.e the chosen column followed by the famous 4.
Q. So what happens when the user tries try to sort on one of the 4 columns? like MGR
Lets 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 (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
Right so now it order like this MGR, ENAME, JOB, DEPTNO
MGR jumps the queue and the remaining order is preserved - Clever right?
Q. Why are all those nulls last doing in there?
These can be found on the Region Attributes
Q. Can I change the Ascending/Descending for each column?
Note: If you are following along at home, first click to sort Employee Name Ascending before continuing for maximum affect
Yes. I can change ENAME to sort Descending
Give it a run and...
... it looks exactly the same ๐ No change at all. Its not sorting Descending like I asked for.
Why? Because the sorting is controlled by a permanent user preference that already has this set to Ascending.
Q. Can I see this user preference?
Sure 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.
Q. If the Column Sequence Number is stored (i.e column 2) and not the Column Name (e.g ENAME), does that mean I can change the Column Sequence and the user will now default to another column?
You can freely order the Column Sequence (i.e Column Name > Layout > Sequence) without affecting the users 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 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.
Q. My table has 8 columns. What happens if I add a new 9th column, the users start sorting on that 9th column and then I drop that column?
Lets 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 I have come to terms that I will never beat it ๐ข
Now, 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
Now drop the column.
alter table "EMP" drop column "HIGH_JUMP_PERSONAL_BEST";
Adjust my Region Query (to remove the column) and lets see.
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. Asc or Desc) on the final column.
If you use an invalid sort direction i.e.
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;
... then the user gets the standard sorting, however no icon is placed next to any column.
Funnily enough, the absolute value 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 affect as column 5 (Hired - remember there is one hidden column) being set as the default sort.
Q. Can I delete all these User Preferences which are used for Sorting
Yep. Stick this in a Before Header Process
BEGIN
APEX_UTIL.REMOVE_SORT_PREFERENCES(:APP_USER);
END;
Warning โ ๏ธ ... that command deletes all sort 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 so wild. This command removes the preference from a User - not an application.
Q. Can I clear user preferences a bit more selectively?
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;
Q. Can I stop APEX saving the user preferences for sorting altogether?
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;
Q. Can I clear the Sorting using Reset Pagination or Clear Cache?
You mean this?
No it doesn't work.
Q. Matt, in your code, how did you know what the Preference Template was called?
I just looked in Administration > Manage Service > Session State > Preferences by User and I found examples like this one.
Q. Because Sort Preferences are saved for my User - including my own, how can I see what the Default Sorts are for a new user?
First, here is what doesn't work
Logging out of APEX & back in again
Restarting your Browser
Deleting Browser Cache & Cookies
Using another Browser
Using Incognito Mode / Private Browsing
Here are 3 options which do work
Option 1: Create a new user and log in with them
- This will work only once. Once the new user views the report, a new User Preference will be created.
Option 2: Duplicate the Report Region in Page Designer
- This generates a new Component ID and therefore a new User Sort Preference. It may be useful to Duplicate & Delete the region several times before finally settling on your preferred defaults
Option 3: Use one of the code samples I provided in this blog with Build Option (as desired).
Option 3.1: Use APEX_UTIL.REMOVE_SORT_PREFERENCES to remove all user preferences. i.e the wild approach.
Option 3.1: Clear selectively based on Static ID
Option 3.3: Use an Application Process to clear all Sort preferences for the current page (and Page Zero)
Q. What happens if I use my own Order By in the SQL
Lets try
select EMPNO,
ENAME,
JOB,
MGR,
HIREDATE,
SAL,
COMM,
DEPTNO
from EMP
ORDER BY ENAME
Give it a run and...
Amazing. Its now sorting by Employee Name... however, not so amazing, now the user cannot sort the columns ๐
Lets try adding them back
Oh dear, my favorite... 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.
Q. Whats all this stuff in Source > Order By Item?
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...
.. wait, what, Advisor doesn't even care! ๐ฅ
OK serious 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 columns headers.
However, what Source > Order By Item does, is to limit the Order By Controls to a Single Page Item, usually a Select List. You may want to do this in some circumstances.
Lets do it now:
Create a Select List Page Item (basically anything that supports a List of Values) in the EMP region with the same name as the Item name in Source > Order By Item. In my case its P6_ORDER_BY
Set the List of Values Type to Static Values and accept the default values. Basically, don't change them.
Set the attributes Display Extra Values and Display Null Value to the Off position
In the Region (EMP) > Source > Order by Item set it to these values and click OK
By the way, the Key has to be unique in this pop up.
Take a peek at the P6_ORDER_BY > List of Values > Static Values. See they have synchronized with Region (EMP) > Source > Order by Item
Fab. Now every time you use the Select List it applies the Clause. e.g ENAME ASC
Run the Page
Now the Select List can only be used to apply predefined sorts. Be aware that column header sorting is now disabled.
Q. Is this better than I had it before?
Personally, No. I think column heading sorting provides much more functionality to the user.
Q. Why is it even a feature of APEX then?
The answer is... that not all Classic Reports use the Theme > Standard Template!
Some use the Theme > Cards Template, some use Theme > Comments Template.
Some use the Cards Region Type (this is a better choice as APEX doesn't insist on specifically names column aliases).
All these types do not have column headings and therefore are not sortable by the user.
However users may still require some type of sorting & developers would also like a declarative option.
The answer is to all this is the Source > Order by Item feature ๐ฅ
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.
Q. Does Source > Order By Item also create a User Preference which APEX then uses to default the Select List next time they use this page?
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 users logs out & logs back in again, then its back to the item's initial default value - not any user preference.
Conclusion
I hope that's given you a huge Deep Dive to Classic Report column sorting. I learnt lots by blogging this & it was strangely fun to interview myself.
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.
ENJOY
What's the picture? Its the Pinewoods Panorama - where you can see from Wharfedale right across Nidderdale to Sutton Bank. Here's a good video of someone driving Sutton Bank.