部分非正规化的独角兽观察

时间:2012-10-04 09:01:06

标签: sql oracle gaps-and-islands

有许多研究人员在unicorns 1 上观察世界上最后剩下的Easter Island。每天研究人员都会记录他们看到的独角兽,目击日期,每只独角兽的婴儿数量,以及在发生瞄准时他们是否喝醉了。它们被单独上传到一个中心位置,然后每天向我发出一个平面文件,显示所有新观察结果。

我有一个包含这些信息的表:

create table unicorn_observations (
     observer_id number not null
   , unicorn_id number not null
   , created date not null -- date the record was inserted into the database
   , lastseen date not null -- date the record was last seen
   , observation_date date not null
   , no_of_babies number not null
   , drunk varchar2(1) not null
   , constraint pk_uo primary key ( observer_id, unicorn_id, created )
   , constraint chk_uo_babies check ( no_of_babies >= 0 )
   , constraint chk_uo_drunk check ( drunk in ('y','n') )
     );

该表在observer_idunicorn_idobservation_datelastseen上分别是唯一的。

有时,管理数据输出的Cobold [sic]会略微出错,并重新输出两次相同的数据。在这种情况下,我更新lastseen而不是创建新记录。我在每列相同的情况下执行此操作

不幸的是,研究人员并不完全了解第三种正常形式。他们每个月都会上传几个独角兽的前几个月的观察结果,即使没有新的观察结果。他们使用 new observation_date执行此操作,这意味着新记录会插入到表中。

我有一个单独的createdlastseen用于完全可追溯性,因为研究人员有时会提交一些观察结果。这些是由数据库创建的,不是提交信息的一部分。

以下是一些示例数据(部分更改了列名称,以便在没有滚动条的情况下使其适合)。

+--------+--------+-----------+-----------+-----------+---------+-------+
| OBS_ID | UNI_ID |  CREATED  | LASTSEEN  | OBS_DATE  | #BABIES | DRUNK |
+--------+--------+-----------+-----------+-----------+---------+-------+
|      1 |      1 | 01-NOV-11 | 01-NOV-11 | 31-OCT-11 |      10 | n     |
|      1 |      2 | 01-NOV-11 | 01-NOV-11 | 31-OCT-11 |      10 | n     |
|      1 |      3 | 01-NOV-11 | 01-NOV-11 | 31-OCT-11 |      10 | n     |
|      1 |      6 | 10-NOV-11 | 10-NOV-11 | 07-NOV-11 |       0 | n     |
|      1 |      1 | 17-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     |
|      1 |      2 | 17-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     |
|      1 |      3 | 17-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     |
|      1 |      6 | 17-NOV-11 | 17-NOV-11 | 17-NOV-11 |       0 | n     |
|      1 |      6 | 01-DEC-11 | 01-DEC-11 | 01-DEC-11 |       0 | n     |
|      1 |      6 | 01-JAN-12 | 01-JAN-12 | 01-JAN-12 |       3 | n     |
|      1 |      6 | 01-FEB-12 | 01-FEB-12 | 01-FEB-12 |       0 | n     |
|      1 |      6 | 01-MAR-12 | 01-MAR-12 | 01-MAR-12 |       0 | n     |
|      1 |      6 | 01-APR-12 | 01-APR-12 | 01-APR-12 |       0 | n     |
|      1 |      1 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |
|      1 |      2 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |
|      1 |      3 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |
|      1 |      6 | 01-MAY-12 | 01-MAY-12 | 01-MAY-12 |       0 | n     |
+--------+--------+-----------+-----------+-----------+---------+-------+

我想对这些观察结果进行部分非规范化处理,以便在收到的新记录中使用相同的observer_idunicorn_idno_of_babiesdrunk(有效负载)但使用较新的 observation_date我更新表last_observation_date中的新列,而不是插入新记录。在这种情况下,我仍会更新lastseen

我需要这样做,因为我有很多复杂的独角兽相关查询加入到这个表中;研究人员每月大约10万次上传旧日期的旧观察结果,我每月收到大约9m的新记录。我已经跑了一年,已经有225米的独角兽观察。因为我只需要知道每个有效负载组合的最后观察日期,我宁愿大量减小表格的大小,并节省自己大量时间进行全扫描。

这意味着该表将成为:

