【ORACLE】收集一些较为少见但很有用的SQL函数及写法.part5

接上篇
【ORACLE】收集一些较为少见但很有用的SQL函数及写法.part4


29.ANY_VALUE

作用:作为聚合函数,返回任意一个值(19c版本才添加的)。
比如常见的一个查询,关联商品基础信息表和销售明细表,对销售明细进行汇总,然后同时又要显示商品编码和商品名称,大多数情况下都是这么写的

select a.item_id,a.item_name,sum(b.sale) 
from item_info a,sale_detail b
where a.item_id=b.item_id
group by a.item_id,a.item_name

但是我们明知在商品信息表中,商品编码是唯一键,一个商品编码肯定只会对应一个商品名称,何必要再group by商品名称呢,这样不会浪费oracle的性能么?
所以,这个sql有了以下变体

select a.item_id,max(a.item_name),sum(b.sale) 
from item_info a,sale_detail b
where a.item_id=b.item_id
group by a.item_id

这样效率会比上面快不少,但是,max这个动作还是进行了一些计算,所以就有了any_value这个函数,这种情况下,使用any_value速度会更快,因为它不需要进行计算,读到哪条就是哪条

select a.item_id,any_value(a.item_name),sum(b.sale) 
from item_info a,sale_detail b
where a.item_id=b.item_id
group by a.item_id

30.APPROX_RANK/APPROX_SUM/APPROX_COUNT

这几个函数带上了approx前缀,所以他们分别是RANK(排行)、sum(求和)、count(计数)这几个函数对应的近似函数,但是实际使用又有很大的区别。
首先,使用这几个函数时必须有下面这一段

GROUP BY ...
HAVING 
  APPROX_RANK (... order by ) [<>=] ?    

这是因为,这几个函数存在的意义就是,在做聚合统计数据时,我们有时候只想看到排行前几名的统计值,而常规写法就是先对所有数据进行聚合,然后排名,再根据排名过滤,这样实际上有大量的计算并不会在最终的结果体现,我们也并不会关心前几名以外的统计数据。

而此函数用近似的算法,先用having的条件把基础数据过滤掉,再进行统计,效率快了很多

SELECT department_id, job_id, 
       APPROX_SUM(salary) 
FROM   hr.employees
GROUP BY department_id, job_id
HAVING 
  APPROX_RANK ( 
  PARTITION BY department_id 
  ORDER BY APPROX_SUM(salary) 
  DESC ) <= 10;

比如此例就是每个department_id 合计salary排行前十的job_id,对应的每个job_id的salary合计

参考
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/APPROX_SUM.html
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/APPROX_COUNT.html
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/APPROX_RANK.html


31.APPROX_MEDIAN

这个实际上是MEDIAN的近似函数,因为上面3个用法差异太大,和这个不是一个套路,所以分开来说

SELECT department_id "Department",
       APPROX_MEDIAN(salary) "Median Salary"
  FROM hr.employees
  GROUP BY department_id
  ORDER BY department_id;

看上去用法几乎和MEDIAN完全一样,其实还有点区别,如下

  SELECT department_id "Department",
         MEDIAN(salary) "MEDIAN",
         APPROX_MEDIAN(salary) "APPROX_MEDIAN",
         APPROX_MEDIAN(salary DETERMINISTIC) "APPROX_MEDIAN_DET ",
         APPROX_MEDIAN(salary , 'ERROR_RATE') "ERROR_RATE",
         APPROX_MEDIAN(salary DETERMINISTIC, 'ERROR_RATE') "ERROR_RATE_DET",
         APPROX_MEDIAN(salary , 'CONFIDENCE') "CONFIDENCE",
         APPROX_MEDIAN(salary DETERMINISTIC, 'CONFIDENCE') "CONFIDENCE_DET"
    FROM hr.employees
   GROUP BY department_id
   ORDER BY department_id;

这里出现了 DETERMINISTIC 、ERROR_RATE 、CONFIDENCE 三个词,其中
加DETERMINISTIC的时候,统计的列只能为数字或者能to_number成数字的,如果是要取非数字的中位值,比如日期,就不能加上 DETERMINISTIC;
ERROR_RATE意为错误率,返回的值表示这种近似统计的中位值错误率大概为多少
CONFIDENCE意为置信度,返回的值表示这种近似统计的可信度有多高,其实满足 置信度+错误率=1

实际上ERROR_RATE和CONFIDENCE在其他近似函数中也有些是可以使用的,这里就不展开了。

这里提醒一下,尽管ORACLE官方希望大家优先使用近似函数,但是以我实测的经验来看,小量的数据尽量不要用近似函数,由于样本数不足的原因,偏差会太大,这个时候错误率和置信度都不靠谱

 SELECT department_id "Department",
         MEDIAN(salary) "MEDIAN",
         APPROX_MEDIAN(salary DETERMINISTIC) "APPROX_MEDIAN_DET ",
         APPROX_MEDIAN(salary DETERMINISTIC, 'ERROR_RATE') "ERROR_RATE_DET",
         APPROX_MEDIAN(salary DETERMINISTIC, 'CONFIDENCE') "CONFIDENCE_DET",
        round( (APPROX_MEDIAN(salary DETERMINISTIC) -MEDIAN(salary))/MEDIAN(salary),2) e
    FROM hr.employees
   GROUP BY department_id
   ORDER BY department_id;
Department MEDIAN APPROX_MEDIAN_DET ERROR_RATE_DET CONFIDENCE_DET E
10 4400 4400 0.01 0.99 0
20 9500 6000 0.03 0.99 -0.37
30 2850 2800 0.03 0.99 -0.02
40 6500 6500 0.01 0.99 0
50 3100 3100 0.02 0.99 0
60 4800 4800 0.02 0.99 0
70 10000 10000 0.01 0.99 0
80 8900 8800 0.03 0.99 -0.01
90 17000 17000 0.03 0.99 0
100 8000 7800 0.02 0.99 -0.03
110 10154 8300 0.02 0.99 -0.18
7000 7000 0.01 0.99 0

加了一列手动计算的差异百分比,可以明显看到 37% 和函数提供的 3% 相差巨大。

参考
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/APPROX_MEDIAN.html


31.NEXT_DAY

作用:返回指定日期的下个指定星期几是哪一天

  select next_day(date '2021-10-08', '星期一'),
         next_day(date '2021-10-08', 2)
    from dual;
NEXT_DAY(DATE’2021-10-08’,‘星期一’) NEXT_DAY(DATE’2021-10-08’,2)
2021-10-11 2021-10-11

第二个参数支持2种表达形式,
第一种是该数据库会话环境对应的星期描述,可能是中文也可能是英文,如果是中文日期环境输入英文描述就会报错,反之亦然。
第二种是一个星期的第几天,星期天是一个星期的第一天,所以传2就是代表星期一了

为了代码兼容性,一般建议不要使用星期描述,应该采用直接传数字的方式,如果一定要传描述,那么建议做如下操作


declare
  l_tmp varchar2(200);
  L date;
