Страница 1 из 1

Справочник с интервалом дат

Добавлено: 10 июл 2008, 18:40
AnViSe
Задача:
Нужно сделать справочник процентных ставок которые действуют с какой-то даты и по какую-то дату.
В результате надо получить процентную ставку действовавшую на определенную дату.

Предполагаемое решение:
Завести два поля типа дата в которых будут занесены даты начала и окончания действия это % ставки. Затем BETWEEN -ом найти нужноую строку.

Проблема:
Как быть с % ставкой действующей на текущий момент, у нее нет даты окончания. Как вариант можно занести в это поле заоблачную дату (типа 3000 год). Но тогда при вводе новой ставки надо у предыдущих изменять эту дату на другую - реальную, а для новой опять ставить заоблачную...

Вопрос:
Может есть какое более изящное решение?

Добавлено: 10 июл 2008, 18:47
mdfv
Хранить дату начала действия ставки,
а потом выбирать ближайшую к искомой вниз
типа этого:

Код: Выделить всё

select * from stavki
where startdate<=:искомой
order by startdate desc
rows 1

Добавлено: 10 июл 2008, 18:59
AnViSe
Т.е. для FB будет типа следующего:

Код: Выделить всё

select frist 1 * from stavki 
where startdate<=:искомой 
order by startdate desc
Правильно?

Добавлено: 10 июл 2008, 19:07
WildSery
Вариант без диапазона медленнее, на существенном объёме данных будет тормозить, и не предусматривает варианта, когда в какой-то период вообще нет процентной ставки.
Я бы диапазоном делал. А что апдейтить надо - ничего страшного не вижу.

У меня это выглядит примерно так (изменения вношу процедурой):

Код: Выделить всё

MaxDate = '01.01.3000';

/* находим ставку, если уже есть в архиве */
ArchValue=0; ArchDateBeg='01.01.1900'; RecID=null;
select RecID, aValue, DateBeg, DateEnd
  from Archive
  where ValId = :ValId and :CheckDate between DateBeg and DateEnd
  into RecID, ArchValue, ArchDateBeg, ArchDateEnd;

/* если значение отличается от существующей записи */
if (ArchiveValue != aValue) then begin
    /* если дата начала совпадает с найденным диапазоном, то меняем существующую запись */
    if (ArchDateBeg = CheckDate) then
      update Archive
        set aValue = :NewValue, DateBeg = :CheckDate, DateEnd = :MaxDate
        where RecID = :RecID and
              aValue != :NewValue and DateBeg != :CheckDate and DateEnd != :MaxDate;
    else
    /* иначе проверяем изменение значения */
    if (ArchDateBeg < CheckDate) then begin
      /* закрываем предыдущий диапазон */
      if (RecID is not null) then
        update Archive set DateEnd = :CheckDate-1 where RecID = :RecID;
      /* вставляем новый */
      insert into Archive (ValId, aValue, DateBeg, DateEnd)
        values (:ValId, :NewValue, :CheckDate, :MaxDate);
    end
end
Особенность такого подхода - можно сколько угодно раз в день вставлять "текущие" значения процедурой - количество диапазонов не изменится, будет запомнено последнее решение.

Добавлено: 10 июл 2008, 19:12
mdfv
версия сервера какая?
Вариант без диапазона медленнее, и не предусматривает варианта, когда в какой-то период вообще нет процентной ставки.
Так ведь такая ситуация имеет место быть, что ставки когда-то небыло.

Re: Справочник с интервалом дат

Добавлено: 10 июл 2008, 19:53
Merlin
AnViSe писал(а): Может есть какое более изящное решение?
Хранить только дату начала.

Добавлено: 10 июл 2008, 20:00
Merlin
WildSery писал(а):Вариант без диапазона медленнее, на существенном объёме данных будет тормозить
:shock:
WildSery писал(а): и не предусматривает варианта, когда в какой-то период вообще нет процентной ставки.
Это не есть факт, мистер Дюк :wink: Нулл - секретное оружие пролетарьята :)
WildSery писал(а): Я бы диапазоном делал. А что апдейтить надо - ничего страшного не вижу.
Обеспечения условия непересекаемости диапазонов в конкурентной среде - в общем случае задача нерешаемая на вразумительных уровнях изоляции транзакций.
WildSery писал(а): Особенность такого подхода - можно сколько угодно раз в день вставлять "текущие" значения процедурой
А особеннось подхода с хранением только начала диапазона - можно сколько угодно раз в день вставлять "текущие" значения простым инсёртом :)

Добавлено: 10 июл 2008, 21:00
armagedon2007
Можно так

Код: Выделить всё

CREATE TABLE NEW_TABLE (
    ADATE  TIMESTAMP NOT NULL,
    VAL    INTEGER NOT NULL
);
И выбирать

Код: Выделить всё

