Расчет разных тарифных периодов для звонка в SQL Server
Для системы оценки звонков я пытаюсь разделить продолжительность телефонного разговора на части для разных тарифных периодов. Вызовы хранятся в базе данных SQL Server и имеют время начала и общую продолжительность. Ставки разные для ночного (0000 - 08:00), пикового (08:00 - 19:00) и непикового (1900-235959) периодов.
Например: звонок начинается в 18:50:00 и длится 1000 секунд. Это приведет к завершению вызова в 19:06:40, что составит 10 минут / 600 секунд для пикового тарифа и 400 секунд для внепикового тарифа.
Очевидно, что вызов может длиться неограниченное количество периодов (мы не устанавливаем максимальную продолжительность вызова). Звонок продолжительностью более 24 часов может включать все 3 периода, начиная с пикового, продолжающегося в непиковое время, ночью и обратно в пиковый тариф.
В настоящее время мы рассчитываем различные тарифные периоды с помощью рекурсии в VB. Мы вычисляем, какая часть звонка приходится на тот же тарифный период, в котором начинается звонок, соответственно меняем время начала и продолжительность звонка и повторяем этот процесс до тех пор, пока не будет достигнута полная продолжительность звонка (пиковая длительность + offpeakDuration + nightDuration == длительность звонка).
По этому поводу у меня 2 вопроса:
Можно ли сделать это эффективно в операторе SQL Server? (Я могу думать о подзапросах или большом количестве кода в хранимых процедурах, но это не приведет к улучшению производительности)
Сможет ли SQL Server выполнять такие вычисления более эффективным с точки зрения ресурсов способом, чем это делают текущие сценарии VB?
Ответов (8)8
Мне кажется, что это операция в два этапа.
- Определите, какие части телефонного звонка используют, какие тарифы и в какое время.
- Суммируйте время в каждой из ставок.
Фаза 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);
Я обсуждал, следует ли использовать в таблице тарифов значения ВРЕМЯ или ИНТЕРВАЛ; в этом контексте время очень похоже на интервалы относительно полуночи, но интервалы могут быть добавлены к отметкам времени, когда время не может. Я придерживался ВРЕМЕНИ, но это сделало вещи беспорядочными.
Сложная часть этого запроса - без зацикливания генерировать соответствующие диапазоны дат и времени для каждого тарифа. Фактически, я закончил тем, что использовал цикл, встроенный в хранимую процедуру, для создания списка целых чисел. (Я также использовал метод, характерный для 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
);
К счастью, вы не упомянули тарифы на выходные, поэтому вы берете с клиентов столько же.
расценки в выходные дни как и в будние дни. Однако ответ должен адаптироваться к таким
ситуации, если это вообще возможно. Если бы вы стали так сложны, как расценки на выходные на
государственные праздники, за исключением Рождества или Нового года, вы взимаете пиковую ставку вместо
тариф в выходные дни из-за высокого спроса, тогда вам будет лучше хранить ставки в постоянной таблице price_date_time.
Первым шагом при заполнении параметра price_date_time является создание списка дат, относящихся к звонкам:
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 необходима рекурсия - достаточно прямой итерации.
Это ветка о вашей проблеме на sqlteam.com. взгляните, потому что он включает в себя несколько довольно хороших решений.
Следуя ответу Майка Вудхауса, это может сработать для вас:
SELECT id, SUM(DATEDIFF(ss, started, ended) * rate)
FROM rates
JOIN calls ON
CASE WHEN started < from_date_time
THEN DATEADD(ss, 1, from_date_time)
ELSE started > from_date_time
AND
CASE WHEN ended > to_date_time
THEN DATEADD(ss, -1, to_date_time)
ELSE ended END
< ended
GROUP BY id
Эффективно в T-SQL? Я подозреваю, что нет, со схемой, описанной в настоящее время.
Однако это может быть возможно, если в вашем прейскуранте будут храниться три тарифа для каждой даты. Есть по крайней мере одна причина, по которой вы можете это сделать, помимо существующей проблемы: вполне вероятно, что в какой-то момент ставки за тот или иной период могут измениться, и вам может потребоваться доступность исторических ставок.
Допустим, у нас есть эти таблицы:
CREATE TABLE rates (
from_date_time DATETIME
, to_date_time DATETIME
, rate MONEY
)
CREATE TABLE calls (
id INT
, started DATETIME
, ended DATETIME
)
Я думаю, что есть три случая, которые следует рассмотреть (может быть, больше, я придумываю это по ходу дела):
- звонок происходит полностью в пределах одного тарифного периода
- звонок начинается в одном тарифном периоде (а) и заканчивается в следующем (б)
- звонок охватывает как минимум один полный тарифный период
Предполагая, что скорость указана в секунду, я думаю, вы могли бы создать что-то вроде следующего (полностью непроверенного) запроса
SELECT id, DATEDIFF(ss, started, ended) * rate /* case 1 */
FROM rates JOIN calls ON started > from_date_time AND ended < to_date_time
UNION
SELECT id, DATEDIFF(ss, started, to_date_time) * rate /* case 2a and the start of case 3 */
FROM rates JOIN calls ON started > from_date_time AND ended > to_date_time
UNION
SELECT id, DATEDIFF(ss, from_date_time, ended) * rate /* case 2b and the last part of case 3 */
FROM rates JOIN calls ON started < from_date_time AND ended < to_date_time
UNION
SELECT id, DATEDIFF(ss, from_date_time, to_date_time) * rate /* case 3 for entire rate periods, should pick up all complete periods */
FROM rates JOIN calls ON started < from_date_time AND ended > to_date_time
Вы можете применить SUM..GROUP BY к этому в SQL или обработать его в своем коде. В качестве альтернативы, с тщательно продуманной логикой, вы, вероятно, могли бы объединить объединенные части в одно предложение WHERE с множеством операторов AND и OR. Я думал, что СОЮЗ демонстрирует намерения более ясно.
HTH и HIW (надеюсь, что это сработает ...)
Фактическая схема для соответствующих таблиц в вашей базе данных была бы очень полезна. Я возьму свои лучшие догадки. Я предположил, что в таблице Rates есть start_time и end_time как количество минут после полуночи.
Использование календарной таблицы (ОЧЕНЬ полезная таблица для большинства баз данных):
SELECT
C.id,
R.rate,
SUM(DATEDIFF(ss,
CASE
WHEN C.start_time < R.rate_start_time THEN R.rate_start_time
ELSE C.start_time
END,
CASE
WHEN C.end_time > R.rate_end_time THEN R.rate_end_time
ELSE C.end_time
END)) AS
FROM
Calls C
INNER JOIN
(
SELECT
DATEADD(mi, Rates.start_time, CAL.calendar_date) AS rate_start_time,
DATEADD(mi, Rates.end_time, CAL.calendar_date) AS rate_end_time,
Rates.rate
FROM
Calendar CAL
INNER JOIN Rates ON
1 = 1
WHERE
CAL.calendar_date >= DATEADD(dy, -1, C.start_time) AND
CAL.calendar_date <= C.start_time
) AS R ON
R.rate_start_time < C.end_time AND
R.rate_end_time > C.start_time
GROUP BY
C.id,
R.rate
Я только что придумал это, когда набирал текст, так что он не тестировался, и вам, скорее всего, придется его настроить, но, надеюсь, вы понимаете общую идею.
Я также только что понял, что вы используете start_time и продолжительность для своих звонков. Вы можете просто заменить C.end_time везде, где вы его видите, на DATEADD (ss, C.start_time, C.duration), предполагая, что продолжительность указывается в секундах.
Это должно работать довольно быстро в любой приличной СУБД, предполагающей правильные индексы и т. Д.
При условии, что ваши звонки длятся менее 100
суток:
WITH generate_range(item) AS
(
SELECT 0
UNION ALL
SELECT item + 1
FROM generate_range
WHERE item < 100
)
SELECT tday, id, span
FROM (
SELECT tday, id,
DATEDIFF(minute,
CASE WHEN tbegin < clbegin THEN clbegin ELSE tbegin END,
CASE WHEN tend < clend THEN tend ELSE clend END
) AS span
FROM (
SELECT DATEADD(day, item, DATEDIFF(day, 0, clbegin)) AS tday,
ti.id,
DATEADD(minute, rangestart, DATEADD(day, item, DATEDIFF(day, 0, clbegin))) AS tbegin,
DATEADD(minute, rangeend, DATEADD(day, item, DATEDIFF(day, 0, clbegin))) AS tend
FROM calls, generate_range, tariff ti
WHERE DATEADD(day, 1, DATEDIFF(day, 0, clend)) > DATEADD(day, item, DATEDIFF(day, 0, clbegin))
) t1
) t2
WHERE span > 0
Я предполагаю, что вы сохраняете диапазоны тарифов в минутах, начиная с полуночи, и считаете их также в минутах.
Большая проблема с выполнением такого рода вычислений на уровне базы данных заключается в том, что он забирает ресурсы у вашей базы данных во время работы, как с точки зрения ЦП, так и с точки зрения доступности строк и таблиц через блокировку. Если бы вы рассчитывали 1 000 000 тарифов как часть пакетной операции, то это могло бы работать в базе данных в течение длительного времени, и в течение этого времени вы не смогли бы использовать базу данных для чего-либо еще.
Если у вас есть ресурс, извлеките все необходимые данные с помощью одной транзакции и выполните все логические вычисления вне базы данных на любом языке по вашему выбору. Затем вставьте все результаты. Базы данных предназначены для хранения и извлечения данных, и любая выполняемая ими бизнес-логика всегда должна быть сведена к минимуму. Несмотря на то, что в некоторых вещах он великолепен, SQL - не лучший язык для работы с датами или строками.
Я подозреваю, что вы уже на правильном пути в своей работе с VBA, и, не зная больше, для меня это определенно кажется рекурсивной или, по крайней мере, итеративной проблемой. Если все сделано правильно, рекурсия может стать мощным и элегантным решением проблемы. Очень редко приходится связывать ресурсы вашей базы данных.
kar_vasile(id,vid,datein,timein,timeout,bikari,tozihat)
{
--- the bikari field is unemployment time you can delete any where
select
id,
vid,
datein,
timein,
timeout,
bikari,
hourwork =
case when
timein <= timeout
then
SUM
(abs(DATEDIFF(mi, timein, timeout)) - bikari)/60 --
calculate Hour
else
SUM(abs(DATEDIFF(mi, timein, '23:59:00:00') + DATEDIFF(mi, '00:00:00', timeout) + 1) - bikari)/60 --
calculate
minute
end
,
minwork =
case when
timein <= timeout
then
SUM
(abs(DATEDIFF(MI, timein, timeout)) - bikari)%60 --
calclate Hour
starttime is later
than endtime
else
SUM(abs(DATEDIFF(mi, timein, '23:59:00:00') + DATEDIFF(mi, '00:00:00', timeout) + 1) - bikari)%60--
calculate minute
starttime is later
than
endtime
end, tozihat
from kar_vasile
group
by id, vid, datein, timein, timeout, tozihat, bikari
}