begin
  --获取当前会话的日期语言
  select value
    into l_tmp
    from v$nls_parameters H
   WHERE H.PARAMETER = 'NLS_DATE_LANGUAGE';
   --修改成AMERICAN
  execute immediate q'{ALTER SESSION SET NLS_DATE_LANGUAGE='AMERICAN'}';
  --使用英文星期描述进行操作
  select next_day(date '2021-10-08', 'MONDAY') INTO L from dual;
  --将当前会话的日期语言还原
  execute immediate q'{ALTER SESSION SET NLS_DATE_LANGUAGE='}' || l_tmp || '''';
end;

32.NULLIF

作用:比较传入的两个参数,如果相等则返回空,如果不等则返回第一个参数
等价于 CASE WHEN expr1 = expr2 THEN NULL ELSE expr1 END

看上去很难想到使用场景,ORACLE官方举个例子,假设我们要出一张报表,要求给出每个人在本次公司组织架构调整后,之前的职位和当前的职位,如果职位没有调整,则之前的职位或者新的职位显示为空,

SELECT e.last_name, NULLIF(j.job_id, e.job_id) "Old Job ID"
  FROM employees e, job_history j
  WHERE e.employee_id = j.employee_id
  ORDER BY last_name, "Old Job ID";

但是这貌似还是不太实用,那么我举另一个例子,
我们偶尔会要出一张存在多个维度的报表,比如

省区 门店 昨日销售额
湖南省区 分店1 10000
湖南省区 分店2 10000
湖南省区 分店3 10000
四川省区 分店4 10000
四川省区 分店5 10000

前面省区会出现重复的值,在报表中,用户认为只有一个值就够了,其他为空,即类似于透视表合并单元格的效果,像下面这样

省区 门店 昨日销售额
湖南省区 分店1 10000
分店2 10000
分店3 10000
四川省区 分店4 10000
分店5 10000

这个时候NULLIF就有用了,按照官方的例子,一般会局限于一行内的两个值,其实oracle是有函数获取上面一个值或者下面一个值的,即使用开窗函数

select nullif(EMPLOYEE_ID, lag(EMPLOYEE_ID) over(order by EMPLOYEE_ID)) "nullif",
       a.EMPLOYEE_ID
  from hr.job_history a
nullif EMPLOYEE_ID
101 101
101
102 102
114 114
122 122
176 176
176
200 200
200
201 201

33.RATIO_TO_REPORT

作用:返回每行该值占此列的百分比

一般情况下,我们计算占比的时候,都是先算好汇总值,然后再用每行的数据去除这个汇总,实际上对此表进行两次查询,如果使用RATIO_TO_REPORT则只需要查一次

SELECT last_name,
       salary,
       round(RATIO_TO_REPORT(salary) OVER(), 4) * 100 || '%' AS rr
  FROM hr.employees
 WHERE job_id = 'PU_CLERK'
 ORDER BY last_name, salary, rr
LAST_NAME SALARY RR
Baida 2900 20.86%
Colmenares 2500 17.99%
Himuro 2600 18.71%
Khoo 3100 22.3%
Tobias 2800 20.14%

另外,此函数由于是over分析函数,所以同样支持partition by 子句来进行分组求占比。

有举一反三习惯的伙伴们这个时候可能会有点难受,从这个函数名 ratio_to_report来看,ratio意思就是占比了,report是报表或者报告,但一个报告肯定不会局限于看占比,那有没有其他的 to_report函数呢?
答案是:目前没有!
所以这个函数起名就很怪异了。。。


34.SOUNDEX

作用:输入任何英文字符串,返回四个字符,作为一个相似发音的代码

select soundex('an apple'),soundex('an ipol') from dual;
SOUNDEX(‘ANAPPLE’) SOUNDEX(‘ANIPOL’)
A514 A514

这四个字符中,第一位其实是输入字符串的第一个字母。这个函数其实就是用来比较两个字符串发音是否类似的。

官方给的例子是这样的

SELECT last_name, first_name
     FROM hr.employees
     WHERE SOUNDEX(last_name)
         = SOUNDEX('SMYTHE')
     ORDER BY last_name, first_name;

LAST_NAME  FIRST_NAME
---------- ----------
Smith      Lindsey
Smith      William

想象下,如果和语音识别及人工智能结合,用人声发出自然语言指令,比如说“告诉我,我们这里有几个叫史密斯的”,语音识别部分转化出一个句子,然后openai转化成查询sql,但是可能存在发音和史密斯很像但又不是史密斯这几个字的,比如斯迷斯之类的,这样就可以结合这个soundex函数找到更多“叫”史密斯的了


35.STANDARD_HASH

其实前面有提到hash,但是这个STANDARD_HASH函数用起来更简单,不用转来转去

select STANDARD_HASH('123','MD5') FROM DUAL;
STANDARD_HASH(‘123’,‘MD5’)
202CB962AC59075B964B07152D234B70

呵,熟悉的“202C”,不过别太高兴,这个函数是12C版本才添加的,11g及以下没有


36.WIDTH_BUCKET

作用:设定一个等距的组,返回传入参数所在的组数。

SELECT a.FIRST_NAME,
       a.SALARY,
       WIDTH_BUCKET(a.SALARY, 7000, 10000, 2) sa_group
  FROM hr.employees a
 where a.department_id = 100
 order by SALARY
FIRST_NAME SALARY SA_GROUP
Luis 6900 0
Ismael 7700 1
Jose Manuel 7800 1
John 8200 1
Daniel 9000 2
Nancy 12008 3

第一个参数是要分组的值,第二个是分组的最小值,第三个是分组的最大值,第四个是要分几组,如果超出分组范围,比如实际值小于设定的最小值,那么就会归到第0个组去,如果大于,则分到设定组数加1的那个组去。

这里开下脑洞,有个常见的需求场景,要查出多个区间的数据,是不是想起被一堆case when支配的恐惧?后来会了pivot后,就尝试先用除法来做一个分组列,再用pivot给转换上去,但是列数却不能确定,因为简单除法只能实现等距,最大分多少组没有定。

所以这个时候,WIDTH_BUCKET这个函数的出现就解决了这个痛点。

这里我们尝试用常见的函数实现一下WIDTH_BUCKET的效果,方便理解,假设传入的4个参数分别为
expr, min_value, max_value, num_buckets

case
         when :expr < :min_valuethen
          0
         when :expr > :max_valuethen
          :num_buckets + 1
         else
          trunc((:expr - :min_value+1) / ((:max_value- :min_value+ 1) / :num_buckets)) + 1
       end

参考
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/WIDTH_BUCKET

おすすめ

転載: blog.csdn.net/wwwwwwgame/article/details/120676775