WITH RECURSIVE
    DD (SDATE, EDATE, VAL) AS
    (SELECT FIRST 1 B.ADATE, (SELECT FIRST 1 DATEADD(SECOND, -1, D.ADATE) FROM NEW_TABLE D WHERE D.ADATE > B.ADATE), VAL
    FROM NEW_TABLE B
    UNION ALL
    SELECT FIRST 1 A.ADATE, (SELECT FIRST 1 DATEADD(SECOND, -1, D.ADATE) FROM NEW_TABLE D WHERE D.ADATE > A.ADATE), A.VAL
    FROM NEW_TABLE A, DD
    WHERE A.ADATE > DD.SDATE
    )
SELECT * FROM DD
WHERE :ADATE BETWEEN SDATE AND  COALESCE(EDATE, '01.01.5000')
только сколько раз оно по справочнику пробежит чтобы выбрать одно зачете смотрите сами

Добавлено: 10 июл 2008, 21:43
armagedon2007
WildSery писал(а): У меня это выглядит примерно так (изменения вношу процедурой):
А что будет в твоем примере если между 01.01.2008 и 31.01.2008
добавить новое значение допустим на 20.01.2008
DateEnd на 20.01.2008 станет 01.01.3000?

Добавлено: 11 июл 2008, 11:44
WildSery
Merlin писал(а)::shock:
Я не претендую на идеальную прямоту рук, но в моих тестах заметно быстрее было диапазоном.
Ты меня засмущал своим авторитетом, пойду ещё потестирую.
Merlin писал(а):Нулл - секретное оружие пролетарьята :)
В поле значения? Нда, действительно вариант. (Не люблю я эти нуллы...)
Merlin писал(а):Обеспечения условия непересекаемости диапазонов в конкурентной среде - в общем случае задача нерешаемая на вразумительных уровнях изоляции транзакций.
Задача задаче рознь. Я наивно считаю, что выставление почти справочных величин, типа ставок чего-нибудь, или курсов валют, или торговых цен, дело сугубо монопольное.
Merlin писал(а):А особеннось подхода с хранением только начала диапазона - можно сколько угодно раз в день вставлять "текущие" значения простым инсёртом :)
...А лучше опять-таки проверять существование нужной записи, потому как "select value from" в конкретную дату писать всегда удобнее, чем "select first 1 value from" ;) Кроме того, это ещё и инстрУмент, чтобы донести до разработчика что с БД что-то не так, если должно быть ограничение на хранение одного значения в конкретную дату, экстремальным методом "multiple rows..."
armagedon2007 писал(а):А что будет в твоем примере...
У меня так не будет :)
Нужна дополнительный алгоритм для внесения таких данных. Типа, чего вообще хотим получить такой вставкой.
И, кстати, не факт, что такая "вставка задним числом" в структуру хранения "датой начала" будет соответствовать ожиданиям. Не во всех задачах требуется тупо делить диапазон на два.

Добавлено: 11 июл 2008, 13:22
Gera
Как быть с % ставкой действующей на текущий момент, у нее нет даты окончания
У нас в базе для этих целей используется NULL.

Если текущее значение используется чаще чем прошлые, то можно добавить еще одну таблицу, хранящую текущее значение и его дату начала(для корректного переноса в архив)

Добавлено: 11 июл 2008, 15:50
WildSery
Gera писал(а):У нас в базе для этих целей используется NULL.
А каким образом выбирается этот "открытый" диапазон? Через coalesce(dateEnd, '01.01.3000')?

Добавлено: 11 июл 2008, 16:00
Gera
А каким образом выбирается этот "открытый" диапазон?
Через Begin_Date <= :Date AND (End_Date IS NULL OR End_Date >= :Date)

Просто NULL - это же значение "Неизвестно", что в данном случае, по моему, логически правильно.

Или же тогда использовать совсем запредельную дату 31.12.9999

Добавлено: 11 июл 2008, 18:43
Merlin
WildSery писал(а):лучше опять-таки проверять существование нужной записи, потому как "select value from" в конкретную дату писать всегда удобнее, чем "select first 1 value from" ;)
И ты, Брутто, про эстетику :)
WildSery писал(а): если должно быть ограничение на хранение одного значения в конкретную дату
Ты тут про курсы вспоминал... А я чо-то про Кириенковский дефолт вспомнил ;)

Добавлено: 17 июл 2008, 08:25
avenger
mdfv писал(а):Хранить дату начала действия ставки,
а потом выбирать ближайшую к искомой вниз
Именно так и делаю. А объединяю данные в процедуре с for select по данным и курсором по процентным ставкам за указанную дату.

Примерно так:

Код: Выделить всё

  WHILE (DATE_CURRENT <= DATE_END) DO
  BEGIN
    IF (DATE_CURRENT > DATE_TMP AND ROW_COUNT > 0) THEN
    BEGIN
      REST_CURRENT = COALESCE(REST_CURRENT, 0) + REST_TMP;
      IF (REST_CURRENT IS NOT NULL AND REST IS NOT NULL AND REST_CURRENT <> REST) THEN
        INTERVAL_ID = INTERVAL_ID + 1;
      FETCH SUM_OPERATIONS INTO :DATE_TMP, :REST_TMP;
    END

    REST   = REST_CURRENT;
    "DATE" = DATE_CURRENT;
    IF (REST IS NOT NULL) THEN
      SUSPEND;

    DATE_CURRENT = DATE_CURRENT + 1;
  END