Страница 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 писал(а):Вариант без диапазона медленнее, на существенном объёме данных будет тормозить
WildSery писал(а):
и не предусматривает варианта, когда в какой-то период вообще нет процентной ставки.
Это не есть факт, мистер Дюк
Нулл - секретное оружие пролетарьята
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 писал(а):
Я не претендую на идеальную прямоту рук, но в моих тестах заметно быстрее было диапазоном.
Ты меня засмущал своим авторитетом, пойду ещё потестирую.
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 <=
ate AND (End_Date IS NULL OR End_Date >=
ate)
Просто 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