create table unicorn_observations (
     observer_id number not null
   , unicorn_id number not null
   , created date not null -- date the record was inserted into the database
   , lastseen date not null -- date the record was last seen
   , observation_date date not null
   , no_of_babies number not null
   , drunk varchar2(1) not null
   , last_observation_date date
   , constraint pk_uo primary key ( observer_id, unicorn_id, created )
   , constraint chk_uo_babies check ( no_of_babies >= 0 )
   , constraint chk_uo_drunk check ( drunk in ('y','n') )
     );

并且表中存储的数据如下所示;如果观察仅被“看到”一次,last_observation_date是否为空并不重要。我在加载数据时不需要帮助,只是将当前表的部分非规范化看起来像这样。

+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
| OBS_ID | UNI_ID |  CREATED  | LASTSEEN  | OBS_DATE  | #BABIES | DRUNK | LAST_OBS_DT |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
|      1 |      6 | 10-NOV-11 | 01-DEC-11 | 07-NOV-11 |       0 | n     | 01-DEC-11   |
|      1 |      1 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      2 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      3 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      6 | 01-JAN-12 | 01-JAN-12 | 01-JAN-12 |       3 | n     |             |
|      1 |      6 | 01-FEB-12 | 01-MAY-12 | 01-FEB-12 |       0 | n     | 01-MAY-12   |
|      1 |      1 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |             |
|      1 |      2 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |             |
|      1 |      3 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     |             |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+

显而易见的答案

select observer_id as obs_id
     , unicorn_id as uni_id
     , min(created) as created
     , max(lastseen) as lastseen
     , min(observation_date) as obs_date
     , no_of_babies as "#BABIES"
     , drunk
     , max(observation_date) as last_obs_date
  from unicorn_observations
 group by observer_id
        , unicorn_id
        , no_of_babies
        , drunk

不起作用,因为它忽略了2012年1月1日 st 对3只独角兽婴儿的单独观察;这反过来意味着11月10日 th 创建的记录的lastseen不正确。

+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
| OBS_ID | UNI_ID |  CREATED  | LASTSEEN  | OBS_DATE  | #BABIES | DRUNK | LAST_OBS_DT |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
|      1 |      1 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      2 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      3 | 01-NOV-11 | 17-NOV-11 | 09-APR-11 |      10 | n     | 31-OCT-11   |
|      1 |      6 | 10-NOV-11 | 01-MAY-12 | 07-NOV-11 |       0 | n     | 01-MAY-12   |
|      1 |      6 | 01-JAN-12 | 01-JAN-12 | 01-JAN-12 |       3 | n     | 01-JAN-12   |
|      1 |      1 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     | 19-APR-12   |
|      1 |      2 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     | 19-APR-12   |
|      1 |      3 | 19-APR-12 | 19-APR-12 | 19-APR-12 |       7 | y     | 19-APR-12   |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+

如果没有一些程序逻辑,我现在看不到这样做的方法,即循环。我宁愿在这种情况下避免循环,因为我必须完全扫描225米行表260次(不同created日期的数量)。即使使用lag()lead()也需要递归,因为每个独角兽有不确定的观察量。

有没有办法在单个SQL语句中创建此数据集?

表规范和示例数据也在SQL Fiddle


尝试了更好的解释:

当某些事情属实时,问题是维持。 2012年1月1日,独角兽6号有3个婴儿。

查看GROUP BY创建的“表格”中的独角兽6;如果我试图在1月份的1 st 上找到婴儿的数量,我会得到两条记录,这是一个矛盾。

+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
| OBS_ID | UNI_ID |  CREATED  | LASTSEEN  | OBS_DATE  | #BABIES | DRUNK | LAST_OBS_DT |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
|      1 |      6 | 10-NOV-11 | 01-MAY-12 | 07-NOV-11 |       0 | n     | 01-MAY-12   |
|      1 |      6 | 01-JAN-12 | 01-JAN-12 | 01-JAN-12 |       3 | n     | 01-JAN-12   |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+

但是,我只想要一行,就像在第二个表中一样。在这里,对于任何时间点,最多只有一个“正确”值,因为独角兽6有0个婴儿的两个时间段在它有3个的那天被分成两行。

