As Justin's said (and the links in his post prove), the cardinality rule is a myth. This aside, there's a good reason to use bitmap indexes on fact tables: separate bitmap indexes on can easily be combined by the optimizer to reduce the numbers of rows to access.
This is very useful with fact tables with a large number of dimensions. While any single dimension may return a large percentage of the data, when combined with others this may fall
dramatically. For example, you may have thousands of orders per day and thousands of customers (with hundreds of orders each), but a given customer is likely to only have 1-2 orders on any given day.
This saves you having to create multi-column b-tree indexes. As (ignoring some skip-scan conditions), the leading column in an index must be referenced in the where clause to be used. So with three dimensions you need to create six multi-column b-tree indexes to ensure an index is available for every query your users may throw at you ( ind1: col1, col2, col3; ind2: col1, col3, col2; ind3: col2, col1, col3 etc.)
With bitmaps, you just need three single column indexes and can leave the optimizer to decide whether it's beneficial to combine them or not.
This example shows how the two single column bitmap indexes are combined, but the b-tree indexes aren't (note Oracle can convert b-tree indexes to bitmaps, but this is rare):
create table big_fact_table (first_date date, second_date date, junk varchar2(20));
insert into big_fact_table
SELECT trunc(sysdate)+floor(level/100), trunc(sysdate)+mod(level,100),
dbms_random.string('x', 20)
FROM dual connect by level <= 1000;
create bitmap index fd_i on big_fact_table (first_date);
create bitmap index sd_i on big_fact_table (second_date);
commit;
exec dbms_stats.gather_table_stats(user, 'big_fact_table', cascade => true);
explain plan for
SELECT * FROM big_fact_table
where first_date = trunc(sysdate)+1
and second_date = trunc(sysdate)+1;
-- both indexes can be combined easily to narrow the output
SELECT * FROM table(dbms_xplan.display(null, null, 'ROWS -COST -BYTES'));
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID | BIG_FACT_TABLE | 1 | 00:00:01 |
| 2 | BITMAP CONVERSION TO ROWIDS| | | |
| 3 | BITMAP AND | | | |
|* 4 | BITMAP INDEX SINGLE VALUE| SD_I | | |
|* 5 | BITMAP INDEX SINGLE VALUE| FD_I | | |
--------------------------------------------------------------------------
drop index fd_i;
drop index sd_i;
create index fd_i on big_fact_table (first_date);
create index sd_i on big_fact_table (second_date);
exec dbms_stats.gather_table_stats(user, 'big_fact_table', cascade => true);
explain plan for
SELECT * FROM big_fact_table
where first_date = trunc(sysdate)+1
and second_date = trunc(sysdate)+1;
-- the optimizer will only use one of the two available indexes
SELECT * FROM table(dbms_xplan.display(null, null, 'ROWS -COST -BYTES'));
-------------------------------------------------------------------------
| Id | Operation | Name | Rows | Time |
-------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 00:00:01 |
|* 1 | TABLE ACCESS BY INDEX ROWID| BIG_FACT_TABLE | 1 | 00:00:01 |
|* 2 | INDEX RANGE SCAN | FD_I | 100 | 00:00:01 |
-------------------------------------------------------------------------