简体   繁体   中英

Oracle 11g Selecting from N to 1 table to a common table to an 1 to N table, with a restriction

Problem

I have these tables and data:

CREATE TABLE a (
    id   NUMBER(3) NOT NULL
);

ALTER TABLE a ADD CONSTRAINT a_pk PRIMARY KEY ( id );

CREATE TABLE b (
    id   NUMBER(6) NOT NULL,
    year NUMBER(4) NOT NULL,
    a_id NUMBER(3) NOT NULL
);

ALTER TABLE b ADD CONSTRAINT b_pk PRIMARY KEY ( id );

CREATE TABLE c (
    id         NUMBER(9) NOT NULL,
    start_date DATE NOT NULL,
    end_date   DATE NOT NULL,
    a_id       NUMBER(3) NOT NULL
);

ALTER TABLE c ADD CONSTRAINT c_pk PRIMARY KEY ( id );

ALTER TABLE b
    ADD CONSTRAINT b_a_fk FOREIGN KEY ( a_id )
        REFERENCES a ( id );

ALTER TABLE c
    ADD CONSTRAINT c_a_fk FOREIGN KEY ( a_id )
        REFERENCES a ( id );

Insert into A (ID) values ('1');
Insert into A (ID) values ('2');
Insert into A (ID) values ('3');

Insert into B (ID,YEAR,A_ID) values ('1','2017','1');
Insert into B (ID,YEAR,A_ID) values ('2','2017','2');
Insert into B (ID,YEAR,A_ID) values ('3','2013','3');
Insert into B (ID,YEAR,A_ID) values ('4','2014','3');
Insert into B (ID,YEAR,A_ID) values ('5','2017','3');
Insert into B (ID,YEAR,A_ID) values ('6','2013','1');

Insert into C (ID,START_DATE,END_DATE,A_ID) values ('1',to_date('01/01/13','DD/MM/RR'),to_date('01/01/14','DD/MM/RR'),'3');
Insert into C (ID,START_DATE,END_DATE,A_ID) values ('2',to_date('01/06/17','DD/MM/RR'),to_date('01/01/18','DD/MM/RR'),'3');
Insert into C (ID,START_DATE,END_DATE,A_ID) values ('3',to_date('01/01/14','DD/MM/RR'),to_date('01/06/14','DD/MM/RR'),'3');
Insert into C (ID,START_DATE,END_DATE,A_ID) values ('4',to_date('01/01/17','DD/MM/RR'),to_date('01/10/17','DD/MM/RR'),'1');
Insert into C (ID,START_DATE,END_DATE,A_ID) values ('5',to_date('01/04/13','DD/MM/RR'),to_date('01/10/13','DD/MM/RR'),'1');
Insert into C (ID,START_DATE,END_DATE,A_ID) values ('6',to_date('01/01/17','DD/MM/RR'),to_date('01/06/18','DD/MM/RR'),'2');

I need for each B: B.id, A.id and C.id, where C is the first oldest start_date row where B.year is in between C.start_date and C.end_date, like this:

BID AID CID
1   1   4
2   2   6
3   3   1
4   3   3
5   3   2
6   1   5

Please help me with an answer or a where to find the information to do this.

I know the nested select doesn't recognize the outside table A, but I don't know how to make a working solution for this or I'm missing something very simple; I don't know. I hope you can help me.

Solution

I had something like this with the help of the community:

select B.id BID, A.id AID, C.id CID
from B
    join A on B.A_id = A.id
    join C
    on C.A_id = A.id 
where B.year between extract (year from C.start_date) and extract(year from C.end_date)
order by B.id ASC , c.start_date asc;

It returned:

BID AID CID
1   1   4
2   2   6
3   3   1
4   3   1
4   3   3
5   3   2
6   1   5

But the real answer was:

select bid, aid, cid
from
  (select B.id bid, A.id aid, C.id cid, 
               row_number() over (partition by b.id order by c.start_date) rn
          from B
          join A on B.A_id = A.id
          join c on C.A_id = A.id
           and B.year between extract(year from c.start_date) 
                          and extract(year from c.end_date))
where rn = 1;

That returns what I was searching for.

Thanks to all, specially to Ponder Stibbons, that gave me the way to do it.

There are too many issues with your DDLs and the query.

Avoid oracle reserved keywords for column names. The one with end as column name will never run.

year   NUMBER(4) NOT NULL,
start     DATE NOT NULL,
end       DATE NOT NULL,

Instead use something like year_t,start_t, end_t or something that is meaningful.

I want to do a select with B.id, A.id and C.id where C is the first row where B.year is in between C.start and C.end, for every B.

You don't do it using and rownum < 1 . In fact rownum < 1 will never return any rows.

The column names you are using are also wrong. For eg: there is no b.start which you use in your order by .

So,considering those changes This query will work.

select B.id, A.id, C.id
from B
    join A on B.A_id = A.id
    join C
    on C.A_id = A.id 
where B.year_t between to_number(to_char(C.start_t, 'YYYY')) and to_number(to_char(C.end_t, 'YYYY')) 
            order by c.start_t,B.id ASC ; 

But, it won't give you

the first row where B.year is in between C.start and C.end, for every B

Clarify with data as to what exactly do you need in the question with some sample records. then i will try to answer.

Build join , number rows and take only first row for each id :

select bid, aid, cid 
  from (select B.id bid, A.id aid, C.id cid, 
               row_number() over (partition by c.a_id order by b.year) rn
          from B
          join A on B.A_id = A.id
          join c on C.A_id = A.id
           and B.year between extract(year from c.start_date) 
                          and extract(year from c.end_date))
  where rn = 1;

Test:

create table a (id number(3));
create table b (id number(6), year number(4), a_id   number(3));
create table c (id number(9), start_date date, end_date date, a_id  number(3));

insert into a values (1);
insert into a values (2);

insert into b values (1, 1901, 1);
insert into b values (2, 1905, 1);
insert into b values (3, 1903, 1);
insert into b values (4, 1902, 2);

insert into c values (1, date '1902-01-01', date '1912-07-17', 1);
insert into c values (2, date '1900-02-18', date '1910-06-08', 2);

Result:

    BID  AID        CID
------- ---- ----------
      3    1          1
      4    2          2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM