Оптимизатор побеждает...

andrey_anonymous
Дата: 25.05.2006 20:37:31
Бьюсь уже целый день, оптимизатор побеждает :)
Поле боя: секционированная таблица вида
create table ane_test (pkey date, p1 number, p2 number, p3 date, val char(100))
partition by range(pkey)
(partition ane_test_p2 values less than (to_date('2006-03','yyyy-mm'))
,partition ane_test_p3 values less than (to_date('2006-04','yyyy-mm'))
,partition ane_test_p4 values less than (to_date('2006-05','yyyy-mm'))
,partition ane_test_p5 values less than (to_date('2006-06','yyyy-mm'))
);
create index ane_test$p1$p2$p3 on ane_test(p1,p2,p3) local;
insert into ane_test select
 to_date('2006-03','yyyy-mm')+rownum/200, mod(rownum,4),
 rownum*3,
 to_date('2006-03','yyyy-mm')+rownum/200+dbms_random.value,
 'Val'||rownum 
from dual connect by level <=18000;
commit;
exec dbms_stats.gather_table_stats(user,'ANE_TEST',cascade=>TRUE);

где pkey - ключ секционирования, который мешает жить.
p1 - некий условный источник данных.
p2 - целочисленная последовательность. Определяет порядок следования записей от источника p1.
Шаг варьируется в широких пределах и носит достаточно случайный характер
(эта особенность не позволяет ограничить p2 снизу без ущерба для логики).
Последовательности p2 для различных значений p1 независимы.
p3 - просто некая дата, коррелирующая с p2, но к рассматриваемой проблеме отношения не имеющая :)

Одним из условий задачи является запрет на изменение предложенного окружения :)

Задача: задешево получить max(val) среди N последних записей при заданном p1.

Вроде все просто, но не совсем:
Пусть для определенности p1=2, N=10, pkey>sysdate-90
-- Просто отбор данных, без агрегации. 
SELECT TRIM(VAL) 
FROM
 ( SELECT /*+ index_desc(t ane_test$p1$p2$p3) */ * FROM ANE_TEST T WHERE PKEY 
  > :B2 AND P1 = :B1 ORDER BY P2 DESC ) WHERE ROWNUM < :B3 


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        1      0.00       0.00          0         11          0           4
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.00       0.00          0         11          0           4
-- Всего 11 логических чтений, замечательный результат

Rows     Row Source Operation
-------  ---------------------------------------------------
      4  COUNT STOPKEY (cr=11 pr=0 pw=0 time=1219 us)
      4   VIEW  (cr=11 pr=0 pw=0 time=1203 us)
      4    SORT ORDER BY STOPKEY (cr=11 pr=0 pw=0 time=1168 us)
     12     PARTITION RANGE ITERATOR PARTITION: 4 KEY (cr=11 pr=0 pw=0 time=543 us)
     12      COUNT STOPKEY (cr=11 pr=0 pw=0 time=616 us) -- Вот оно, счастье!
                    -- Умный оптимизатор отбирает в каждой просматриваемой секции 
                    -- индекса не более N записей. Очень логично и замечательно работает.
     12       TABLE ACCESS BY LOCAL INDEX ROWID ANE_TEST PARTITION: 4 KEY (cr=11 pr=0 pw=0 time=591 us)
     12        INDEX RANGE SCAN DESCENDING ANE_TEST$P1$P2$P3 PARTITION: 4 KEY (cr=7 pr=0 pw=0 time=351 us)(object id 523680)

--------------------------------------------------------------------------------
-- НО СТОИТ ДОБАВИТЬ АГРЕГАЦИЮ - и счастья как не бывало:
SELECT MAX(TRIM(VAL)) 
FROM
(SELECT VAL FROM ( SELECT /*+ index_desc(t ane_test$p1$p2$p3) */ * FROM 
  ANE_TEST T WHERE PKEY > :B2 AND P1 = :B1 ORDER BY P2 DESC ) WHERE ROWNUM < :B3 )


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        1      0.02       0.02          0        351          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3      0.02       0.02          0        351          0           1
-- Та же задача, те же параметры, но уже 351 логическое чтение.
-- В "боевой" обстановке - до 300000, что непозволительно много.
Misses in library cache during parse: 0
Optimizer goal: ALL_ROWS
Parsing user id: 25939     (recursive depth: 1)

Rows     Row Source Operation
-------  ---------------------------------------------------
      1  SORT AGGREGATE (cr=351 pr=0 pw=0 time=24574 us)
      4   VIEW  (cr=351 pr=0 pw=0 time=24346 us)
      4    COUNT STOPKEY (cr=351 pr=0 pw=0 time=24340 us)
      4     VIEW  (cr=351 pr=0 pw=0 time=24325 us)
      4      SORT ORDER BY STOPKEY (cr=351 pr=0 pw=0 time=24287 us)
   4500       PARTITION RANGE ITERATOR PARTITION: 4 KEY (cr=351 pr=0 pw=0 time=18084 us) -- Обратите внимание, COUNT STOPKEY отсутствует. 
                    -- Т.е. в каждом разделе сканируется все поддерево индекса и 
                    -- отсечение "излишков" производится лишь при сортировке. 
   4500        TABLE ACCESS BY LOCAL INDEX ROWID ANE_TEST PARTITION: 4 KEY (cr=351 pr=0 pw=0 time=13769 us)
   4500         INDEX RANGE SCAN DESCENDING ANE_TEST$P1$P2$P3 PARTITION: 4 KEY (cr=23 pr=0 pw=0 time=4701 us)(object id 523680)
