понедельник, 10 февраля 2014 г.

Запросы с агрегатами в СУБД Oracle

В этой статье я собрал запросы, иллюстрирующие типичное использование агрегатных функций. Я начинаю с рассмотрения общих агрегатов (totals), когда агрегатные функции применяются ко всей выборке. Затем рассматриваются частные агрегаты, или субагрегаты (subtotals), получаемые при помощи GROUP BY, и совместное использование общих и частных агрегатов в запросах. Приводятся примеры запросов для получения доли или процента частных агрегатов в общих. В заключение, даны примеры использования HAVING.

Итак, начнем. Простейший случай - применение агрегатной функции ко всей выборке, в результате чего мы получаем общее (total) агрегатное значение:

SQL> -- сколько всего пользователей в БД?
SQL> SELECT COUNT(*) FROM all_users;
 
  COUNT(*)
----------
        14

SQL> -- сколько объектов БД доступно пользователю?
SQL> SELECT COUNT(ALL *) FROM all_objects;
 
COUNT(ALL*)
-----------
      20850

Здесь COUNT(ALL *) равнозначно COUNT(*).

SQL> -- объекты скольких различных типов доступны пользователю?
SQL> SELECT COUNT(DISTINCT object_type) FROM all_objects;
 
COUNT(DISTINCTOBJECT_TYPE)
--------------------------
                        34

Чтобы сравнить агрегатные данные по двум конкретным пользователям, воспользуемся UNION ALL:

SQL> SELECT 'SYS', COUNT(*), COUNT(UNIQUE object_type)
     FROM all_objects WHERE owner='SYS'
     UNION ALL
     SELECT 'SYSTEM', COUNT(*), COUNT(UNIQUE object_type)
     FROM all_objects WHERE owner='SYSTEM'
     ;
 
'SYS'    COUNT(*) COUNT(UNIQUEOBJECT_TYPE)
------ ---------- ------------------------
SYS          8984                       32
SYSTEM        502                       12

Выражения COUNT(DISTINCT object_type) и COUNT(UNIQUE object_type) равнозначны.

Что, если нас интересует подсчет только определенных значений? Например, сколько таблиц и вью каждого владельца доступно текущему пользователю? В этом случае нам поможет вложение скалярной функции DECODE в агрегатную:

SQL> SELECT COUNT(DECODE(object_type, 'TABLE', 1, 'VIEW', 1, NULL)) FROM all_objects;
 
