One option could be to move to 12c where there are session specific stats for global temporary tables. But even that doesn't necessarily solve the entire issue. For example, look at this in a single session:
SQL>
SQL> create global temporary table gtt on commit preserve rows
2 as select * from dba_objects
3 where 1=0;
Table created.
SQL>
SQL> insert into gtt
2 select * from dba_objects
3 where rownum <= 100;
100 rows created.
SQL> commit;
Commit complete.
SQL>
SQL> variable rc refcursor
SQL> exec open :rc for 'select count(*) from gtt';
PL/SQL procedure successfully completed.
SQL> print rc
COUNT(*)
----------
100
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor());
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------
SQL_ID gmxmjw679mxx5, child number 0
-------------------------------------
select count(*) from gtt
Plan hash value: 3344941513
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | 2 (100)| |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| GTT | 100 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
18 rows selected.
SQL>
SQL> insert into gtt
2 select * from dba_objects
3 where rownum <= 10000;
10000 rows created.
SQL>
SQL> variable rc refcursor
SQL> exec open :rc for 'select count(*) from gtt';
PL/SQL procedure successfully completed.
SQL> print rc
COUNT(*)
----------
10100
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor());
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------
SQL_ID gmxmjw679mxx5, child number 0
-------------------------------------
select count(*) from gtt
Plan hash value: 3344941513
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | 2 (100)| |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| GTT | 100 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
18 rows selected.
SQL>
So I created 100 rows, optimized for that, and then when I put another 10,000 rows in, I still optimized for just 100.
What you could do is to have some simple permutations of the same SQL to cater (say) 4 orders of magnitude. A table alias is sufficient to form a new SQL, eg
SQL>
SQL> create global temporary table gtt on commit preserve rows
2 as select * from dba_objects
3 where 1=0;
Table created.
SQL>
SQL> insert into gtt
2 select * from dba_objects
3 where rownum <= 100;
100 rows created.
SQL> commit;
Commit complete.
SQL>
SQL> variable rc refcursor
SQL> exec open :rc for 'select count(*) from gtt g_100_or_less';
PL/SQL procedure successfully completed.
SQL> print rc
COUNT(*)
----------
100
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor());
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------
SQL_ID grk7zx21v16c9, child number 0
-------------------------------------
select count(*) from gtt g_100_or_less
Plan hash value: 3344941513
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | 2 (100)| |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| GTT | 100 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
18 rows selected.
SQL>
SQL> insert into gtt
2 select * from dba_objects
3 where rownum <= 800;
800 rows created.
SQL>
SQL> variable rc refcursor
SQL> exec open :rc for 'select count(*) from gtt g_1000_or_less';
PL/SQL procedure successfully completed.
SQL> print rc
COUNT(*)
----------
900
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor());
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------
SQL_ID c4yxah6ngtjf9, child number 0
-------------------------------------
select count(*) from gtt g_1000_or_less
Plan hash value: 3344941513
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | 4 (100)| |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| GTT | 900 | 4 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
18 rows selected.
SQL>
SQL> insert into gtt
2 select * from dba_objects
3 where rownum <= 8000;
8000 rows created.
SQL>
SQL> variable rc refcursor
SQL> exec open :rc for 'select count(*) from gtt g_10k_or_less';
PL/SQL procedure successfully completed.
SQL> print rc
COUNT(*)
----------
8900
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor());
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------
SQL_ID 1p4u746xy4kta, child number 0
-------------------------------------
select count(*) from gtt g_10k_or_less
Plan hash value: 3344941513
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | 25 (100)| |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| GTT | 8242 | 25 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
18 rows selected.
SQL>
SQL>
SQL>
So assuming you have control over the *insertion* code, the logic would look along the lines of:
insert ...
if sql%rowcount < 100 then
open rc for 'select ... from gtt g_100_or_less';
elsif sql%rowcount < 1000 then
open rc for 'select ... from gtt g_1000_or_less';
elsif sql%rowcount < 10000 then
open rc for 'select ... from gtt g_10k_or_less';
else
open rc for 'select ... from gtt g_big';
In that way, you have some reasonably accurate stats data without literal SQL polluting your cache. Depending on volume of executions you could adjust the number of permutations to best suit your needs.