Sorting in APEX: Classic Reports and Card Regions Explained

Matt MulvaneyMatt Mulvaney
12 min read

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:

  1. 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

  2. Set the List of Values Type to Static Values and accept the default values. Basically, don't change them.

  3. Set the attributes Display Extra Values and Display Null Value to the Off position

  4. 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.

  5. 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

  6. 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.

5
Subscribe to my newsletter

Read articles from Matt Mulvaney directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Matt Mulvaney
Matt Mulvaney

With around 20 years on the job, Matt is one of the most experienced software developers at Pretius. He likes meeting new people, traveling to conferences, and working on different projects. He’s also a big sports fan (regularly watches Leeds United, Formula 1, and boxing), and not just as a spectator – he often starts his days on a mountain bike, to tune his mind.