COUNT(DECODE(OBJECT_TYPE,'TABL
------------------------------
                          6496

SQL> -- доля таблиц и вью в общем количестве доступных объектов:
SQL> SELECT COUNT(DECODE(object_type, 'TABLE', 1, 'VIEW', 1, NULL))/COUNT(*) FROM all_objects;

COUNT(DECODE(OBJECT_TYPE,'TABL
------------------------------
             0.311558752997602

SQL> -- пользователи-владельцы более 5000 объектов
SQL> SELECT username
     FROM all_users au
     WHERE 5000 < (SELECT count(*) FROM dba_objects WHERE owner=au.username)
     ORDER BY 1;
 
USERNAME
------------------------------
SYS

Перейдем к частным агрегатам, или субагрегатам (subtotals). Использование агрегатной функции с GROUP BY позволяет получить агрегатные данные в выбранном разрезе:

SQL> -- сколько объектов каждого типа доступно пользователю?
SQL> SELECT object_type, COUNT(*)
     FROM all_objects
     GROUP BY object_type;
 
OBJECT_TYPE           COUNT(*)
------------------- ----------
EDITION                      1
INDEX PARTITION            126
TABLE SUBPARTITION          32
CONSUMER GROUP               2
SEQUENCE                   157
...
INDEXTYPE                    8
CLUSTER                     10
TYPE                      1608
JOB                         14
EVALUATION CONTEXT           9
 
34 rows selected

SQL> -- сколько разных типов объектов у каждого пользователя?
SQL> SELECT owner, COUNT(*), COUNT(DISTINCT object_type)
     FROM all_objects
     GROUP BY owner;
 
OWNER                            COUNT(*) COUNT(DISTINCTOBJECT_TYPE)
------------------------------ ---------- --------------------------
APEX_040000                          2770                         11
APEX_040200                          3005                         12
AY                                     22                          7
CTXSYS                                380                         13
FLOWS_FILES                            11                          4
HR                                     34                          6
MDSYS                                 659                         14
OUTLN                                   8                          3
PUBLIC                               4013                          1
SYS                                  8984                         32
SYSTEM                                502                         12
XDB                                   462                         15
 
12 rows selected

Поскольку запросы с GROUP BY возвращают табличный результат, то такой результат можно агрегировать еще раз:

SQL> -- среднее количество доступных объектов каждого владельца
SQL> SELECT AVG(cnt)
     FROM (
         SELECT object_type, COUNT(*) cnt
         FROM all_objects
         GROUP BY object_type);
 
     AVG(CNT)
-------------
613.235294117

И даже так:

SQL> SELECT AVG(COUNT(*))
     FROM all_objects
     GROUP BY object_type;
 
AVG(COUNT(*))
-------------
613.235294117

Здесь вложенная агрегатная функция COUNT(*) работает с GROUP BY, чтобы произвести частные агрегатные значения в разрезе object_type, а внешняя AVG() - агрегирует частные значения в общее. Вложить агрегатные функции друг в друга глубже невозможно.

Это ограничение, конечно, не распространяется на вложение подзапросов с агрегатами:

SQL> -- общее количество доступных объектов
SQL> SELECT SUM(s)
     FROM (
         -- кол-во объектов в разрезе владельца
         SELECT owner, SUM(cnt) s
         FROM (
             -- кол-во объектов в разрезе владельца и типа
             SELECT owner, object_type, COUNT(*) cnt
             FROM all_objects
             GROUP BY owner, object_type)
         GROUP BY owner);
 
    SUM(S)
----------
     20850

Используя субагрегирование с GROUP BY, получим доли таблиц в объектах каждого владельца:

SQL> SELECT owner, COUNT(DECODE(object_type,'TABLE', 1, NULL))/COUNT(*) ratio
     FROM all_objects
     GROUP BY owner;
 
OWNER                               RATIO
------------------------------ ----------
APEX_040200                    0.15041597
MDSYS                          0.11532625
PUBLIC                                  0
OUTLN                               0.375
CTXSYS                         0.12894736
HR                             0.20588235
FLOWS_FILES                    0.09090909
SYSTEM                         0.30677290
APEX_040000                    0.15379061
XDB                            0.17099567
SYS                            0.10574354
AY                                    0.5
 
12 rows selected

Что, если нужно получить долю объектов каждого владельца в общем количестве объектов? В таком случае, помимо субагрегата в разрезе владельцев, нам нужен общий агрегат (по всем строкам):

SQL> SELECT owner, COUNT(*)/(SELECT COUNT(*) FROM all_objects) ratio
     FROM all_objects
     GROUP BY owner;
 
OWNER                               RATIO
------------------------------ ----------
APEX_040200                    0.14412470
MDSYS                          0.03160671
PUBLIC                         0.19247002
OUTLN                          0.00038369
CTXSYS                         0.01822541
HR                             0.00163069
FLOWS_FILES                    0.00052757
SYSTEM                         0.02407673
APEX_040000                    0.13285371
XDB                            0.02215827
SYS                            0.43088729
AY                             0.00105515
 
12 rows selected

Более эффективный и удобочитаемый вариант, представляющий одновременно общее и частные агрегатные значения, а также долю частных агрегатов в общем:

SQL> SELECT owner, cnt.total, COUNT(*) subtotal, ROUND(COUNT(*)/cnt.total, 4) ratio
     FROM all_objects,
         (SELECT COUNT(*) total FROM all_objects) cnt
     GROUP BY owner, cnt.total
     ORDER BY 2 DESC;
 
OWNER                               TOTAL   SUBTOTAL      RATIO
------------------------------ ---------- ---------- ----------
APEX_040000                         20850       2770     0.1329
APEX_040200                         20850       3005     0.1441
AY                                  20850         22     0.0011
CTXSYS                              20850        380     0.0182
FLOWS_FILES                         20850         11     0.0005
HR                                  20850         34     0.0016
MDSYS                               20850        659     0.0316
OUTLN                               20850          8     0.0004
PUBLIC                              20850       4013     0.1925
SYS                                 20850       8984     0.4309
SYSTEM                              20850        502     0.0241
XDB                                 20850        462     0.0222
 
12 rows selected

И еще вариация на ту же тему:

SQL> WITH
         t AS (
             SELECT COUNT(*) total
             FROM all_objects),
         s AS (
             SELECT object_type, COUNT(*) subtotal
             FROM all_objects
             GROUP BY object_type)
     SELECT
         s.object_type, ROUND(s.subtotal/t.total*100, 2) "Percent"
     FROM
         t CROSS JOIN s
     ORDER BY
           2 DESC;
 
OBJECT_TYPE            Percent
------------------- ----------
VIEW                     20.57
SYNONYM                  19.66
INDEX                    19.13
TABLE                    10.59
TYPE                      7.71
...
SCHEDULE                  0.01
DESTINATION               0.01
CONSUMER GROUP            0.01
RULE                         0
EDITION                      0
 
34 rows selected
 

Чтобы ограничить результат агрегирующего запроса только строками, которые нас интересуют, в нашем распоряжении имеется опция HAVING:

SQL> -- имеются ли в БД 5 или более пользователей, созданных в один день?
SQL> SELECT TRUNC(created), COUNT(*)
     FROM all_users
     GROUP BY TRUNC(created)
     HAVING COUNT(*) > 5;
 
TRUNC(CREATED)   COUNT(*)
-------------- ----------
27.08.2011             12

SQL> -- что это за пользователи?
SQL> SELECT *
     FROM all_users
     WHERE TRUNC(created) IN (
         SELECT TRUNC(created)
         FROM all_users
         GROUP BY TRUNC(created)
         HAVING COUNT(*) > 5)
     ORDER BY TRUNC(created);
 
USERNAME                          USER_ID CREATED
------------------------------ ---------- -----------
SYS                                     0 27.08.2011
MDSYS                                  42 27.08.2011
ANONYMOUS                              35 27.08.2011
XDB                                    34 27.08.2011
HR                                     43 27.08.2011
APEX_040000                            47 27.08.2011
APEX_PUBLIC_USER                       45 27.08.2011
FLOWS_FILES                            44 27.08.2011
XS$NULL                        2147483638 27.08.2011
SYSTEM                                  5 27.08.2011
CTXSYS                                 32 27.08.2011
OUTLN                                   9 27.08.2011
 
12 rows selected

Как известно, именно HAVING помогает отыскать строки с дублирующимися значениями столбцов, или сочетаниями значений. Так, проверить на дублирование сочетание значений столбцов A, B и C таблицы T можно следующим запросом:

SELECT a, b, c, COUNT(*)
FROM t
GROUP BY a, b, c
HAVING COUNT(*) > 1;

Если нам необходимы только строки с агрегатными значениями, большими (или меньшими), чем другое агрегатное значение, на помощь вновь приходит HAVING и подзапрос с агрегатной функцией:

SQL> -- владельцы объектов, у которых кол-во объектов больше среднего
SQL> SELECT owner, COUNT(*)
     FROM dba_objects
     GROUP BY owner
     HAVING COUNT(*) > (SELECT AVG(COUNT(*)) FROM dba_objects GROUP BY owner);
 
OWNER                            COUNT(*)
------------------------------ ----------
APEX_040200                          3203
PUBLIC                               4014
APEX_040000                          2955
SYS                                  9202

Для получения более широкой картины работы с агрегатами в СУБД Oracle см. мои статьи ROLLUP, CUBE и GROUPING SETS в Oracle 11gR2, Пользовательские агрегатные функции и Сводные таблицы в Oracle 11g.

Комментариев нет:

Отправить комментарий