+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
| OBS_ID | UNI_ID |  CREATED  | LASTSEEN  | OBS_DATE  | #BABIES | DRUNK | LAST_OBS_DT |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+
|      1 |      6 | 10-NOV-11 | 01-DEC-11 | 07-NOV-11 |       0 | n     | 01-DEC-11   |
|      1 |      6 | 01-JAN-12 | 01-JAN-12 | 01-JAN-12 |       3 | n     |             |
|      1 |      6 | 01-FEB-12 | 01-MAY-12 | 01-FEB-12 |       0 | n     | 01-MAY-12   |
+--------+--------+-----------+-----------+-----------+---------+-------+-------------+

<子> 1。在moai周围放牧

3 个答案:

答案 0 :(得分:2)

试试这个。

with cte as
(
    select v.*,  ROW_NUMBER() over (partition by grp, unicorn_id order by grp, unicorn_id) rn
    from
    (
        select u.*, 
            ROW_NUMBER() over (partition by unicorn_id order by no_of_babies, drunk, created )
            -ROW_NUMBER() over (partition by unicorn_id order by created) as grp
        from unicorn_observations u
    ) v
) 
    select 
        observer_id, cte.unicorn_id, mincreated,maxlastseen,minobsdate,no_of_babies,drunk,maxobsdate
    from cte 
        inner join 
        (    
            select 
                unicorn_id, grp, 
                min(created) as mincreated,
                max(lastseen) as maxlastseen, 
                min(observation_date) as minobsdate,
                max(observation_date) as maxobsdate
            from cte 
            group by unicorn_id, grp
        ) v
        on cte.grp = v.grp
        and cte.unicorn_id = v.unicorn_id
    where rn=1  
    order by created;

答案 1 :(得分:1)

基于我认为您正在尝试做的事情,主要是关于与unicorn 6的具体问题的更新,我认为这会得到您想要的结果。它不需要递归leadlag,但需要两个级别。

select *
from (
    select observer_id, unicorn_id,
        case when first_obs_dt is null then created
            else lag(created) over (order by rn) end as created,
        case when last_obs_dt is null then lastseen
            else lead(lastseen) over (order by rn) end as lastseen,
        case when first_obs_dt is null then observation_date
            else lag(observation_date) over (order by rn)
            end as observation_date,
        no_of_babies,
        drunk,
        case when last_obs_dt is null then observation_date
            else null end as last_obs_dt
    from (
        select observer_id, unicorn_id, created, lastseen, 
            observation_date, no_of_babies, drunk,
            case when lag_no_babies != no_of_babies or lag_drunk != drunk
                or lag_obs_dt is null then null
                else lag_obs_dt end as first_obs_dt,
            case when lead_no_babies != no_of_babies or lead_drunk != drunk
                or lead_obs_dt is null then null
                else lead_obs_dt end as last_obs_dt,
            rownum rn
        from (
            select observer_id, unicorn_id, created, lastseen,
                observation_date, no_of_babies, drunk,
                lag(observation_date)
                    over (partition by observer_id, unicorn_id, no_of_babies,
                            drunk
                        order by observation_date) lag_obs_dt,
                lag(no_of_babies)
                    over (partition by observer_id, unicorn_id, drunk
                        order by observation_date) lag_no_babies,
                lag(drunk)
                    over (partition by observer_id, unicorn_id, no_of_babies
                        order by observation_date) lag_drunk,
                lead(observation_date)
                    over (partition by observer_id, unicorn_id, no_of_babies,
                        drunk
                    order by observation_date) lead_obs_dt,
                lead(no_of_babies)
                    over (partition by observer_id, unicorn_id, drunk
                        order by observation_date) lead_no_babies,
                lead(drunk)
                    over (partition by observer_id, unicorn_id, no_of_babies
                        order by observation_date) lead_drunk
            from unicorn_observations
            order by 1,2,5
        )
    )
    where first_obs_dt is null or last_obs_dt is null
)
where last_obs_dt is not null
order by 1,2,3,4;

给出了:

