Расчет разных тарифных периодов для звонка в 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)

Мне кажется, что это операция в два этапа.

  1. Определите, какие части телефонного звонка используют, какие тарифы и в какое время.
  2. Суммируйте время в каждой из ставок.

Фаза 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
)

Я думаю, что есть три случая, которые следует рассмотреть (может быть, больше, я придумываю это по ходу дела):

  1. звонок происходит полностью в пределах одного тарифного периода
  2. звонок начинается в одном тарифном периоде (а) и заканчивается в следующем (б)
  3. звонок охватывает как минимум один полный тарифный период

Предполагая, что скорость указана в секунду, я думаю, вы могли бы создать что-то вроде следующего (полностью непроверенного) запроса

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
}