开发者

Automatic Adjusting Range Table

I have a table with a start date range, an end date range, and a few other additional columns. On input of a new record, I want to automatically adjust any overlapping date ranges (shrinking them, splitting them, or deleting them to allow for the new input -- see algorithm below). I also want to ensure that no overlapping records can accidentally be inserted into this table.

I'm using Oracle and Java for my application code. How should I enforce the prevention of overlapping date ranges and also allow for automatically adjusting overlapping ranges? Should I create an AFTER INSERT trigger, with a dbms_lock to serialize access, to prevent the overlapping data. Then in Java, apply the logic to auto adjust everything? Or should that part be in PL/SQL in stored procedure call? This is something that we need for a couple other tables so it'd be nice to abstract.

If anyone has something like this already written, please share :)

I did find this reference: http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:474221407101

Here's an example of how each of the 4 overlapping cases should be handled for adjustment on insert:

= Example 1 =
In DB (Start, End, Value):
(0, 10, 'X')
**(30, 100, 'Z')
(200, 500, 'Y')

Input
(20, 50, 'A')
Gives
(0, 10, 'X')
**(20, 50, 'A')
**(51, 100, 'Z')
(200, 500, 'Y')


= Example 2 =
In DB (Start, End, Value):
(0, 10, 'X')
**(30, 100, 'Z')
(200, 500, 'Y')

Input 
(40, 80, 'A')
Gives
(0, 10, 'X')
**(30, 39, 'Z')
**(40, 80, 'A')
**(81, 100, 'Z')
(200, 500, 'Y')


= Example 3 =
In DB (Start, End, Value):
(0, 10, 'X')
**(30, 100, 'Z')
(200, 500, 'Y')

Input
(50, 120, 'A')
Gives
(0, 10, 'X')
**(30, 49, 'Z')
**(50, 120, 'A')
(200, 500, 'Y')


= Example 4 =
In DB (Start, End, Value)开发者_开发知识库:
(0, 10, 'X')
**(30, 100, 'Z')
(200, 500, 'Y')

Input
(20, 120, 'A')
Gives
(0, 10, 'X')
**(20, 120, 'A')
(200, 500, 'Y')

The algorithm is as follows:

given range = g; input range  = i; output range set = o

if i.start <= g.start
  if i.end >= g.end
    o_1 = i
  else
    o_1 = i
    o_2 = (i.end + 1, g.end)
else
  if i.end >= g.end
    o_1 = (g.start, i.start - 1)
    o_2 = i
  else
    o_1 = (g.start, i.start - 1)
    o_2 = i
    o_3 = (i.end + 1, g.end)


I've generally seen data models like that having the starting point of the range being the only one being tracked, where the ending point is then implicit. So it'd be

CREATE TABLE MY_TABLE
(START_AT    NUMBER,
 VALUE       NUMBER,
 CONSTRAINT MY_TABLE_PK (START_AT)
);

If you need to present values in the existing format, you could use analytics and a materialized view, using LEAD(START_AT) OVER (ORDER BY START_AT) (I think it's right, but untested) to get the interpreted end value.


AskTom's article gives a good example of how it can be done, but note that this example locks the whole table which will severely impact concurrency of your application.

If concurrency is a matter for you, you should just add a sequence column (with ORDER option if you use RAC) and write a query like this:

SELECT  *
FROM    (
        SELECT  *, rownum AS rn
        FROM    mytable
        WHERE   start_date <= :date
                AND end_date >= :date
        ORDER BY
                seq DESC
        )
WHERE   rn = 1

to find out effective range (and other data) for a given date.

This will return the last inserted range containing the given date.

You can make this query a little bit more efficient by running a maintenance procedure that would get rid of the overlapping ranges on a timely basis (like described in the post) and rewriting the query like this:

SELECT  *
FROM    (
        SELECT  *, rownum AS rn2
        FROM    (
                SELECT  *
                FROM    (
                        SELECT  *, rownum AS rn
                        FROM    mytable
                        WHERE   seq <= :lseq
                                AND start_date <= :date
                                AND end_date >= :date
                        ORDER BY
                                start_date DESC
                        )
                WHERE   rn = 1
                UNION ALL
                SELECT  *
                FROM    (
                        SELECT  *, rownum AS rn
                        FROM    mytable
                        WHERE   seq > :lseq
                                AND start_date <= :date
                                AND end_date >= :date
                        ORDER BY
                                seq DESC
                        )
                WHERE   rn = 1
                )
        ORDER BY
                seq DESC
        )
WHERE   rn2 = 1

Create the indexes on start_date and seq for this to work fast.

The latter query will select the first matching range from the processed ranges (which are known to be non-overlapping), the first matching range from the unprocessed ranges (which are few) and of the two records, will select the one with the highest seq.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