Есть лента новостей. Все новости разделены на 10 категорий(Политика, спорт, авто, недвижимость и тд).Однако, задача стояла - сделать это оптимально, а стандартное решение с обычным TopN через row_number никак оптимальным не назвать, особенно в случае больших таблиц, относительно небольшого количества категорий и неравномерного распределения или просто общей низкой селективности. И вот после нескольких более-менее хороших вариантов, подглядев в решение с PostgreSQL(правда я в нем не стал сильно разбираться, достаточно было увидеть рекурсию, min и предикат), получился отличный вариант.
Надо 1 запросом для каждой категории выбрать 4 новости.
Получается если перебрать результат - сразу идет 4 новости о политике, затем 4 новости о спорте и тд.
Но обо всем по порядку:
1. Получение distinct значений из индекса
Пусть есть табличка с индексом на поле а:
create table xt_test(a not null,b not null,c) as select length(object_name) ,nvl(object_id,0) ,o.OBJECT_NAME from dba_objects o; create index ix_test_a on xt_test(a); DB11G/XTENDER> select i.index_name 2 ,i.distinct_keys,i.num_rows 3 ,i.blevel,i.leaf_blocks 4 ,i.avg_leaf_blocks_per_key,i.avg_data_blocks_per_key 5 from user_indexes i where i.table_name='XT_TEST'; INDEX_NAME DISTINCT_KEYS NUM_ROWS BLEVEL LEAF_BLOCKS AVG_LEAF_BLOCKS_PER_KEY AVG_DATA_BLOCKS_PER_KEY ----------- ------------- --------- -------- ----------- ----------------------- ----------------------- IX_TEST_A 30 69230 1 135 4 191 1 row selected.
drop table xt_test purge; create table xt_test(a not null,b not null,c) as select length(object_name) ,nvl(object_id,0) ,o.OBJECT_NAME from dba_objects o ; create index ix_test_a on xt_test(a); begin dbms_stats.gather_table_stats( '' ,'XT_TEST' ,estimate_percent=>100 ,cascade=>true ,method_opt => 'for all indexed columns size auto' ); end; / select i.index_name ,i.distinct_keys,i.num_rows ,i.blevel,i.leaf_blocks ,i.avg_leaf_blocks_per_key,i.avg_data_blocks_per_key from user_indexes i where i.table_name='XT_TEST';
Распределение значений у этого поля очень неравномерное:
A | COUNT(*) |
---|---|
1 | 11 |
2 | 20 |
3 | 59 |
4 | 92 |
5 | 178 |
6 | 251 |
7 | 521 |
9 | 570 |
10 | 636 |
8 | 640 |
11 | 962 |
12 | 970 |
13 | 1151 |
15 | 1363 |
14 | 1544 |
16 | 1692 |
18 | 2021 |
17 | 2023 |
19 | 2550 |
20 | 2606 |
21 | 3050 |
22 | 3171 |
23 | 3395 |
24 | 3472 |
29 | 3527 |
27 | 3596 |
26 | 3698 |
28 | 4130 |
25 | 4268 |
30 | 17063 |
Всего | 69230 |
С IFS:
DB11G/XTENDER> select/*+ INDEX(xt_test) */ distinct a from xt_test; 30 rows selected. Elapsed: 00:00:00.02 Execution Plan ---------------------------------------------------------- Plan hash value: 3405466263 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 30 | 90 | 140 (3)| 00:00:02 | | 1 | SORT UNIQUE NOSORT| | 30 | 90 | 140 (3)| 00:00:02 | | 2 | INDEX FULL SCAN | IX_TEST_A | 69230 | 202K| 137 (1)| 00:00:02 | -------------------------------------------------------------------------------- Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 138 consistent gets 0 physical reads 0 redo size 751 bytes sent via SQL*Net to client 431 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 30 rows processedC IFFS:
DB11G/XTENDER> select distinct a from xt_test; 30 rows selected. Elapsed: 00:00:00.05 Execution Plan ---------------------------------------------------------- Plan hash value: 4206828362 ----------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 30 | 90 | 42 (10)| 00:00:01 | | 1 | HASH UNIQUE | | 30 | 90 | 42 (10)| 00:00:01 | | 2 | INDEX FAST FULL SCAN| IX_TEST_A | 69230 | 202K| 38 (0)| 00:00:01 | ----------------------------------------------------------------------------------- Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 143 consistent gets 0 physical reads 0 redo size 751 bytes sent via SQL*Net to client 431 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 30 rows processedА ведь можно же было бы идти по дереву заходя только в нужные блоки, а не по всем листовым блокам! Однако, Oracle сам по себе такое не осилит и тут как раз придется его выкрутиться: у Oracle помимо IFS(min/max) есть и IRS(min/max) хорошо работающий с диапазонами и границами, и с помощью рекурсивного запроса, можно заставить его читать только нужное!
DB11G/XTENDER> with t_unique( a ) as ( 2 select min(t1.a) 3 from xt_test t1 4 union all 5 select (select min(t1.a) from xt_test t1 where t1.a>t.a) 6 from t_unique t 7 where a is not null 8 ) 9 select * from t_unique where a is not null; 30 rows selected. Elapsed: 00:00:00.00 Execution Plan ---------------------------------------------------------- Plan hash value: 2791305641 ------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 2 | 26 | 4 (0)| 00:00:01 | |* 1 | VIEW | | 2 | 26 | 4 (0)| 00:00:01 | | 2 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | | | 3 | SORT AGGREGATE | | 1 | 3 | | | | 4 | INDEX FULL SCAN (MIN/MAX) | IX_TEST_A | 1 | 3 | 2 (0)| 00:00:01 | | 5 | SORT AGGREGATE | | 1 | 3 | | | | 6 | FIRST ROW | | 1 | 3 | 2 (0)| 00:00:01 | |* 7 | INDEX RANGE SCAN (MIN/MAX) | IX_TEST_A | 1 | 3 | 2 (0)| 00:00:01 | |* 8 | RECURSIVE WITH PUMP | | | | | | ------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("A" IS NOT NULL) 7 - access("T1"."A">:B1) 8 - filter("A" IS NOT NULL) Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 36 consistent gets 0 physical reads 0 redo size 751 bytes sent via SQL*Net to client 431 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 32 sorts (memory) 0 sorts (disk) 30 rows processedРазница очевидна: 36 consistent gets на 30 значений, вместо 135. И это очень маленькая табличка, а на миллионах и миллиардах записей разница будет еще более огромной!
Поясню алгоритм:
- В первой части union all (3-4 строки плана) мы указываем с чего начать рекурсию, а конкретно выбираем минимальное(первое) значение из индекса.
- Затем с помощью IRS(min/max) (7-6-5 строки плана) выбираем первое значение большее выбранного на предыдущем этапе
- Повторяем рекурсию пока что-то находим
Переходим к следующему:
2. Топ N записей для каждого значению ключа
Теперь вооруженные легким получением каждого начального значения, мы легко можем получить Топ для каждого из них, но для этого придется воспользоваться либо недокументированным Lateral() с включением соответствующего ивента, либо более простым и стандартным table(). Проблема остается только в том, что воспользоваться инлайн вью с row_number/rownum не получится, т.к. предикат с верхнего уровня не просунется, и придется воспользоваться простым ограничением по count stop key(по rownum) c обязательным доступом по IRS descending (order by там в общем лишний, но дополнительно уменьшает стоимость чтения IRS descending который нам и нужен для неявной сортировки) c подсказкой index_desc, чтобы прибить намертво, иначе сортировка может слететь:
DB11G/XTENDER> with t_unique( a ) as ( 2 select min(t1.a) 3 from xt_test t1 4 union all 5 select (select min(t1.a) from xt_test t1 where t1.a>t.a) 6 from t_unique t 7 where a is not null 8 ) 9 select/*+ use_nl(rids tt) */ * 10 from t_unique v 11 ,table( 12 cast( 13 multiset( 14 select/*+ index_desc(tt ix_xt_test_ab) */ tt.rowid rid 15 from xt_test tt 16 where tt.a=v.a 17 and rownum<=5 18 order by tt.b desc 19 ) 20 as sys.odcivarchar2list 21 ) 22 ) rids 23 ,xt_test tt 24 where tt.rowid=rids.column_value 25 order by tt.a,tt.b desc; 150 rows selected. Elapsed: 00:00:00.01 Execution Plan ---------------------------------------------------------- Plan hash value: 4085270117 ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 11M| 506M| | 149K (1)| 00:29:54 | | 1 | SORT ORDER BY | | 11M| 506M| 649M| 149K (1)| 00:29:54 | | 2 | NESTED LOOPS | | 11M| 506M| | 16402 (1)| 00:03:17 | | 3 | NESTED LOOPS | | 16336 | 239K| | 60 (0)| 00:00:01 | | 4 | VIEW | | 2 | 26 | | 4 (0)| 00:00:01 | | 5 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | | | | 6 | SORT AGGREGATE | | 1 | 3 | | | | | 7 | INDEX FULL SCAN (MIN/MAX) | IX_TEST_A | 1 | 3 | | 2 (0)| 00:00:01 | | 8 | SORT AGGREGATE | | 1 | 3 | | | | | 9 | FIRST ROW | | 1 | 3 | | 2 (0)| 00:00:01 | |* 10 | INDEX RANGE SCAN (MIN/MAX) | IX_TEST_A | 1 | 3 | | 2 (0)| 00:00:01 | |* 11 | RECURSIVE WITH PUMP | | | | | | | | 12 | COLLECTION ITERATOR SUBQUERY FETCH | | 8168 | 16336 | | 28 (0)| 00:00:01 | |* 13 | COUNT STOPKEY | | | | | | | |* 14 | INDEX RANGE SCAN DESCENDING | IX_XT_TEST_AB | 2308 | 64624 | | 8 (0)| 00:00:01 | |* 15 | TABLE ACCESS BY USER ROWID | XT_TEST | 692 | 22144 | | 1 (0)| 00:00:01 | ---------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 10 - access("T1"."A">:B1) 11 - filter("A" IS NOT NULL) 13 - filter(ROWNUM<=5) 14 - access("TT"."A"=:B1) 15 - access(CHARTOROWID(VALUE(KOKBF$))) Statistics ---------------------------------------------------------- 1 recursive calls 0 db block gets 166 consistent gets 0 physical reads 0 redo size 7523 bytes sent via SQL*Net to client 519 bytes received via SQL*Net from client 11 SQL*Net roundtrips to/from client 33 sorts (memory) 0 sorts (disk) 150 rows processedАналогично можно через lateral:
alter session set events '22829 trace name context forever'; with t_unique( a ) as ( select min(t1.a) from xt_test t1 union all select (select min(t1.a) from xt_test t1 where t1.a>t.a) from t_unique t where a is not null ) select/*+ use_nl(rids tt) */ * from t_unique v ,lateral( select/*+ index_desc(tt ix_xt_test_ab) */ tt.* from xt_test tt where tt.a=v.a and rownum<=5 order by tt.a, b desc ) r order by r.a,r.b descВ целом, конечно, можно было обойтись и без опасной сортировки, а взять и воспользоваться xmltable вместо table с передачей параметра сразу во внутренний подзапрос, но это чуток потяжелее чем обычный table.
Comments
Интересный метод, спасибо, Саян
Имхо, имеет один недостаток, который может быть существенным, - пока не умеет считать стоимость для рекурсивных WITH.
Длинный коммент (>4096) не удалось оставить, поэтому отписал отдельно http://iusoltsev.wordpress.com/2012/09/26/recursive-subquery-factoring-cost/
Отправить комментарий