Мне кажется, что это операция с двумя фазами.
- Определите, какие части телефонного звонка используют какие тарифы и в какое время.
- Суммируйте время в каждом из курсов.
Этап 1 сложнее, чем этап 2. Я работал с примером в IBM Informix Dynamic Server (IDS), потому что у меня нет MS SQL Server. Идеи должны быть переведены достаточно легко. Предложение INTO TEMP создает временную таблицу с соответствующей схемой; таблица является закрытой для сеанса и исчезает, когда сеанс заканчивается (или вы явно удаляете ее). В IDS вы также можете использовать явный оператор CREATE TEMP TABLE, а затем INSERT INTO temp-table SELECT ... в качестве более подробного способа выполнения той же работы, что и INTO TEMP.
Как часто в вопросах SQL по SO, вы не предоставили нам схему, поэтому каждый должен придумать схему, которая может соответствовать или не соответствовать тому, что вы описываете.
Предположим, ваши данные находятся в двух таблицах. Первая таблица содержит записи в журнале вызовов, основную информацию о совершенных вызовах, такую как телефон, выполняющий вызов, номер вызываемого абонента, время начала вызова и продолжительность вызова:
CREATE TABLE clr -- call log record
(
phone_id VARCHAR(24) NOT NULL, -- billing plan
called_number VARCHAR(24) NOT NULL, -- needed to validate call
start_time TIMESTAMP NOT NULL, -- date and time when call started
duration INTEGER NOT NULL -- duration of call in seconds
CHECK(duration > 0),
PRIMARY KEY(phone_id, start_time)
-- other complicated range-based constraints omitted!
-- foreign keys omitted
-- there would probably be an auto-generated number here too.
);
INSERT INTO clr(phone_id, called_number, start_time, duration)
VALUES('650-656-3180', '650-794-3714', '2009-02-26 15:17:19', 186234);
Для удобства (в основном для сохранения записи сложения несколько раз) я хочу получить копию таблицы clr с фактическим временем окончания:
SELECT phone_id, called_number, start_time AS call_start, duration,
start_time + duration UNITS SECOND AS call_end
FROM clr
INTO TEMP clr_end;
Данные о тарифах хранятся в простой таблице:
CREATE TABLE tariff
(
tariff_code CHAR(1) NOT NULL -- code for the tariff
CHECK(tariff_code IN ('P','N','O'))
PRIMARY KEY,
rate_start TIME NOT NULL, -- time when rate starts
rate_end TIME NOT NULL, -- time when rate ends
rate_charged DECIMAL(7,4) NOT NULL -- rate charged (cents per second)
);
INSERT INTO tariff(tariff_code, rate_start, rate_end, rate_charged)
VALUES('N', '00:00:00', '08:00:00', 0.9876);
INSERT INTO tariff(tariff_code, rate_start, rate_end, rate_charged)
VALUES('P', '08:00:00', '19:00:00', 2.3456);
INSERT INTO tariff(tariff_code, rate_start, rate_end, rate_charged)
VALUES('O', '19:00:00', '23:59:59', 1.2345);
Я обсуждал, должна ли таблица тарифов использовать значения TIME или INTERVAL; в этом контексте времена очень похожи на интервалы относительно полуночи, но интервалы могут быть добавлены к временным меткам, где времена не могут. Я застрял с ВРЕМЯ, но это сделало вещи грязными.
Сложная часть этого запроса - генерирование соответствующих диапазонов даты и времени для каждого тарифа без петель. Фактически, я использовал цикл, встроенный в хранимую процедуру, для генерации списка целых чисел. (Я также использовал метод, специфичный для IBM Informix Dynamic Server, IDS, использующий номера идентификаторов таблиц из системного каталога в качестве источника непрерывных целых чисел в диапазоне 1..N, который работает для чисел от 1 до 60 в версии 11,50.)
CREATE PROCEDURE integers(lo INTEGER DEFAULT 0, hi INTEGER DEFAULT 0)
RETURNING INT AS number;
DEFINE i INTEGER;
FOR i = lo TO hi STEP 1
RETURN i WITH RESUME;
END FOR;
END PROCEDURE;
В простом случае (и наиболее распространенном случае) вызов приходится на однотарифный период; многопериодные звонки добавляют волнение.
Предположим, мы можем создать табличное выражение, соответствующее этой схеме и охватывающее все значения меток времени, которые нам могут понадобиться:
CREATE TEMP TABLE tariff_date_time
(
tariff_code CHAR(1) NOT NULL,
rate_start TIMESTAMP NOT NULL,
rate_end TIMESTAMP NOT NULL,
rate_charged DECIMAL(7,4) NOT NULL
);
К счастью, вы не упомянули тарифы на выходные дни, поэтому с клиентов взимается одинаковая сумма
цены в выходные дни как в течение недели. Тем не менее, ответ должен адаптироваться к таким
ситуаций, если это вообще возможно. Если бы вам нужно было так же сложно, как указывать выходные дни на
праздничные дни, за исключением того, что в Рождество или Новый год вы взимаете пиковую ставку вместо
тариф выходного дня из-за высокого спроса, тогда лучше всего хранить ставки в постоянной таблице тарифов_дата_времени.
Первый шаг в заполнении тарифа_дата_времени состоит в создании списка дат, которые относятся к вызовам:
SELECT DISTINCT EXTEND(DATE(call_start) + number, YEAR TO SECOND) AS call_date
FROM clr_end,
TABLE(integers(0, (SELECT DATE(call_end) - DATE(call_start) FROM clr_end)))
AS date_list(number)
INTO TEMP call_dates;
Разница между двумя значениями даты - целое число дней (в IDS).
Процедура целых чисел генерирует значения от 0 до количества дней, охватываемых вызовом, и сохраняет результат во временной таблице. Для более общего случая нескольких записей может быть лучше рассчитать минимальную и максимальную даты и сгенерировать промежуточные даты, а не генерировать даты несколько раз, а затем устранить их с помощью предложения DISTINCT.
Теперь используйте декартово произведение таблицы тарифов с таблицей call_dates, чтобы генерировать информацию о ставках для каждого дня. Здесь время тарифа будет точнее как интервалы.
SELECT r.tariff_code,
d.call_date + (r.rate_start - TIME '00:00:00') AS rate_start,
d.call_date + (r.rate_end - TIME '00:00:00') AS rate_end,
r.rate_charged
FROM call_dates AS d, tariff AS r
INTO TEMP tariff_date_time;
Теперь нам нужно сопоставить запись в журнале звонков с действующими тарифами. Условие является стандартным способом борьбы с перекрытиями - два периода времени перекрываются, если конец первого более поздний, чем начало второго, и если начало первого предшествует концу второго:
SELECT tdt.*, clr_end.*
FROM tariff_date_time tdt, clr_end
WHERE tdt.rate_end > clr_end.call_start
AND tdt.rate_start < clr_end.call_end
INTO TEMP call_time_tariff;
Затем нам нужно установить время начала и окончания тарифа. Время начала тарифа - это самое позднее время начала тарифа и время начала звонка. Время окончания тарифа - это более раннее время окончания тарифа и время окончания звонка:
SELECT phone_id, called_number, tariff_code, rate_charged,
call_start, duration,
CASE WHEN rate_start < call_start THEN call_start
ELSE rate_start END AS rate_start,
CASE WHEN rate_end >= call_end THEN call_end
ELSE rate_end END AS rate_end
FROM call_time_tariff
INTO TEMP call_time_tariff_times;
Наконец, нам нужно сложить время, потраченное на каждую тарифную ставку, и взять это время (в секундах) и умножить на начисленную стоимость. Поскольку результатом SUM (rate_end - rate_start) является ИНТЕРВАЛ, а не число, мне пришлось вызвать функцию преобразования для преобразования ИНТЕРВАЛА в ДЕСЯТИЧНОЕ число секунд, и эта (нестандартная) функция - iv_seconds:
SELECT phone_id, called_number, tariff_code, rate_charged,
call_start, duration,
SUM(rate_end - rate_start) AS tariff_time,
rate_charged * iv_seconds(SUM(rate_end - rate_start)) AS tariff_cost
FROM call_time_tariff_times
GROUP BY phone_id, called_number, tariff_code, rate_charged,
call_start, duration;
Для данных примера это дало данные (где я не печатаю номер телефона и вызываемый номер для компактности):
N 0.9876 2009-02-26 15:17:19 186234 0 16:00:00 56885.760000000
O 1.2345 2009-02-26 15:17:19 186234 0 10:01:11 44529.649500000
P 2.3456 2009-02-26 15:17:19 186234 1 01:42:41 217111.081600000
Это очень дорогой звонок, но телефон будет рад этому. Вы можете нажать на любой из промежуточных результатов, чтобы увидеть, как ответ получен. Вы можете использовать меньше временных таблиц за счет некоторой ясности.
Для одного вызова это не будет сильно отличаться от запуска кода в VB на клиенте. Для многих звонков это может быть более эффективным. Я далек от убеждения, что в VB необходима рекурсия - прямой итерации должно быть достаточно.