【问题标题】:Is there any way to make this query a loop or more efficient?有没有办法让这个查询成为一个循环或更有效?
【发布时间】:2019-01-24 16:19:36
【问题描述】:

我有一个员工历史记录表,其中包含状态日期、状态和员工 ID。状态日期不在同一日期,有时可能会持续几个月而不更新。我需要创建一个报告,每月创建员工状态的每月快照。

目前,我正在选择 sysdate 月份开始之前的最大状态日期,并通过从查询中一次减去一个月来重复此查询,然后进行联合以合并所有这些日期。

SELECT id, 
       status_date, 
       status, 
       (trunc(sysdate, 'month')) AS Activity_Month
FROM empl_hist as e1 join
    (SELECT id, max(status_date) AS max_date, status_date FROM empl_hist 
     WHERE status_date <= (trunc(sysdate, 'month')) e2 
        on e1.id = e2.id and e1.status_date = e2.status_date

UNION ALL

SELECT id, 
    status_date, 
    status,
    (trunc(add_months(sysdate,-1), 'month')) AS Activity_Month,
FROM empl_hist as e1 join
    (SELECT id, max(status_date) AS max_date, status_date FROM empl_hist 
     WHERE status_date <= (trunc(add_months(sysdate,-1), 'month')) e2 
        on e1.id = e2.id and e1.status_date = e2.status_date

我需要这份过去 24 个月的报告,似乎应该有更好的方法来做这件事,而不是做 23 个工会。

【问题讨论】:

  • 样本数据和预期结果会有所帮助。
  • 这是一个无效的查询。 e2.status_date 不存在。你打算e2.max_date吗?
  • 抱歉,已更正该错误

标签: sql oracle


【解决方案1】:

您可以使用递归查询来生成您的报告月份,然后将其连接到您的 empl_hist 表并使用聚合函数来获得所需的结果:

WITH REPORT(ACTIVITY_MONTH) AS (
   SELECT CAST(ADD_MONTHS(TRUNC(SYSDATE,'month'),-24) AS DATE)
     FROM DUAL
  UNION ALL
   SELECT CAST(ADD_MONTHS(ACTIVITY_MONTH,1) AS DATE)
     FROM REPORT
    WHERE ADD_MONTHS(ACTIVITY_MONTH,1) < SYSDATE
)
SELECT eh.id
     , max(eh.status_date) status_Date
     , max(eh.status) keep (dense_rank first order by status_date desc) status
     , r.activity_month
  FROM REPORT r
  JOIN EMPL_HIST EH
    ON EH.STATUS_DATE <= r.ACTIVITY_MONTH
 group by eh.id, r.activity_month

【讨论】:

  • 加一个用于普及Recursive Subquery Factoring,即使它比经典 conenct by level 方法多出两倍以上的行数
  • 感谢 connect by level 曾经是无证的,因此标记使用风险自负,我不知道是否仍然如此。无论哪种方式,当Recursive Subquery Factoring 被添加到 Oracle 时,我都进行了切换。
【解决方案2】:

如果您在第一步中计算了有效期间隔,即最后一天(包括)状态有效 - status_date_to

如果status_date 被截断,即没有时间,则建议的计算有效。

如果有时间组件,请使用status_date - interval '1' second 而不是status_date - 1

您只加入EPML_HISTORY 中与报告月份匹配 的那些记录,即c.reporting_month between h.status_date and h.status_date_to

WITH calendar AS (
SELECT  trunc(add_months(sysdate, 1-level), 'MM') reporting_month
FROM    dual
CONNECT BY level <= 24
), hist as (
SELECT id, 
       status_date,
       nvl(lead(status_date-1) over (partition by id order by status_date),add_months(trunc(sysdate, 'MM'),1)) as status_date_to,
       status
from empl_hist)
select REPORTING_MONTH, ID, STATUS
from calendar c
join hist h on c.reporting_month between h.status_date and h.status_date_to
order by id, reporting_month;

REPORTING_MONTH             ID STATUS    
------------------- ---------- ----------
01.05.2018 00:00:00          1 active    
01.06.2018 00:00:00          1 active    
01.07.2018 00:00:00          1 active    
01.08.2018 00:00:00          1 active    
01.09.2018 00:00:00          1 deactive  
01.10.2018 00:00:00          1 deactive  
01.11.2018 00:00:00          1 deactive  
01.12.2018 00:00:00          1 deactive  
01.01.2019 00:00:00          1 active    
01.08.2018 00:00:00          2 active    
01.09.2018 00:00:00          2 active    
01.10.2018 00:00:00          2 active    
01.11.2018 00:00:00          2 deactive  
01.12.2018 00:00:00          2 deactive  
01.01.2019 00:00:00          2 deactive  
01.01.2019 00:00:00          4 active  

另请注意,您应该可以重新审视分配 报告月份status_date &lt;= (trunc(sysdate, 'month') 的逻辑。在下面的示例数据中,id=3 被忽略,因为它是在当月 5 日激活的,但 id=4 被报告是因为它是在当月 1 日激活的。

样本数据

drop table empl_hist;
create table empl_hist(
id number,
status_date date,
status varchar2(10));

insert into empl_hist values(1,DATE'2018-05-01','active');
insert into empl_hist values(1,DATE'2018-08-05','deactive');
insert into empl_hist values(1,DATE'2018-12-05','active');
insert into empl_hist values(2,DATE'2018-07-05','active');
insert into empl_hist values(2,DATE'2018-10-05','deactive');
insert into empl_hist values(3,DATE'2019-01-05','active');
insert into empl_hist values(4,DATE'2019-01-01','active');

【讨论】:

  • 这是一个很好的解决方案,但是,由于从 activity_date 中减去 1 并使用完全封闭的范围,它可能会丢失数据。例如如果 ID 6 有两条记录,其日期如下:2018-09-30 08:30:002018-11-01 16:30:00。在第一种情况下,status_to_date 将是2018-10-31 16:30:00,因此由于完全封闭的范围运营商BETWEEN,因此不会在 11 月的报告期间被选中,第二个也不会。而是使用半开放范围并且不要在前导操作中减去 1:status_date &lt;= reporting_month and reporting_month &lt; status_to_date
  • 您完全正确@Sentinel,建议的解决方案仅适用于截断的DATEs(如第二句所述)。对于带有时间分量的DATEs,您必须减去interval '1' second 才能安全地使用BETWEEN 谓词。但当然你可以切换到 semi open 间隔。一些纯粹主义者会声称您应该在NULL 上留下最后一个开放间隔,并相应地进行测试。写更长的谓词是一个品味和接受度的问题;)
  • 谢谢!今天学到了新东西。以前从未使用过connect。
  • @Bob 使用 Recursive Subquery Factoring 检查 Sentinel 的答案
【解决方案3】:

您可以使用子查询生成 24 个月的列表并将其加入。然后我会使用row_number 来确定窗口内的最新记录以避免自加入:

SELECT   id, status_date, status, Activity_Month
FROM     (
            SELECT     e.*,
                       m.Activity_Month,
                       row_number() OVER (PARTITION BY m.Activity_Month 
                                          ORDER BY e.status_date DESC) rn
            FROM       (
                          SELECT  trunc(add_months(sysdate, 1-level), 'month') Activity_Month
                          FROM    dual
                          CONNECT BY level <= 24
                       ) m
            INNER JOIN empl_hist e
                    ON e.status_date <= m.Activity_Month
         ) base
WHERE    rn = 1
ORDER BY Activity_Month

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-28
    • 2022-01-18
    • 2022-11-03
    • 1970-01-01
    • 2015-08-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多