Расчет разных тарифных периодов для звонка в SQL Server - PullRequest
2 голосов
/ 25 февраля 2009

Для системы оценки вызовов я пытаюсь разделить длительность телефонного разговора на под-длительности для разных тарифных периодов. Вызовы хранятся в базе данных SQL Server и имеют время начала и общую продолжительность. Тарифы различны для ночного (0000 - 0800), пикового (0800 - 1900) и непикового (1900-235959) периодов.

Например: Вызов начинается в 18:50:00 и длится 1000 секунд. Это завершит вызов в 19:06:40, сделав его 10 минут / 600 секунд в пиковом тарифе и 400 секунд в непиковом тарифе.

Очевидно, что вызов может охватывать неограниченное количество периодов (мы не устанавливаем максимальную продолжительность вызова). Вызов продолжительностью> 24 ч может охватывать все 3 периода, начиная с пика, заканчивая в непиковое время, ночью и обратно в пиковый тариф.

В настоящее время мы рассчитываем различные тарифные периоды с использованием рекурсии в VB. Мы рассчитываем, сколько вызовов происходит за тот же тарифный период, в котором начинается вызов, соответственно меняем время начала и продолжительность вызова и повторяем этот процесс до достижения полной продолжительности вызова (peakDuration + offpeakDuration + nightDuration == длительность звонка).

По этому вопросу у меня есть 2 вопроса:

  • Возможно ли сделать это эффективно в операторе SQL Server? (Я могу думать о подзапросах или большом количестве кодирования в хранимых процедурах, но это не приведет к улучшению производительности)

  • Сможет ли SQL Server выполнять такие вычисления более эффективно с точки зрения ресурсов, чем это делают нынешние сценарии VB?

Ответы [ 8 ]

2 голосов
/ 01 марта 2009

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

  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);

Я обсуждал, должна ли таблица тарифов использовать значения 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 необходима рекурсия - прямой итерации должно быть достаточно.

1 голос
/ 12 апреля 2011
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
}
0 голосов
/ 08 марта 2009

Большая проблема с выполнением такого рода вычислений на уровне базы данных заключается в том, что он забирает ресурсы из вашей базы данных во время ее работы, как с точки зрения ЦП, так и доступности строк и таблиц посредством блокировки. Если вы вычисляете 1 000 000 тарифов как часть пакетной операции, это может долго выполняться в базе данных, и в течение этого времени вы не сможете использовать базу данных ни для чего другого.

Если у вас есть ресурс, извлеките все необходимые данные за одну транзакцию и выполните все логические вычисления вне базы данных на выбранном вами языке. Затем вставьте все результаты. Базы данных предназначены для хранения и извлечения данных, и любая выполняемая ими бизнес-логика должна всегда оставаться на абсолютном минимуме. Несмотря на блестящие результаты, SQL не является лучшим языком для работы с датами и строками.

Я подозреваю, что вы уже на правильном пути в своей работе с VBA, и, не зная больше, это определенно кажется мне рекурсивной или, по крайней мере, итерационной проблемой. Если все сделано правильно, рекурсия может стать мощным и элегантным решением проблемы. Связывание ресурсов вашей базы данных очень редко.

0 голосов
/ 25 февраля 2009

При условии, что ваши звонки длятся менее 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

Я предполагаю, что вы сохраняете диапазоны своих тарифов в минутах от полуночи, а также рассчитывает продолжительность в минутах.

0 голосов
/ 25 февраля 2009

Фактическая схема для соответствующих таблиц в вашей базе данных была бы очень полезна. Я возьму мои лучшие догадки. Я предположил, что таблица Rates имеет начальное и конечное время в виде количества минут после полуночи.

Использование таблицы календаря (очень полезная таблица в большинстве баз данных):

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

Я только что придумал это, когда набирал текст, так что он не проверен, и вам, скорее всего, придется его настроить, но, надеюсь, вы поймете общую идею.

Я также только что понял, что вы используете время начала и время для ваших звонков. Вы можете просто заменить C.end_time везде, где вы его видите, на DATEADD (ss, C.start_time, C.duration), предполагая, что продолжительность указана в секундах.

Это должно выполняться довольно быстро в любой достойной СУБД, при условии правильных индексов и т. Д.

0 голосов
/ 25 февраля 2009

Исходя из ответа Майка Вудхауса, это может работать на вас:

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
0 голосов
/ 25 февраля 2009

Это ветка о вашей проблеме, которая возникла на sqlteam.com. взгляните, потому что он включает в себя несколько приятных решений.

0 голосов
/ 25 февраля 2009

Эффективно в 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 или обработать его в своем коде. В качестве альтернативы, с тщательно продуманной логикой, вы, вероятно, могли бы объединить части UNIONed в одно предложение WHERE с множеством AND и OR. Я думал, что СОЮЗ показал намерение более ясно.

HTH & HIW (надеюсь, это работает ...)

...