OBSERVER_ID UNICORN_ID CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D LAST_OBS_
----------- ---------- --------- --------- --------- ------------ - ---------
          1          1 17-NOV-11 01-NOV-11 09-APR-11           10 n 31-OCT-11
          1          1 19-APR-12 19-APR-12 19-APR-12            7 y 19-APR-12
          1          2 17-NOV-11 01-NOV-11 09-APR-11           10 n 31-OCT-11
          1          2 19-APR-12 19-APR-12 19-APR-12            7 y 19-APR-12
          1          3 17-NOV-11 01-NOV-11 09-APR-11           10 n 31-OCT-11
          1          3 19-APR-12 19-APR-12 19-APR-12            7 y 19-APR-12
          1          6 10-NOV-11 01-DEC-11 07-NOV-11            0 n 01-DEC-11
          1          6 01-JAN-12 01-JAN-12 01-JAN-12            3 n 01-JAN-12
          1          6 01-FEB-12 01-MAY-12 01-FEB-12            0 n 01-MAY-12

9 rows selected.

它有麒麟6的三个记录,但第三个的lastseenobservation_date与你的样本相反,所以我不确定我是否仍然不理解那。我假设您希望在每个分组中保留最早的observation_date和最新lastseen,理由是它在添加新记录时会发生什么,但我不确定。 ..

因此,最里面的查询从表中获取原始数据,并为lead lag获取observation_dateno_of_babiesdrunk列使用略有不同的分区。 order byrownum以后可以使用的CREATED LASTSEEN OBSERVATI NO_OF_BABIES D LAG_OBS_D LAG_NO_BABIES L LEAD_OBS_ LEAD_NO_BABIES L --------- --------- --------- ------------ - --------- ------------- - --------- -------------- - 10-NOV-11 10-NOV-11 07-NOV-11 0 n 17-NOV-11 0 n 17-NOV-11 17-NOV-11 17-NOV-11 0 n 07-NOV-11 0 n 01-DEC-11 0 n 01-DEC-11 01-DEC-11 01-DEC-11 0 n 17-NOV-11 0 n 01-FEB-12 3 n 01-JAN-12 01-JAN-12 01-JAN-12 3 n 0 0 01-FEB-12 01-FEB-12 01-FEB-12 0 n 01-DEC-11 3 n 01-MAR-12 0 n 01-MAR-12 01-MAR-12 01-MAR-12 0 n 01-FEB-12 0 n 01-APR-12 0 n 01-APR-12 01-APR-12 01-APR-12 0 n 01-MAR-12 0 n 01-MAY-12 0 n 01-MAY-12 01-MAY-12 01-MAY-12 0 n 01-APR-12 0 n ,在下一步中获得并用于之后的序列中。仅为了简洁的独角兽6:

lead

如果lagobservation_date值发生了变化,则下一级会消除num_of_babies的{​​{1}}和drunk值 - 您只是特别提及分裂婴儿数,但我认为你也想分开清醒。在此之后,nullfirst_obs_date last_obs_date的任何内容都是小范围的开头或结尾。

CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D FIRST_OBS LAST_OBS_         RN
--------- --------- --------- ------------ - --------- --------- ----------
10-NOV-11 10-NOV-11 07-NOV-11            0 n           17-NOV-11          1
17-NOV-11 17-NOV-11 17-NOV-11            0 n 07-NOV-11 01-DEC-11          2
01-DEC-11 01-DEC-11 01-DEC-11            0 n 17-NOV-11                    3
01-JAN-12 01-JAN-12 01-JAN-12            3 n                              4
01-FEB-12 01-FEB-12 01-FEB-12            0 n           01-MAR-12          5
01-MAR-12 01-MAR-12 01-MAR-12            0 n 01-FEB-12 01-APR-12          6
01-APR-12 01-APR-12 01-APR-12            0 n 01-MAR-12 01-MAY-12          7
01-MAY-12 01-MAY-12 01-MAY-12            0 n 01-APR-12                    8

现在可以忽略小范围的开始或结束的任何内容,因为这些值与之前或之后的值相同或被其取代。这涉及不确定数量的观测问题 - 此时您忽略了多少并不重要。因此,下一级通过过滤first_obs_dtlast_obs_dt都为非空的行来消除这些中间值。在该过滤集合中,有第二层leadlag来获取每个日期的第一个或最后一个值 - 这就是我不确定是否正确,因为它与你的样品。

CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D LAST_OBS_
--------- --------- --------- ------------ - ---------
10-NOV-11 01-DEC-11 07-NOV-11            0 n
10-NOV-11 01-DEC-11 07-NOV-11            0 n 01-DEC-11
01-JAN-12 01-JAN-12 01-JAN-12            3 n 01-JAN-12
01-FEB-12 01-MAY-12 01-FEB-12            0 n
01-FEB-12 01-MAY-12 01-FEB-12            0 n 01-MAY-12

最后,过滤掉没有last_obs_dt的剩余行。

现在我等着看我误解了哪个位...... * 8-)


在对leadlag排序进行更正后,针对独角兽1的每个阶段的相同信息:

CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D LAG_OBS_D LAG_NO_BABIES L LEAD_OBS_ LEAD_NO_BABIES L
--------- --------- --------- ------------ - --------- ------------- - --------- -------------- -
17-NOV-11 17-NOV-11 09-APR-11           10 n                           31-OCT-11             10 n
01-NOV-11 01-NOV-11 31-OCT-11           10 n 09-APR-11            10 n
19-APR-12 19-APR-12 19-APR-12            7 y

CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D FIRST_OBS LAST_OBS_         RN
--------- --------- --------- ------------ - --------- --------- ----------
17-NOV-11 17-NOV-11 09-APR-11           10 n           31-OCT-11          1
01-NOV-11 01-NOV-11 31-OCT-11           10 n 09-APR-11                    2
19-APR-12 19-APR-12 19-APR-12            7 y                              3

CREATED   LASTSEEN  OBSERVATI NO_OF_BABIES D LAST_OBS_
--------- --------- --------- ------------ - ---------
17-NOV-11 17-NOV-11 09-APR-11           10 n 09-APR-11
19-APR-12 19-APR-12 19-APR-12            7 y 19-APR-12

我不确定保存的observation_datelastseen在原始数据是否按此顺序输入时会发生什么声音,或者在添加新记录的情况下您会做什么在将来。

答案 2 :(得分:0)

这种类型的问题可以通过首先在子查询中创建一些标志然后使用它们来解决。

with obs_flags as (
    select 
       observer_id as obs_id
     , unicorn_id as uni_id
     , case when lag(observation_date) over (
           partition by unicorn_id, no_of_babies, drunk
           order by unicorn_id, observation_date
       ) is null then 1 else 0 end as group_start
     , case when lead(observation_date) over (
           partition by unicorn_id, no_of_babies,drunk
           order by unicorn_id, observation_date
       ) is null then 1 else 0 end as group_end
     , observation_date
     , no_of_babies
     , drunk
     , lastseen
     , created
  from unicorn_observations
)
select obs_start.obs_id
     , obs_start.uni_id
     , obs_start.created
     , obs_end.lastseen as lastseen
     , obs_start.observation_date
     , obs_start.no_of_babies as "#BABIES"
     , obs_start.drunk
     , obs_end.observation_date as last_obs_date
  from obs_flags obs_start
  join obs_flags obs_end on 
      obs_start.group_start = 1 and
      obs_end.group_end = 1 and
      obs_start.uni_id = obs_end.uni_id and
      obs_start.no_of_babies = obs_end.no_of_babies and
      obs_start.drunk = obs_end.drunk and
      obs_start.observation_date <= obs_end.observation_date and
      --Only join with the first end point we find:
      not exists (
          select * from obs_flags f where
              obs_start.uni_id = f.uni_id and
              obs_start.no_of_babies = f.no_of_babies and
              obs_start.drunk = f.drunk and
              f.group_end = 1 and
              f.observation_date < obs_end.observation_date and
              f.observation_date >= obs_start.observation_date
      );

这是一个复杂的问题;我可能还没有完全满足你的要求(或者那里可能有拼写错误。我没有Oracle来测试它)。但是,它应该让你知道如何做到这一点。

基本上,您首先会找到您感兴趣的期间的所有开始和结束记录。然后您将每个开始记录加入到同一分组中的下一个结束记录中。

更新:我的原始代码没有检查开始后是否结束了。我解决了这个问题。

Ben指出,

Update2:not exists子句在这里会很慢。另一个帮助我加快速度的方法是分两步完成:先找到所有可能的配对,然后分别选择正确的配对。

在这种情况下,在临时表或子查询中,将每个obs_start加入到每个可能正确的obs_end

然后,在这些配对中,为每个obs_end选择最早obs_start的配对。