--------------------------------------------------------------------------------

Трасса 10053 показывает, что для запроса с агрегацией "SINGLE TABLE ACCESS PATH (First K Rows)", дающий требуемый эффект, даже не рассматривается.

Тот же печальный результат получается даже если написать
with tab as ("отбор данных без агрегации") select * from tab;

Так что пришлось остановиться на компромиссном варианте (выборка №1 в массив на стороне PL/SQL и ручное вычисление max(val).)

Если кто-то сумеет решить задачу средствами SQL (т.е. сумеет впихнуть COUNT STOPKEY обратно под PARTITION RANGE ITERATOR) - буду рад если поделитесь :)
Заглянул
Дата: 25.05.2006 21:05:43
А какой у Вас сервер?
У меня вроде работает..
> SELECT max(TRIM(VAL) )
  2  FROM
  3   ( SELECT /*+ index_desc(t ane_test$p1$p2$p3) */ * FROM ANE_TEST T WHERE PKEY
  4    > &p2 AND P1 = &p1 ORDER BY P2 DESC ) WHERE ROWNUM < &p3;
Enter value for p2: sysdate - 90
Enter value for p1: 2
Enter value for p3: 10
old   4:   > &p2 AND P1 = &p1 ORDER BY P2 DESC ) WHERE ROWNUM < &p3
new   4:   > sysdate - 90 AND P1 = 2 ORDER BY P2 DESC ) WHERE ROWNUM < 10

Elapsed: 00:00:00.16

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=488 Card=1 Bytes=102)
   1    0   SORT (AGGREGATE)
   2    1     COUNT (STOPKEY)
   3    2       VIEW (Cost=488 Card=4500 Bytes=459000)
   4    3         SORT (ORDER BY STOPKEY) (Cost=488 Card=4500 Bytes=558000)
   5    4           PARTITION RANGE (ITERATOR) (Cost=359 Card=4500 Bytes=558000)
   6    5             COUNT (STOPKEY)
   7    6               TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'ANE_TEST' (TABLE) (Cost=35
          9 Card=4500 Bytes=558000)

   8    7                 INDEX (RANGE SCAN DESCENDING) OF 'ANE_TEST$P1$P2$P3' (INDEX) (Co
          st=30 Card=4500)





Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
         12  consistent gets
          0  physical reads
          0  redo size
        416  bytes sent via SQL*Net to client
        431  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          1  rows processed

> set autotrace off
> select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Prod
PL/SQL Release 10.2.0.1.0 - Production
CORE    10.2.0.1.0      Production
TNS for 32-bit Windows: Version 10.2.0.1.0 - Production
NLSRTL Version 10.2.0.1.0 - Production

Elapsed: 00:00:00.15
>
andrey_anonymous
Дата: 25.05.2006 21:08:24
Заглянул
А какой у Вас сервер?
У меня вроде работает..

Разные :)
От 9.2.0.7 до 10.2.0.1 - с одинаковым эффектом.
Можете снять 10053? Может, удастся понять что ему мешает...
Andrew Max
Дата: 25.05.2006 21:17:07
2 Заглянул:
> SELECT max(TRIM(VAL) )
  2  FROM
  3   ( SELECT /*+ index_desc(t ane_test$p1$p2$p3) */ * FROM ANE_TEST T WHERE PKEY
  4    > &p2 AND P1 = &p1 ORDER BY P2 DESC ) WHERE ROWNUM < &p3;

это далеко не то же самое, что у автора:
SELECT MAX(TRIM(VAL)) 
FROM
(SELECT VAL FROM ( SELECT /*+ index_desc(t ane_test$p1$p2$p3) */ * FROM 
  ANE_TEST T WHERE PKEY > :B2 AND P1 = :B1 ORDER BY P2 DESC ) WHERE ROWNUM < :B3 )
andrey_anonymous
Дата: 25.05.2006 21:19:37
Andrew Max
это далеко не то же самое, что у автора:

У меня и с литералами та же проблема, так что хочется верить в разницу параметров оптимизатора :)
Заглянул
Дата: 25.05.2006 21:28:03
Могу, сейчас сделаю и вышлю по почте.
Заглянул
Дата: 25.05.2006 21:29:37
2 Andrew Max:
Конечно, это разные вещи, кто ж говорит, что одно и то же ;)
Решил по-быстрому в плюсе протестировать.
andrey_anonymous
Дата: 25.05.2006 21:38:39
Заглянул
Могу, сейчас сделаю и вышлю по почте.

Можно на адрес aedemsky at mail dot ru
Заглянул
Дата: 25.05.2006 21:59:35
Выслал. Надеюсь, поможет в решении проблемы.
andrey_anonymous
Дата: 25.05.2006 22:12:18
Заглянул
Выслал. Надеюсь, поможет в решении проблемы.

Thnks!
Если поможет - доложусь :)